lesli_view 1.1.5 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +8 -0
- data/app/assets/stylesheets/lesli_view/{view.tailwind.css → view.css} +91 -7
- data/lib/lesli_view/components/timeline.html.erb +38 -25
- data/lib/lesli_view/components/timeline.rb +18 -5
- data/lib/lesli_view/forms/builder_horizontal.rb +4 -0
- data/lib/lesli_view/forms/fields.rb +14 -0
- data/lib/lesli_view/items/activities.html.erb +5 -1
- data/lib/lesli_view/items/activities.rb +111 -11
- data/lib/lesli_view/items/attachment.html.erb +45 -0
- data/lib/lesli_view/items/attachment.rb +63 -0
- data/lib/lesli_view/items/attachments.html.erb +69 -34
- data/lib/lesli_view/items/attachments.rb +38 -2
- data/lib/lesli_view/items/discussion.html.erb +7 -5
- data/lib/lesli_view/items/discussion.rb +26 -0
- data/lib/lesli_view/items/discussions.html.erb +32 -17
- data/lib/lesli_view/items/discussions.rb +27 -5
- data/lib/lesli_view/items/task.html.erb +15 -6
- data/lib/lesli_view/items/task.rb +14 -0
- data/lib/lesli_view/items/tasks.html.erb +28 -15
- data/lib/lesli_view/items/tasks.rb +27 -2
- data/lib/lesli_view/version.rb +2 -2
- data/lib/lesli_view.rb +1 -0
- data/readme.md +3 -3
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d42e452e24ee35eb0a02019b62748ffe8c2cdaa42da6a7047292deda3134a3d3
|
|
4
|
+
data.tar.gz: 8b70b0bfd4aa3f64600a35d4cc4c59bee46561ecb9978dc22b18f7b577c98c8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24785b4a4dfa224ce97181642e986fd4c51601e267c7b9dc3565003a073e6baf3c0fcacab9a87f98067b43eae15d2763a9f007dff48dc61e03a37433d45f9ec0
|
|
7
|
+
data.tar.gz: 94bedf88e3f7a79e63b59734e7c62f46b304d77110dab101494a15463685d47c57bb63ecc41d4915a11fd7605ebf141e5c86616c845ab4c0763228cdc156ffe3
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
require "bundler/setup"
|
|
2
|
+
require "rake/testtask"
|
|
2
3
|
|
|
3
4
|
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
5
|
load "rails/tasks/engine.rake"
|
|
5
6
|
|
|
6
7
|
require "bundler/gem_tasks"
|
|
8
|
+
|
|
9
|
+
Rake::TestTask.new(:test) do |test|
|
|
10
|
+
test.libs << "test"
|
|
11
|
+
test.pattern = "test/**/*_test.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
task default: :test
|
|
@@ -36,6 +36,9 @@ Building a better future, one line of code at a time.
|
|
|
36
36
|
:root, :host {
|
|
37
37
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,
|
|
38
38
|
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
39
|
+
--color-red-50: oklch(97.1% 0.013 17.38);
|
|
40
|
+
--color-red-500: oklch(63.7% 0.237 25.331);
|
|
41
|
+
--color-red-600: oklch(57.7% 0.245 27.325);
|
|
39
42
|
--color-amber-50: oklch(98.7% 0.022 95.277);
|
|
40
43
|
--color-amber-100: oklch(96.2% 0.059 95.617);
|
|
41
44
|
--color-amber-200: oklch(92.4% 0.12 95.746);
|
|
@@ -123,10 +126,12 @@ Building a better future, one line of code at a time.
|
|
|
123
126
|
--text-6xl: 3.75rem;
|
|
124
127
|
--text-6xl--line-height: 1;
|
|
125
128
|
--font-weight-light: 300;
|
|
129
|
+
--font-weight-normal: 400;
|
|
126
130
|
--font-weight-medium: 500;
|
|
127
131
|
--font-weight-semibold: 600;
|
|
128
132
|
--font-weight-bold: 700;
|
|
129
133
|
--tracking-tight: -0.025em;
|
|
134
|
+
--tracking-normal: 0em;
|
|
130
135
|
--tracking-wide: 0.025em;
|
|
131
136
|
--radius-sm: 0.25rem;
|
|
132
137
|
--radius-md: 0.375rem;
|
|
@@ -374,9 +379,6 @@ Building a better future, one line of code at a time.
|
|
|
374
379
|
--tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
|
|
375
380
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
376
381
|
}
|
|
377
|
-
.cursor-not-allowed {
|
|
378
|
-
cursor: not-allowed;
|
|
379
|
-
}
|
|
380
382
|
.cursor-pointer {
|
|
381
383
|
cursor: pointer;
|
|
382
384
|
}
|
|
@@ -606,6 +608,9 @@ Building a better future, one line of code at a time.
|
|
|
606
608
|
.p-6 {
|
|
607
609
|
padding: calc(var(--spacing) * 6);
|
|
608
610
|
}
|
|
611
|
+
.px-3 {
|
|
612
|
+
padding-inline: calc(var(--spacing) * 3);
|
|
613
|
+
}
|
|
609
614
|
.px-4 {
|
|
610
615
|
padding-inline: calc(var(--spacing) * 4);
|
|
611
616
|
}
|
|
@@ -615,6 +620,9 @@ Building a better future, one line of code at a time.
|
|
|
615
620
|
.py-0\.5 {
|
|
616
621
|
padding-block: calc(var(--spacing) * 0.5);
|
|
617
622
|
}
|
|
623
|
+
.py-2 {
|
|
624
|
+
padding-block: calc(var(--spacing) * 2);
|
|
625
|
+
}
|
|
618
626
|
.py-2\.5 {
|
|
619
627
|
padding-block: calc(var(--spacing) * 2.5);
|
|
620
628
|
}
|
|
@@ -727,6 +735,10 @@ Building a better future, one line of code at a time.
|
|
|
727
735
|
--tw-font-weight: var(--font-weight-medium);
|
|
728
736
|
font-weight: var(--font-weight-medium);
|
|
729
737
|
}
|
|
738
|
+
.font-normal {
|
|
739
|
+
--tw-font-weight: var(--font-weight-normal);
|
|
740
|
+
font-weight: var(--font-weight-normal);
|
|
741
|
+
}
|
|
730
742
|
.font-semibold {
|
|
731
743
|
--tw-font-weight: var(--font-weight-semibold);
|
|
732
744
|
font-weight: var(--font-weight-semibold);
|
|
@@ -735,6 +747,10 @@ Building a better future, one line of code at a time.
|
|
|
735
747
|
--tw-tracking: 0.2em;
|
|
736
748
|
letter-spacing: 0.2em;
|
|
737
749
|
}
|
|
750
|
+
.tracking-normal {
|
|
751
|
+
--tw-tracking: var(--tracking-normal);
|
|
752
|
+
letter-spacing: var(--tracking-normal);
|
|
753
|
+
}
|
|
738
754
|
.tracking-tight {
|
|
739
755
|
--tw-tracking: var(--tracking-tight);
|
|
740
756
|
letter-spacing: var(--tracking-tight);
|
|
@@ -752,6 +768,9 @@ Building a better future, one line of code at a time.
|
|
|
752
768
|
.whitespace-nowrap {
|
|
753
769
|
white-space: nowrap;
|
|
754
770
|
}
|
|
771
|
+
.whitespace-pre-wrap {
|
|
772
|
+
white-space: pre-wrap;
|
|
773
|
+
}
|
|
755
774
|
.text-\[var\(--lesli-color-primary\)\] {
|
|
756
775
|
color: var(--lesli-color-primary);
|
|
757
776
|
}
|
|
@@ -764,6 +783,9 @@ Building a better future, one line of code at a time.
|
|
|
764
783
|
.text-cyan-500 {
|
|
765
784
|
color: var(--color-cyan-500);
|
|
766
785
|
}
|
|
786
|
+
.text-red-600 {
|
|
787
|
+
color: var(--color-red-600);
|
|
788
|
+
}
|
|
767
789
|
.text-sky-600 {
|
|
768
790
|
color: var(--color-sky-600);
|
|
769
791
|
}
|
|
@@ -788,12 +810,18 @@ Building a better future, one line of code at a time.
|
|
|
788
810
|
.text-slate-800 {
|
|
789
811
|
color: var(--color-slate-800);
|
|
790
812
|
}
|
|
813
|
+
.text-slate-900 {
|
|
814
|
+
color: var(--color-slate-900);
|
|
815
|
+
}
|
|
791
816
|
.text-white {
|
|
792
817
|
color: var(--color-white);
|
|
793
818
|
}
|
|
794
819
|
.capitalize {
|
|
795
820
|
text-transform: capitalize;
|
|
796
821
|
}
|
|
822
|
+
.normal-case {
|
|
823
|
+
text-transform: none;
|
|
824
|
+
}
|
|
797
825
|
.uppercase {
|
|
798
826
|
text-transform: uppercase;
|
|
799
827
|
}
|
|
@@ -876,6 +904,33 @@ Building a better future, one line of code at a time.
|
|
|
876
904
|
.group-last\:hidden:is(:where(.group):last-child *) {
|
|
877
905
|
display: none;
|
|
878
906
|
}
|
|
907
|
+
.file\:mr-4::file-selector-button {
|
|
908
|
+
margin-right: calc(var(--spacing) * 4);
|
|
909
|
+
}
|
|
910
|
+
.file\:border-0::file-selector-button {
|
|
911
|
+
border-style: var(--tw-border-style);
|
|
912
|
+
border-width: 0px;
|
|
913
|
+
}
|
|
914
|
+
.file\:bg-slate-100::file-selector-button {
|
|
915
|
+
background-color: var(--color-slate-100);
|
|
916
|
+
}
|
|
917
|
+
.file\:px-4::file-selector-button {
|
|
918
|
+
padding-inline: calc(var(--spacing) * 4);
|
|
919
|
+
}
|
|
920
|
+
.file\:py-2\.5::file-selector-button {
|
|
921
|
+
padding-block: calc(var(--spacing) * 2.5);
|
|
922
|
+
}
|
|
923
|
+
.file\:text-sm::file-selector-button {
|
|
924
|
+
font-size: var(--text-sm);
|
|
925
|
+
line-height: var(--tw-leading, var(--text-sm--line-height));
|
|
926
|
+
}
|
|
927
|
+
.file\:font-medium::file-selector-button {
|
|
928
|
+
--tw-font-weight: var(--font-weight-medium);
|
|
929
|
+
font-weight: var(--font-weight-medium);
|
|
930
|
+
}
|
|
931
|
+
.file\:text-slate-700::file-selector-button {
|
|
932
|
+
color: var(--color-slate-700);
|
|
933
|
+
}
|
|
879
934
|
.last\:border-b-0:last-child {
|
|
880
935
|
border-bottom-style: var(--tw-border-style);
|
|
881
936
|
border-bottom-width: 0px;
|
|
@@ -887,12 +942,21 @@ Building a better future, one line of code at a time.
|
|
|
887
942
|
.hover\:border-slate-300:hover {
|
|
888
943
|
border-color: var(--color-slate-300);
|
|
889
944
|
}
|
|
945
|
+
.hover\:border-slate-400:hover {
|
|
946
|
+
border-color: var(--color-slate-400);
|
|
947
|
+
}
|
|
948
|
+
.hover\:bg-red-50:hover {
|
|
949
|
+
background-color: var(--color-red-50);
|
|
950
|
+
}
|
|
890
951
|
.hover\:bg-sky-50:hover {
|
|
891
952
|
background-color: var(--color-sky-50);
|
|
892
953
|
}
|
|
893
954
|
.hover\:bg-slate-50:hover {
|
|
894
955
|
background-color: var(--color-slate-50);
|
|
895
956
|
}
|
|
957
|
+
.hover\:bg-slate-100:hover {
|
|
958
|
+
background-color: var(--color-slate-100);
|
|
959
|
+
}
|
|
896
960
|
.hover\:text-sky-800:hover {
|
|
897
961
|
color: var(--color-sky-800);
|
|
898
962
|
}
|
|
@@ -902,11 +966,17 @@ Building a better future, one line of code at a time.
|
|
|
902
966
|
.hover\:underline:hover {
|
|
903
967
|
text-decoration-line: underline;
|
|
904
968
|
}
|
|
969
|
+
.hover\:file\:bg-slate-200:hover::file-selector-button {
|
|
970
|
+
background-color: var(--color-slate-200);
|
|
971
|
+
}
|
|
905
972
|
}
|
|
906
973
|
.focus\:ring-2:focus {
|
|
907
974
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
908
975
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
909
976
|
}
|
|
977
|
+
.focus\:ring-\[var\(--lesli-color-primary\)\]:focus {
|
|
978
|
+
--tw-ring-color: var(--lesli-color-primary);
|
|
979
|
+
}
|
|
910
980
|
.focus\:ring-sky-500\/30:focus {
|
|
911
981
|
--tw-ring-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 30%, transparent);
|
|
912
982
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -933,6 +1003,12 @@ Building a better future, one line of code at a time.
|
|
|
933
1003
|
.focus-visible\:ring-blue-500:focus-visible {
|
|
934
1004
|
--tw-ring-color: var(--color-blue-500);
|
|
935
1005
|
}
|
|
1006
|
+
.focus-visible\:ring-red-500\/40:focus-visible {
|
|
1007
|
+
--tw-ring-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 40%, transparent);
|
|
1008
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1009
|
+
--tw-ring-color: color-mix(in oklab, var(--color-red-500) 40%, transparent);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
936
1012
|
.focus-visible\:outline-2:focus-visible {
|
|
937
1013
|
outline-style: var(--tw-outline-style);
|
|
938
1014
|
outline-width: 2px;
|
|
@@ -950,9 +1026,6 @@ Building a better future, one line of code at a time.
|
|
|
950
1026
|
.focus-visible\:ring-inset:focus-visible {
|
|
951
1027
|
--tw-ring-inset: inset;
|
|
952
1028
|
}
|
|
953
|
-
.disabled\:cursor-not-allowed:disabled {
|
|
954
|
-
cursor: not-allowed;
|
|
955
|
-
}
|
|
956
1029
|
@media (prefers-reduced-motion: no-preference) {
|
|
957
1030
|
.motion-safe\:animate-pulse {
|
|
958
1031
|
animation: var(--animate-pulse);
|
|
@@ -980,6 +1053,12 @@ Building a better future, one line of code at a time.
|
|
|
980
1053
|
.sm\:flex-row {
|
|
981
1054
|
flex-direction: row;
|
|
982
1055
|
}
|
|
1056
|
+
.sm\:items-center {
|
|
1057
|
+
align-items: center;
|
|
1058
|
+
}
|
|
1059
|
+
.sm\:justify-between {
|
|
1060
|
+
justify-content: space-between;
|
|
1061
|
+
}
|
|
983
1062
|
.sm\:gap-4 {
|
|
984
1063
|
gap: calc(var(--spacing) * 4);
|
|
985
1064
|
}
|
|
@@ -1054,12 +1133,17 @@ Building a better future, one line of code at a time.
|
|
|
1054
1133
|
}
|
|
1055
1134
|
}
|
|
1056
1135
|
@media (width >= 64rem) {
|
|
1136
|
+
.lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(12rem\,0\.35fr\)_auto\] {
|
|
1137
|
+
grid-template-columns: minmax(0,1fr) minmax(12rem,0.35fr) auto;
|
|
1138
|
+
}
|
|
1139
|
+
.lg\:items-end {
|
|
1140
|
+
align-items: flex-end;
|
|
1141
|
+
}
|
|
1057
1142
|
.lg\:px-8 {
|
|
1058
1143
|
padding-inline: calc(var(--spacing) * 8);
|
|
1059
1144
|
}
|
|
1060
1145
|
}
|
|
1061
1146
|
}
|
|
1062
|
-
@charset "UTF-8";
|
|
1063
1147
|
:root {
|
|
1064
1148
|
--lesli-color-primary-50: #F2F7FF;
|
|
1065
1149
|
--lesli-color-primary-100: #E3EEFF;
|
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
<div class="lesli-timelines py-6">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
2
|
+
<% if activities.any? %>
|
|
3
|
+
<ul class="m-0 w-full list-none p-0">
|
|
4
|
+
<% activities.each do |activity| %>
|
|
5
|
+
<li class="group grid grid-cols-[1.5rem_minmax(0,1fr)] gap-x-4 md:grid-cols-[minmax(8rem,auto)_1.5rem_minmax(0,1fr)] md:gap-x-6">
|
|
6
|
+
<div class="col-start-2 mb-2 min-w-0 text-xs font-bold uppercase tracking-[0.2em] text-slate-500 md:col-start-1 md:row-start-1 md:mb-0 md:pt-1 md:text-right">
|
|
7
|
+
<time><%= activity["date"] %></time>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</
|
|
15
|
-
<span class="absolute top-7 bottom-0 left-1/2 w-px -translate-x-1/2 bg-slate-300 group-last:hidden" aria-hidden="true"></span>
|
|
16
|
-
</div>
|
|
9
|
+
<% if activity["actor"].present? %>
|
|
10
|
+
<span class="mt-1 block font-normal normal-case tracking-normal text-slate-400">
|
|
11
|
+
<%= activity["actor"] %>
|
|
12
|
+
</span>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
<div class="relative col-start-1 row-span-2 row-start-1 flex justify-center md:col-start-2">
|
|
17
|
+
<span
|
|
18
|
+
class="material-symbols-outlined relative z-10 bg-white text-xl leading-none text-[var(--lesli-color-primary)]"
|
|
19
|
+
aria-hidden="true">
|
|
20
|
+
<%= icon_for(activity) %>
|
|
21
|
+
</span>
|
|
22
|
+
<span class="absolute top-7 bottom-0 left-1/2 w-px -translate-x-1/2 bg-slate-300 group-last:hidden" aria-hidden="true"></span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="col-start-2 row-start-2 min-w-0 pb-10 md:col-start-3 md:row-start-1">
|
|
26
|
+
<h6 class="text-base font-semibold text-slate-800">
|
|
27
|
+
<%= activity["operation"] %>
|
|
28
|
+
</h6>
|
|
29
|
+
|
|
30
|
+
<% if activity["description"].present? %>
|
|
31
|
+
<p class="mt-1 text-sm leading-6 text-slate-600">
|
|
32
|
+
<%= activity["description"] %>
|
|
33
|
+
</p>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
</li>
|
|
37
|
+
<% end %>
|
|
38
|
+
</ul>
|
|
39
|
+
<% else %>
|
|
40
|
+
<%= render LesliView::Elements::Empty.new(text: empty_text) %>
|
|
41
|
+
<% end %>
|
|
29
42
|
</div>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "lesli_view/elements/empty"
|
|
4
|
+
|
|
3
5
|
=begin
|
|
4
6
|
|
|
5
7
|
Lesli
|
|
@@ -35,12 +37,23 @@ Building a better future, one line of code at a time.
|
|
|
35
37
|
module LesliView
|
|
36
38
|
module Components
|
|
37
39
|
class Timeline < ViewComponent::Base
|
|
38
|
-
|
|
40
|
+
DEFAULT_ICON = "radio_button_checked"
|
|
41
|
+
|
|
42
|
+
attr_reader :activities, :empty_text, :icons
|
|
43
|
+
|
|
44
|
+
def initialize(
|
|
45
|
+
activities: [],
|
|
46
|
+
icons: {},
|
|
47
|
+
empty_text: "No activities have been recorded yet."
|
|
48
|
+
)
|
|
49
|
+
@activities = Array(activities)
|
|
50
|
+
@icons = icons.transform_keys(&:to_sym)
|
|
51
|
+
@empty_text = empty_text
|
|
52
|
+
end
|
|
39
53
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
@activities = activities
|
|
54
|
+
def icon_for(activity)
|
|
55
|
+
icons.fetch(activity["icon"]&.to_sym, DEFAULT_ICON)
|
|
43
56
|
end
|
|
44
|
-
end
|
|
57
|
+
end
|
|
45
58
|
end
|
|
46
59
|
end
|
|
@@ -25,6 +25,10 @@ module LesliView
|
|
|
25
25
|
super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def field_control_textarea_rich(attribute, label: nil, message: nil, category: nil, icon: nil, **options)
|
|
29
|
+
super(attribute, label: label, message: message, category: category, icon: icon, horizontal: true, **options)
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
def field_textarea(attribute, **options)
|
|
29
33
|
field_control_textarea(attribute, **options)
|
|
30
34
|
end
|
|
@@ -81,6 +81,20 @@ module LesliView
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def field_control_textarea(attribute, label: nil, message: nil, category: nil, icon: nil, horizontal: false, **options)
|
|
84
|
+
label_html = label == false ? nil : label(attribute, label)
|
|
85
|
+
control_html = text_area(attribute, options)
|
|
86
|
+
|
|
87
|
+
field_control_builder(
|
|
88
|
+
label_html: label_html,
|
|
89
|
+
control_html: control_html,
|
|
90
|
+
message_text: message,
|
|
91
|
+
category: category,
|
|
92
|
+
horizontal: horizontal,
|
|
93
|
+
icon: icon
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def field_control_textarea_rich(attribute, label: nil, message: nil, category: nil, icon: nil, horizontal: false, **options)
|
|
84
98
|
label_html = label == false ? nil : label(attribute, label)
|
|
85
99
|
control_html = rich_textarea(attribute, options)
|
|
86
100
|
|
|
@@ -32,5 +32,9 @@ Building a better future, one line of code at a time.
|
|
|
32
32
|
<article
|
|
33
33
|
class="lesli-items-activities bg-white p-4 sm:p-8"
|
|
34
34
|
id="lesli-items-activities">
|
|
35
|
-
<%= render
|
|
35
|
+
<%= render LesliView::Components::Timeline.new(
|
|
36
|
+
activities: activities,
|
|
37
|
+
empty_text: empty_text,
|
|
38
|
+
icons: icons
|
|
39
|
+
) %>
|
|
36
40
|
</article>
|
|
@@ -32,17 +32,117 @@ Building a better future, one line of code at a time.
|
|
|
32
32
|
module LesliView
|
|
33
33
|
module Items
|
|
34
34
|
class Activities < ViewComponent::Base
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
DEFAULT_LIMIT = 25
|
|
36
|
+
I18N_SCOPE = "lesli.items.activities"
|
|
37
|
+
DATE_FORMAT = "%Y-%m-%d · %H:%M"
|
|
38
|
+
|
|
39
|
+
attr_reader :activities, :empty_text, :icons, :i18n_scope
|
|
40
|
+
|
|
41
|
+
def initialize(
|
|
42
|
+
resource,
|
|
43
|
+
icons: {},
|
|
44
|
+
i18n_scope: I18N_SCOPE,
|
|
45
|
+
limit: DEFAULT_LIMIT,
|
|
46
|
+
empty_text: nil
|
|
47
|
+
)
|
|
48
|
+
@icons = icons
|
|
49
|
+
@i18n_scope = i18n_scope
|
|
50
|
+
@activities = activity_scope(resource, limit).map do |activity|
|
|
51
|
+
activity_to_timeline(activity)
|
|
52
|
+
end
|
|
53
|
+
@empty_text = empty_text || I18n.t(
|
|
54
|
+
"#{i18n_scope}.empty",
|
|
55
|
+
default: "No activities have been recorded yet."
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def activity_to_timeline(activity)
|
|
62
|
+
metadata = activity.metadata.is_a?(Hash) ? activity.metadata : {}
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
"actor" => actor_name(activity.actor),
|
|
66
|
+
"date" => I18n.l(activity.created_at, format: DATE_FORMAT),
|
|
67
|
+
"description" => activity_description(activity, metadata),
|
|
68
|
+
"icon" => activity.activity_code.to_s,
|
|
69
|
+
"operation" => activity_operation(activity)
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def activity_description(activity, metadata)
|
|
74
|
+
I18n.t(
|
|
75
|
+
"#{activity_i18n_key(activity)}.#{description_key(metadata)}",
|
|
76
|
+
from: snapshot_name(metadata_value(metadata, :from)),
|
|
77
|
+
to: snapshot_name(metadata_value(metadata, :to)),
|
|
78
|
+
default: activity.description.to_s
|
|
79
|
+
).presence
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def activity_i18n_key(activity)
|
|
83
|
+
"#{i18n_scope}.events.#{activity.activity_code}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def activity_operation(activity)
|
|
87
|
+
I18n.t(
|
|
88
|
+
"#{activity_i18n_key(activity)}.operation",
|
|
89
|
+
default: activity.activity_code.to_s.humanize
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def activity_scope(resource, limit)
|
|
94
|
+
resource.activities
|
|
95
|
+
.includes(:actor)
|
|
96
|
+
.reorder(created_at: :desc, id: :desc)
|
|
97
|
+
.limit(normalize_limit(limit))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def actor_name(actor)
|
|
101
|
+
full_name = actor.full_name if actor.respond_to?(:full_name)
|
|
102
|
+
name = actor.name if actor.respond_to?(:name)
|
|
103
|
+
|
|
104
|
+
full_name.presence || name.presence || I18n.t(
|
|
105
|
+
"#{i18n_scope}.unknown_actor",
|
|
106
|
+
default: "Unknown user"
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def description_key(metadata)
|
|
111
|
+
has_change = metadata.key?(:from) || metadata.key?("from") ||
|
|
112
|
+
metadata.key?(:to) || metadata.key?("to")
|
|
113
|
+
|
|
114
|
+
return "description" unless has_change
|
|
115
|
+
return "set" if metadata_value(metadata, :from).nil?
|
|
116
|
+
return "removed" if metadata_value(metadata, :to).nil?
|
|
117
|
+
|
|
118
|
+
"changed"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def metadata_value(metadata, key)
|
|
122
|
+
return metadata[key] if metadata.key?(key)
|
|
123
|
+
|
|
124
|
+
metadata[key.to_s]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize_limit(limit)
|
|
128
|
+
normalized_limit = Integer(limit)
|
|
129
|
+
return normalized_limit if normalized_limit.positive?
|
|
130
|
+
|
|
131
|
+
raise ArgumentError, "limit must be a positive integer"
|
|
132
|
+
rescue TypeError, ArgumentError
|
|
133
|
+
raise ArgumentError, "limit must be a positive integer"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def snapshot_name(snapshot)
|
|
137
|
+
return "" if snapshot.nil?
|
|
138
|
+
return snapshot.to_s unless snapshot.is_a?(Hash)
|
|
139
|
+
|
|
140
|
+
[:name, :subject, :uid, :id].each do |key|
|
|
141
|
+
value = metadata_value(snapshot, key)
|
|
142
|
+
return value.to_s if value.present?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
""
|
|
46
146
|
end
|
|
47
147
|
end
|
|
48
148
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<li
|
|
2
|
+
id="<%= dom_id(resource) %>"
|
|
3
|
+
class="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
4
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
5
|
+
<span
|
|
6
|
+
class="material-symbols-outlined shrink-0 text-3xl text-[var(--lesli-color-primary)]"
|
|
7
|
+
aria-hidden="true">
|
|
8
|
+
attach_file
|
|
9
|
+
</span>
|
|
10
|
+
|
|
11
|
+
<div class="min-w-0">
|
|
12
|
+
<p class="truncate text-sm font-medium text-slate-900" title="<%= filename %>">
|
|
13
|
+
<%= display_name %>
|
|
14
|
+
</p>
|
|
15
|
+
<p class="mt-1 text-xs text-slate-500">
|
|
16
|
+
<%= details %>
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
22
|
+
<%= link_to(
|
|
23
|
+
I18n.t("lesli.items.attachments.download", default: "Download"),
|
|
24
|
+
path,
|
|
25
|
+
class: "inline-flex items-center gap-2 rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:border-slate-400 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--lesli-color-primary)]",
|
|
26
|
+
data: { turbo: false }
|
|
27
|
+
) %>
|
|
28
|
+
|
|
29
|
+
<%= button_to(
|
|
30
|
+
I18n.t("lesli.items.attachments.remove", default: "Remove"),
|
|
31
|
+
path,
|
|
32
|
+
method: :delete,
|
|
33
|
+
class: "inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/40",
|
|
34
|
+
form: {
|
|
35
|
+
class: "inline-flex",
|
|
36
|
+
data: {
|
|
37
|
+
turbo_confirm: I18n.t(
|
|
38
|
+
"lesli.items.attachments.confirm_remove",
|
|
39
|
+
default: "Remove this file?"
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
) %>
|
|
44
|
+
</div>
|
|
45
|
+
</li>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
Lesli
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
6
|
+
|
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program. If not, see http://www.gnu.org/licenses/.
|
|
19
|
+
|
|
20
|
+
Lesli · Ruby on Rails SaaS Development Framework.
|
|
21
|
+
|
|
22
|
+
Made with ♥ by LesliTech
|
|
23
|
+
Building a better future, one line of code at a time.
|
|
24
|
+
|
|
25
|
+
@contact hello@lesli.tech
|
|
26
|
+
@website https://www.lesli.tech
|
|
27
|
+
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
|
28
|
+
|
|
29
|
+
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
|
+
// ·
|
|
31
|
+
=end
|
|
32
|
+
|
|
33
|
+
module LesliView
|
|
34
|
+
module Items
|
|
35
|
+
class Attachment < ViewComponent::Base
|
|
36
|
+
attr_reader :resource
|
|
37
|
+
|
|
38
|
+
def initialize(attachment, path: nil)
|
|
39
|
+
@resource = attachment
|
|
40
|
+
@path = path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def path
|
|
44
|
+
@path || helpers.polymorphic_path(resource)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def filename
|
|
48
|
+
resource.file.filename.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def display_name
|
|
52
|
+
resource.respond_to?(:display_name) ? resource.display_name : filename
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def details
|
|
56
|
+
[
|
|
57
|
+
resource.file.blob.content_type,
|
|
58
|
+
helpers.number_to_human_size(resource.file.blob.byte_size)
|
|
59
|
+
].compact.join(" · ")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -29,40 +29,75 @@ Building a better future, one line of code at a time.
|
|
|
29
29
|
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
30
|
// ·
|
|
31
31
|
%>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<% ['file-word-2-line','file-excel-2-line','file-pdf-2-line'].each do |file| %>
|
|
61
|
-
<div class="file-list">
|
|
62
|
-
<span class="ri-<%= file %>"></span>
|
|
63
|
-
<p><%= file %></p>
|
|
32
|
+
<article
|
|
33
|
+
class="lesli-items-attachments rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
|
|
34
|
+
id="<%= dom_id(resource, :attachments) %>">
|
|
35
|
+
<%= form_with(
|
|
36
|
+
scope: scope_key,
|
|
37
|
+
model: attachment,
|
|
38
|
+
builder: LesliView::Forms::Builder,
|
|
39
|
+
url: create_path,
|
|
40
|
+
multipart: true
|
|
41
|
+
) do |form| %>
|
|
42
|
+
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(12rem,0.35fr)_auto] lg:items-end">
|
|
43
|
+
<div>
|
|
44
|
+
<%= form.label(
|
|
45
|
+
:file,
|
|
46
|
+
I18n.t("lesli.items.attachments.choose_file", default: "Choose a file"),
|
|
47
|
+
class: "mb-2 block text-sm font-medium text-slate-700"
|
|
48
|
+
) %>
|
|
49
|
+
<%= form.file_field(
|
|
50
|
+
:file,
|
|
51
|
+
required: true,
|
|
52
|
+
accept: accepted_content_types,
|
|
53
|
+
class: "block w-full cursor-pointer rounded-lg border border-slate-300 bg-white text-sm text-slate-700 file:mr-4 file:border-0 file:bg-slate-100 file:px-4 file:py-2.5 file:text-sm file:font-medium file:text-slate-700 hover:file:bg-slate-200 focus:outline-none focus:ring-2 focus:ring-[var(--lesli-color-primary)]"
|
|
54
|
+
) %>
|
|
55
|
+
<% if attachment.errors[:file].any? %>
|
|
56
|
+
<p class="mt-2 text-sm text-red-600">
|
|
57
|
+
<%= attachment.errors[:file].to_sentence %>
|
|
58
|
+
</p>
|
|
59
|
+
<% end %>
|
|
64
60
|
</div>
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
|
|
62
|
+
<%= form.field_control_text(
|
|
63
|
+
:label,
|
|
64
|
+
label: I18n.t("lesli.items.attachments.label", default: "Label"),
|
|
65
|
+
maxlength: 255
|
|
66
|
+
) %>
|
|
67
|
+
|
|
68
|
+
<%= form.field_control_button(
|
|
69
|
+
I18n.t("lesli.items.attachments.add", default: "Add file"),
|
|
70
|
+
{
|
|
71
|
+
data: {
|
|
72
|
+
turbo_submits_with: I18n.t(
|
|
73
|
+
"lesli.items.attachments.uploading",
|
|
74
|
+
default: "Uploading..."
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
icon: "upload_file"
|
|
79
|
+
) %>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
82
|
+
|
|
83
|
+
<hr class="my-6 border-0 border-t border-slate-200">
|
|
84
|
+
|
|
85
|
+
<% if attachments.any? %>
|
|
86
|
+
<ul class="m-0 list-none space-y-3 p-0">
|
|
87
|
+
<% attachments.each do |item| %>
|
|
88
|
+
<%= render LesliView::Items::Attachment.new(
|
|
89
|
+
item,
|
|
90
|
+
path: attachment_path(item)
|
|
91
|
+
) %>
|
|
92
|
+
<% end %>
|
|
93
|
+
</ul>
|
|
94
|
+
<% else %>
|
|
95
|
+
<p class="py-6 text-center text-sm text-slate-500">
|
|
96
|
+
<%= I18n.t(
|
|
97
|
+
"lesli.items.attachments.empty",
|
|
98
|
+
default: "No files have been attached yet."
|
|
99
|
+
) %>
|
|
100
|
+
</p>
|
|
101
|
+
<% end %>
|
|
67
102
|
</article>
|
|
68
103
|
|
|
@@ -33,12 +33,48 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
module LesliView
|
|
34
34
|
module Items
|
|
35
35
|
class Attachments < ViewComponent::Base
|
|
36
|
-
attr_reader :resource, :
|
|
36
|
+
attr_reader :resource, :attachment
|
|
37
37
|
|
|
38
|
-
def initialize(
|
|
38
|
+
def initialize(
|
|
39
|
+
resource,
|
|
40
|
+
attachment: nil,
|
|
41
|
+
path_to_create: nil,
|
|
42
|
+
path_to_update: nil
|
|
43
|
+
)
|
|
39
44
|
@resource = resource
|
|
40
45
|
@path_to_create = path_to_create
|
|
41
46
|
@path_to_update = path_to_update
|
|
47
|
+
@attachment = attachment || attachment_class.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def attachment_class
|
|
51
|
+
resource.class.lesli_attachments_class
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def attachments
|
|
55
|
+
resource.attachments.with_attached_file
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_path
|
|
59
|
+
@path_to_create || helpers.polymorphic_path(
|
|
60
|
+
[:items, :attachments],
|
|
61
|
+
attachable_type: resource.class.name,
|
|
62
|
+
attachable_id: resource.id
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def attachment_path(item)
|
|
67
|
+
return @path_to_update.call(item) if @path_to_update.respond_to?(:call)
|
|
68
|
+
|
|
69
|
+
helpers.polymorphic_path(item)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def scope_key
|
|
73
|
+
ActiveSupport::Inflector.underscore(attachment_class.name).tr("/", "_")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def accepted_content_types
|
|
77
|
+
attachment_class.lesli_attachment_content_types.join(",")
|
|
42
78
|
end
|
|
43
79
|
end
|
|
44
80
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<li class="lesli-items-discussions-comments-comment flex flex-col gap-3 border-b border-slate-200 pb-6 last:border-b-0 last:pb-0 sm:flex-row sm:gap-4">
|
|
2
2
|
<div class="shrink-0 text-sm leading-5 text-slate-500 sm:w-40 sm:text-right">
|
|
3
|
-
<strong class="block font-semibold text-slate-800">
|
|
4
|
-
|
|
3
|
+
<strong class="block font-semibold text-slate-800">
|
|
4
|
+
<%= author_name %>
|
|
5
|
+
</strong>
|
|
6
|
+
<time datetime="<%= created_at_iso8601 %>">
|
|
7
|
+
<%= created_at_label %>
|
|
8
|
+
</time>
|
|
5
9
|
</div>
|
|
6
10
|
|
|
7
|
-
<div class="
|
|
8
|
-
<%== discussion.message %>
|
|
9
|
-
</div>
|
|
11
|
+
<div class="min-w-0 flex-1 whitespace-pre-wrap break-words text-slate-700"><%= discussion.message %></div>
|
|
10
12
|
</li>
|
|
@@ -37,6 +37,32 @@ module LesliView
|
|
|
37
37
|
def initialize(discussion)
|
|
38
38
|
@discussion = discussion
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
def author_name
|
|
42
|
+
author = discussion.author
|
|
43
|
+
full_name = author.full_name if author.respond_to?(:full_name)
|
|
44
|
+
name = author.name if author.respond_to?(:name)
|
|
45
|
+
|
|
46
|
+
full_name.presence || name.presence || I18n.t(
|
|
47
|
+
"lesli.items.discussions.unknown_author",
|
|
48
|
+
default: "Unknown user"
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def created_at_label
|
|
53
|
+
return if discussion.created_at.blank?
|
|
54
|
+
|
|
55
|
+
I18n.l(
|
|
56
|
+
discussion.created_at,
|
|
57
|
+
format: "%Y-%m-%d · %H:%M"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def created_at_iso8601
|
|
62
|
+
return if discussion.created_at.blank?
|
|
63
|
+
|
|
64
|
+
discussion.created_at.strftime("%Y-%m-%dT%H:%M:%S%:z")
|
|
65
|
+
end
|
|
40
66
|
end
|
|
41
67
|
end
|
|
42
68
|
end
|
|
@@ -29,31 +29,46 @@ Building a better future, one line of code at a time.
|
|
|
29
29
|
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
30
30
|
// ·
|
|
31
31
|
%>
|
|
32
|
-
<article
|
|
32
|
+
<article
|
|
33
|
+
class="lesli-items-discussions bg-white p-4 sm:p-8"
|
|
34
|
+
id="<%= dom_id(resource, :discussions) %>">
|
|
33
35
|
<div class="lesli-items-discussions-form p-4">
|
|
34
|
-
<% discussion_class = "#{@resource.class.name.deconstantize}::Items::Discussion".constantize %>
|
|
35
|
-
<% scope_key = ActiveSupport::Inflector.underscore(discussion_class.name).tr("/", "_") %>
|
|
36
36
|
<%= form_with(
|
|
37
37
|
scope: scope_key,
|
|
38
|
-
model:
|
|
38
|
+
model: comment,
|
|
39
39
|
builder: LesliView::Forms::Builder,
|
|
40
|
-
url:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
url: create_path
|
|
41
|
+
) do |form| %>
|
|
42
|
+
<%= form.field_control_textarea(:message, rows: 4) %>
|
|
43
|
+
<% if comment.errors[:message].any? %>
|
|
44
|
+
<p class="mt-2 text-sm text-red-600">
|
|
45
|
+
<%= comment.errors.full_messages_for(:message).to_sentence %>
|
|
46
|
+
</p>
|
|
47
|
+
<% end %>
|
|
48
|
+
<%= form.field_control_submit I18n.t(
|
|
49
|
+
"lesli.items.discussions.add",
|
|
50
|
+
default: "Add comment"
|
|
51
|
+
) %>
|
|
46
52
|
<% end %>
|
|
47
53
|
</div>
|
|
48
54
|
<hr class="my-6 border-0 border-t border-slate-200">
|
|
49
55
|
|
|
50
56
|
<div class="lesli-items-discussions-container p-4">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
<% if discussions.any? %>
|
|
58
|
+
<ul
|
|
59
|
+
id="<%= "#{scope_key}-lesli-items-discussions-list" %>"
|
|
60
|
+
class="m-0 list-none space-y-6 p-0">
|
|
61
|
+
<% discussions.each do |item| %>
|
|
62
|
+
<%= render(LesliView::Items::Discussion.new(item)) %>
|
|
63
|
+
<% end %>
|
|
64
|
+
</ul>
|
|
65
|
+
<% else %>
|
|
66
|
+
<p class="py-6 text-center text-sm text-slate-500">
|
|
67
|
+
<%= I18n.t(
|
|
68
|
+
"lesli.items.discussions.empty",
|
|
69
|
+
default: "No comments have been added yet."
|
|
70
|
+
) %>
|
|
71
|
+
</p>
|
|
72
|
+
<% end %>
|
|
58
73
|
</div>
|
|
59
74
|
</article>
|
|
@@ -32,13 +32,35 @@ Building a better future, one line of code at a time.
|
|
|
32
32
|
module LesliView
|
|
33
33
|
module Items
|
|
34
34
|
class Discussions < ViewComponent::Base
|
|
35
|
-
attr_reader :
|
|
36
|
-
|
|
37
|
-
delegate :lexxy_rich_textarea_tag, to: :helpers
|
|
35
|
+
attr_reader :comment, :resource
|
|
38
36
|
|
|
39
|
-
def initialize(resource,
|
|
37
|
+
def initialize(resource, comment: nil)
|
|
40
38
|
@resource = resource
|
|
41
|
-
@
|
|
39
|
+
@comment = comment || discussion_class.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def discussion_class
|
|
43
|
+
resource.class.lesli_discussions_class
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def discussions
|
|
47
|
+
resource.public_send(
|
|
48
|
+
resource.class.lesli_discussions_association_name
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_path
|
|
53
|
+
helpers.polymorphic_path(
|
|
54
|
+
[:items, :discussions],
|
|
55
|
+
discussable_type: resource.class.name,
|
|
56
|
+
discussable_id: resource.id
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def scope_key
|
|
61
|
+
ActiveSupport::Inflector
|
|
62
|
+
.underscore(discussion_class.name)
|
|
63
|
+
.tr("/", "_")
|
|
42
64
|
end
|
|
43
65
|
end
|
|
44
66
|
end
|
|
@@ -32,7 +32,7 @@ Building a better future, one line of code at a time.
|
|
|
32
32
|
|
|
33
33
|
<li
|
|
34
34
|
id="<%= dom_id(@resource, @scope_key) %>"
|
|
35
|
-
class="lesli-items-tasks-item rounded-xl border transition-colors <%= @resource.done ? "lesli-items-tasks-task-done
|
|
35
|
+
class="lesli-items-tasks-item rounded-xl border transition-colors <%= @resource.done ? "lesli-items-tasks-task-done border-slate-200 bg-slate-50 hover:border-slate-300 hover:bg-slate-100" : "border-slate-200 hover:border-slate-300 hover:bg-slate-50" %>">
|
|
36
36
|
<%= form_with(
|
|
37
37
|
url: helpers.polymorphic_path(@resource),
|
|
38
38
|
method: :patch,
|
|
@@ -42,8 +42,11 @@ Building a better future, one line of code at a time.
|
|
|
42
42
|
<%= form.hidden_field :id, value: @resource.id %>
|
|
43
43
|
<%= form.button(
|
|
44
44
|
type: "submit",
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
aria: {
|
|
46
|
+
label: toggle_label,
|
|
47
|
+
pressed: @resource.done?
|
|
48
|
+
},
|
|
49
|
+
class: "flex w-full cursor-pointer items-center rounded-xl px-4 py-3 text-left text-sm font-medium text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--lesli-color-primary)]"
|
|
47
50
|
) do %>
|
|
48
51
|
<span class="flex min-w-0 items-center gap-3">
|
|
49
52
|
<span
|
|
@@ -51,9 +54,15 @@ Building a better future, one line of code at a time.
|
|
|
51
54
|
aria-hidden="true">
|
|
52
55
|
<%= @resource.done ? "check_box" : "check_box_outline_blank" %>
|
|
53
56
|
</span>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
<% if @resource.done? %>
|
|
58
|
+
<del class="min-w-0 break-words text-slate-500 line-through">
|
|
59
|
+
<%= @resource.title %>
|
|
60
|
+
</del>
|
|
61
|
+
<% else %>
|
|
62
|
+
<span class="min-w-0 break-words">
|
|
63
|
+
<%= @resource.title %>
|
|
64
|
+
</span>
|
|
65
|
+
<% end %>
|
|
57
66
|
</span>
|
|
58
67
|
<% end %>
|
|
59
68
|
<% end %>
|
|
@@ -39,6 +39,20 @@ module LesliView
|
|
|
39
39
|
@resource = task
|
|
40
40
|
@scope_key = scope
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
def toggle_label
|
|
44
|
+
if resource.done?
|
|
45
|
+
I18n.t(
|
|
46
|
+
"lesli.items.tasks.reopen_action",
|
|
47
|
+
default: "Mark task as incomplete"
|
|
48
|
+
)
|
|
49
|
+
else
|
|
50
|
+
I18n.t(
|
|
51
|
+
"lesli.items.tasks.complete_action",
|
|
52
|
+
default: "Mark task as complete"
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
42
56
|
end
|
|
43
57
|
end
|
|
44
58
|
end
|
|
@@ -33,29 +33,42 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
class="lesli-items-tasks rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
|
|
34
34
|
id="<%= dom_id(@resource, :tasks) %>">
|
|
35
35
|
<div class="lesli-items-tasks-form">
|
|
36
|
-
<% task_class = "#{@resource.class.name.deconstantize}::Items::Task".constantize %>
|
|
37
|
-
<% scope_key = ActiveSupport::Inflector.underscore(task_class.name).tr("/", "_") %>
|
|
38
36
|
<%= form_with(
|
|
39
37
|
scope: scope_key,
|
|
40
|
-
model:
|
|
38
|
+
model: task,
|
|
41
39
|
builder: LesliView::Forms::Builder,
|
|
42
|
-
url:
|
|
43
|
-
|
|
44
|
-
taskable_id: @resource.id
|
|
45
|
-
)) do |form| %>
|
|
40
|
+
url: create_path
|
|
41
|
+
) do |form| %>
|
|
46
42
|
<%= form.field_control_text :title %>
|
|
47
|
-
|
|
43
|
+
<% if task.errors[:title].any? %>
|
|
44
|
+
<p class="mt-2 text-sm text-red-600">
|
|
45
|
+
<%= task.errors.full_messages_for(:title).to_sentence %>
|
|
46
|
+
</p>
|
|
47
|
+
<% end %>
|
|
48
|
+
<%= form.field_control_submit I18n.t(
|
|
49
|
+
"lesli.items.tasks.add",
|
|
50
|
+
default: "Add task"
|
|
51
|
+
) %>
|
|
48
52
|
<% end %>
|
|
49
53
|
</div>
|
|
50
54
|
<hr class="my-6 border-0 border-t border-slate-200">
|
|
51
55
|
|
|
52
56
|
<div class="lesli-items-tasks-container">
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
<% if tasks.any? %>
|
|
58
|
+
<ul
|
|
59
|
+
id="<%= "#{scope_key}-lesli-items-tasks-list" %>"
|
|
60
|
+
class="m-0 list-none space-y-3 p-0">
|
|
61
|
+
<% tasks.each do |item| %>
|
|
62
|
+
<%= render(LesliView::Items::Task.new(item, scope_key)) %>
|
|
63
|
+
<% end %>
|
|
64
|
+
</ul>
|
|
65
|
+
<% else %>
|
|
66
|
+
<p class="py-6 text-center text-sm text-slate-500">
|
|
67
|
+
<%= I18n.t(
|
|
68
|
+
"lesli.items.tasks.empty",
|
|
69
|
+
default: "No tasks have been added yet."
|
|
70
|
+
) %>
|
|
71
|
+
</p>
|
|
72
|
+
<% end %>
|
|
60
73
|
</div>
|
|
61
74
|
</article>
|
|
@@ -33,10 +33,35 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
module LesliView
|
|
34
34
|
module Items
|
|
35
35
|
class Tasks < ViewComponent::Base
|
|
36
|
-
attr_reader :resource, :
|
|
36
|
+
attr_reader :resource, :task
|
|
37
37
|
|
|
38
|
-
def initialize(resource)
|
|
38
|
+
def initialize(resource, task: nil)
|
|
39
39
|
@resource = resource
|
|
40
|
+
@task = task || task_class.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def task_class
|
|
44
|
+
resource.class.lesli_tasks_class
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tasks
|
|
48
|
+
resource.public_send(
|
|
49
|
+
resource.class.lesli_tasks_association_name
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_path
|
|
54
|
+
helpers.polymorphic_path(
|
|
55
|
+
[:items, :tasks],
|
|
56
|
+
taskable_type: resource.class.name,
|
|
57
|
+
taskable_id: resource.id
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def scope_key
|
|
62
|
+
ActiveSupport::Inflector
|
|
63
|
+
.underscore(task_class.name)
|
|
64
|
+
.tr("/", "_")
|
|
40
65
|
end
|
|
41
66
|
end
|
|
42
67
|
end
|
data/lib/lesli_view/version.rb
CHANGED
data/lib/lesli_view.rb
CHANGED
data/readme.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
<br />
|
|
9
9
|
|
|
10
10
|
<div align="center">
|
|
11
|
-
<a target="_blank" href="https://github.com/LesliTech/LesliView/actions/workflows/
|
|
11
|
+
<a target="_blank" href="https://github.com/LesliTech/LesliView/actions/workflows/lesli-ci-gem.yaml">
|
|
12
12
|
<img
|
|
13
13
|
alt="LesliView test status"
|
|
14
|
-
src="https://img.shields.io/github/actions/workflow/status/LesliTech/LesliView/
|
|
14
|
+
src="https://img.shields.io/github/actions/workflow/status/LesliTech/LesliView/lesli-ci-gem.yaml?branch=main&style=for-the-badge&logo=github&label=tests">
|
|
15
15
|
</a>
|
|
16
16
|
<a target="_blank" href="https://rubygems.org/gems/lesli_view">
|
|
17
17
|
<img alt="Gem Version" src="https://img.shields.io/gem/v/lesli_view?style=for-the-badge&logo=ruby">
|
|
@@ -53,7 +53,7 @@ LesliView provides:
|
|
|
53
53
|
|
|
54
54
|
The current development baseline is:
|
|
55
55
|
|
|
56
|
-
- Ruby 3.
|
|
56
|
+
- Ruby 3.3 or newer (the release workflow uses Ruby 3.3.5)
|
|
57
57
|
- Rails 8.1
|
|
58
58
|
- LesliAssets, configured in the host application's asset build
|
|
59
59
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lesli_view
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Lesli Development Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: view_component
|
|
@@ -48,7 +48,7 @@ extensions: []
|
|
|
48
48
|
extra_rdoc_files: []
|
|
49
49
|
files:
|
|
50
50
|
- Rakefile
|
|
51
|
-
- app/assets/stylesheets/lesli_view/view.
|
|
51
|
+
- app/assets/stylesheets/lesli_view/view.css
|
|
52
52
|
- lib/lesli_view.rb
|
|
53
53
|
- lib/lesli_view/charts/bar.rb
|
|
54
54
|
- lib/lesli_view/charts/general.html.erb
|
|
@@ -89,6 +89,8 @@ files:
|
|
|
89
89
|
- lib/lesli_view/items/actions.rb
|
|
90
90
|
- lib/lesli_view/items/activities.html.erb
|
|
91
91
|
- lib/lesli_view/items/activities.rb
|
|
92
|
+
- lib/lesli_view/items/attachment.html.erb
|
|
93
|
+
- lib/lesli_view/items/attachment.rb
|
|
92
94
|
- lib/lesli_view/items/attachments.html.erb
|
|
93
95
|
- lib/lesli_view/items/attachments.rb
|
|
94
96
|
- lib/lesli_view/items/discussion.html.erb
|
|
@@ -118,7 +120,7 @@ files:
|
|
|
118
120
|
- readme.md
|
|
119
121
|
homepage: https://www.lesli.dev/
|
|
120
122
|
licenses:
|
|
121
|
-
- GPL-3.0
|
|
123
|
+
- GPL-3.0-or-later
|
|
122
124
|
metadata:
|
|
123
125
|
homepage_uri: https://www.lesli.dev/
|
|
124
126
|
changelog_uri: https://github.com/LesliTech/LesliView/releases
|
|
@@ -133,7 +135,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
133
135
|
requirements:
|
|
134
136
|
- - ">="
|
|
135
137
|
- !ruby/object:Gem::Version
|
|
136
|
-
version: '
|
|
138
|
+
version: '3.3'
|
|
139
|
+
- - "<"
|
|
140
|
+
- !ruby/object:Gem::Version
|
|
141
|
+
version: '4.0'
|
|
137
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
143
|
requirements:
|
|
139
144
|
- - ">="
|