@jskit-ai/users-web 0.1.81 → 0.1.82

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.
Files changed (36) hide show
  1. package/package.descriptor.mjs +51 -9
  2. package/package.json +21 -10
  3. package/src/client/bulkActions.js +47 -0
  4. package/src/client/components/AccountSettingsClientElement.vue +69 -24
  5. package/src/client/components/CrudAddEditScreen.vue +186 -0
  6. package/src/client/components/CrudListBulkActionSurface.vue +126 -0
  7. package/src/client/components/CrudListFilterSurface.vue +377 -0
  8. package/src/client/components/CrudListScreen.vue +434 -0
  9. package/src/client/components/CrudViewScreen.vue +186 -0
  10. package/src/client/components/ProfileClientElement.vue +19 -12
  11. package/src/client/composables/records/useAddEdit.js +23 -2
  12. package/src/client/composables/records/useCrudList.js +5 -1
  13. package/src/client/composables/records/useView.js +1 -0
  14. package/src/client/composables/runtime/operationUiHelpers.js +7 -3
  15. package/src/client/composables/runtime/useEndpointResource.js +12 -2
  16. package/src/client/composables/runtime/useUiFeedback.js +4 -2
  17. package/src/client/composables/support/resourceLoadStateHelpers.js +33 -1
  18. package/src/client/composables/useAccountSettingsRuntime.js +10 -1
  19. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  20. package/src/client/composables/useCrudListBulkActions.js +147 -0
  21. package/src/client/composables/useCrudListScreen.js +107 -0
  22. package/src/client/composables/useCrudViewScreen.js +67 -0
  23. package/src/client/composables/usePagedCollection.js +6 -1
  24. package/src/client/filters.js +15 -0
  25. package/src/client/index.js +5 -0
  26. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +34 -8
  27. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +34 -8
  28. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +34 -8
  29. package/test/crudListBulkActionSurface.test.js +27 -0
  30. package/test/crudListFilterSurface.test.js +45 -0
  31. package/test/crudScreenComponents.test.js +62 -0
  32. package/test/errorIntentContract.test.js +31 -0
  33. package/test/exportsContract.test.js +11 -0
  34. package/test/resourceLoadStateHelpers.test.js +35 -1
  35. package/test/settingsPlacementContract.test.js +61 -0
  36. package/test/useCrudListBulkActions.test.js +65 -0
@@ -0,0 +1,377 @@
1
+ <script setup>
2
+ import { computed, ref } from "vue";
3
+ import { useDisplay } from "vuetify";
4
+
5
+ const props = defineProps({
6
+ filters: {
7
+ type: Object,
8
+ default: () => ({})
9
+ },
10
+ runtime: {
11
+ type: Object,
12
+ default: null
13
+ },
14
+ title: {
15
+ type: String,
16
+ default: "Filters"
17
+ }
18
+ });
19
+
20
+ const display = useDisplay();
21
+ const sheetOpen = ref(false);
22
+
23
+ const filterEntries = computed(() =>
24
+ Object.values(props.filters && typeof props.filters === "object" && !Array.isArray(props.filters)
25
+ ? props.filters
26
+ : {})
27
+ .filter((filter) => filter?.key && filter?.type)
28
+ );
29
+ const runtimeValues = computed(() => props.runtime?.values || {});
30
+ const activeChips = computed(() => Array.isArray(props.runtime?.activeChips?.value) ? props.runtime.activeChips.value : []);
31
+ const hasActiveFilters = computed(() => Boolean(props.runtime?.hasActiveFilters?.value));
32
+ const shouldRender = computed(() => filterEntries.value.length > 0 && Boolean(props.runtime?.values));
33
+ const isCompactLayout = computed(() => {
34
+ const displayName = String(display?.name?.value || "").trim().toLowerCase();
35
+ return displayName === "xs" || displayName === "sm";
36
+ });
37
+
38
+ function optionItems(filter = {}) {
39
+ return Array.isArray(filter.options) ? filter.options : [];
40
+ }
41
+
42
+ function placeholder(filter = {}, fallback = "") {
43
+ return String(filter?.ui?.placeholder || fallback || "").trim();
44
+ }
45
+
46
+ function clearChip(chip = {}) {
47
+ props.runtime?.clearChip?.(chip);
48
+ }
49
+
50
+ function clearFilters() {
51
+ props.runtime?.clearFilters?.();
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <section v-if="shouldRender" class="crud-list-filter-surface">
57
+ <div class="crud-list-filter-surface__summary">
58
+ <v-btn
59
+ v-if="isCompactLayout"
60
+ color="primary"
61
+ variant="tonal"
62
+ class="crud-list-filter-surface__open"
63
+ @click="sheetOpen = true"
64
+ >
65
+ {{ title }}
66
+ <v-chip v-if="activeChips.length > 0" class="ml-2" size="x-small" color="primary" variant="flat">
67
+ {{ activeChips.length }}
68
+ </v-chip>
69
+ </v-btn>
70
+
71
+ <div v-if="hasActiveFilters" class="crud-list-filter-surface__chips">
72
+ <v-chip
73
+ v-for="chip in activeChips"
74
+ :key="chip.id"
75
+ closable
76
+ size="small"
77
+ variant="tonal"
78
+ @click:close="clearChip(chip)"
79
+ >
80
+ {{ chip.label }}
81
+ </v-chip>
82
+ <v-btn size="small" variant="text" @click="clearFilters">Clear all</v-btn>
83
+ </div>
84
+ </div>
85
+
86
+ <v-sheet v-if="!isCompactLayout" rounded="lg" border class="crud-list-filter-surface__panel">
87
+ <div class="crud-list-filter-surface__controls">
88
+ <template v-for="filter in filterEntries" :key="filter.key">
89
+ <v-switch
90
+ v-if="filter.type === 'flag'"
91
+ v-model="runtimeValues[filter.key]"
92
+ :label="filter.label"
93
+ color="primary"
94
+ density="comfortable"
95
+ hide-details
96
+ class="crud-list-filter-surface__control"
97
+ />
98
+ <v-select
99
+ v-else-if="filter.type === 'enum' || filter.type === 'enumMany' || filter.type === 'presence'"
100
+ v-model="runtimeValues[filter.key]"
101
+ :items="optionItems(filter)"
102
+ :label="filter.label"
103
+ :placeholder="placeholder(filter)"
104
+ :multiple="filter.type === 'enumMany'"
105
+ :chips="filter.type === 'enumMany'"
106
+ :closable-chips="filter.type === 'enumMany'"
107
+ clearable
108
+ item-title="label"
109
+ item-value="value"
110
+ variant="outlined"
111
+ density="comfortable"
112
+ hide-details="auto"
113
+ class="crud-list-filter-surface__control"
114
+ />
115
+ <v-combobox
116
+ v-else-if="filter.type === 'recordIdMany'"
117
+ v-model="runtimeValues[filter.key]"
118
+ :label="filter.label"
119
+ :placeholder="placeholder(filter, 'Enter ids')"
120
+ multiple
121
+ chips
122
+ closable-chips
123
+ clearable
124
+ variant="outlined"
125
+ density="comfortable"
126
+ hide-details="auto"
127
+ class="crud-list-filter-surface__control"
128
+ />
129
+ <v-text-field
130
+ v-else-if="filter.type === 'recordId'"
131
+ v-model="runtimeValues[filter.key]"
132
+ :label="filter.label"
133
+ :placeholder="placeholder(filter, 'Enter id')"
134
+ clearable
135
+ variant="outlined"
136
+ density="comfortable"
137
+ hide-details="auto"
138
+ class="crud-list-filter-surface__control"
139
+ />
140
+ <div v-else-if="filter.type === 'dateRange'" class="crud-list-filter-surface__range">
141
+ <v-text-field
142
+ v-model="runtimeValues[filter.key].from"
143
+ :label="`${filter.label} from`"
144
+ type="date"
145
+ variant="outlined"
146
+ density="comfortable"
147
+ hide-details="auto"
148
+ />
149
+ <v-text-field
150
+ v-model="runtimeValues[filter.key].to"
151
+ :label="`${filter.label} to`"
152
+ type="date"
153
+ variant="outlined"
154
+ density="comfortable"
155
+ hide-details="auto"
156
+ />
157
+ </div>
158
+ <div v-else-if="filter.type === 'numberRange'" class="crud-list-filter-surface__range">
159
+ <v-text-field
160
+ v-model="runtimeValues[filter.key].min"
161
+ :label="`${filter.label} min`"
162
+ type="number"
163
+ variant="outlined"
164
+ density="comfortable"
165
+ hide-details="auto"
166
+ />
167
+ <v-text-field
168
+ v-model="runtimeValues[filter.key].max"
169
+ :label="`${filter.label} max`"
170
+ type="number"
171
+ variant="outlined"
172
+ density="comfortable"
173
+ hide-details="auto"
174
+ />
175
+ </div>
176
+ <v-text-field
177
+ v-else-if="filter.type === 'date'"
178
+ v-model="runtimeValues[filter.key]"
179
+ :label="filter.label"
180
+ type="date"
181
+ clearable
182
+ variant="outlined"
183
+ density="comfortable"
184
+ hide-details="auto"
185
+ class="crud-list-filter-surface__control"
186
+ />
187
+ </template>
188
+ </div>
189
+ </v-sheet>
190
+
191
+ <v-dialog v-model="sheetOpen" max-width="640" location="bottom">
192
+ <v-card rounded="lg" class="crud-list-filter-surface__sheet">
193
+ <v-card-title class="d-flex align-center justify-space-between">
194
+ <span>{{ title }}</span>
195
+ <v-btn variant="text" @click="sheetOpen = false">Close</v-btn>
196
+ </v-card-title>
197
+ <v-card-text>
198
+ <div class="crud-list-filter-surface__controls crud-list-filter-surface__controls--stacked">
199
+ <template v-for="filter in filterEntries" :key="filter.key">
200
+ <v-switch
201
+ v-if="filter.type === 'flag'"
202
+ v-model="runtimeValues[filter.key]"
203
+ :label="filter.label"
204
+ color="primary"
205
+ density="comfortable"
206
+ hide-details
207
+ />
208
+ <v-select
209
+ v-else-if="filter.type === 'enum' || filter.type === 'enumMany' || filter.type === 'presence'"
210
+ v-model="runtimeValues[filter.key]"
211
+ :items="optionItems(filter)"
212
+ :label="filter.label"
213
+ :placeholder="placeholder(filter)"
214
+ :multiple="filter.type === 'enumMany'"
215
+ :chips="filter.type === 'enumMany'"
216
+ :closable-chips="filter.type === 'enumMany'"
217
+ clearable
218
+ item-title="label"
219
+ item-value="value"
220
+ variant="outlined"
221
+ density="comfortable"
222
+ hide-details="auto"
223
+ />
224
+ <v-combobox
225
+ v-else-if="filter.type === 'recordIdMany'"
226
+ v-model="runtimeValues[filter.key]"
227
+ :label="filter.label"
228
+ :placeholder="placeholder(filter, 'Enter ids')"
229
+ multiple
230
+ chips
231
+ closable-chips
232
+ clearable
233
+ variant="outlined"
234
+ density="comfortable"
235
+ hide-details="auto"
236
+ />
237
+ <v-text-field
238
+ v-else-if="filter.type === 'recordId'"
239
+ v-model="runtimeValues[filter.key]"
240
+ :label="filter.label"
241
+ :placeholder="placeholder(filter, 'Enter id')"
242
+ clearable
243
+ variant="outlined"
244
+ density="comfortable"
245
+ hide-details="auto"
246
+ />
247
+ <div v-else-if="filter.type === 'dateRange'" class="crud-list-filter-surface__range">
248
+ <v-text-field
249
+ v-model="runtimeValues[filter.key].from"
250
+ :label="`${filter.label} from`"
251
+ type="date"
252
+ variant="outlined"
253
+ density="comfortable"
254
+ hide-details="auto"
255
+ />
256
+ <v-text-field
257
+ v-model="runtimeValues[filter.key].to"
258
+ :label="`${filter.label} to`"
259
+ type="date"
260
+ variant="outlined"
261
+ density="comfortable"
262
+ hide-details="auto"
263
+ />
264
+ </div>
265
+ <div v-else-if="filter.type === 'numberRange'" class="crud-list-filter-surface__range">
266
+ <v-text-field
267
+ v-model="runtimeValues[filter.key].min"
268
+ :label="`${filter.label} min`"
269
+ type="number"
270
+ variant="outlined"
271
+ density="comfortable"
272
+ hide-details="auto"
273
+ />
274
+ <v-text-field
275
+ v-model="runtimeValues[filter.key].max"
276
+ :label="`${filter.label} max`"
277
+ type="number"
278
+ variant="outlined"
279
+ density="comfortable"
280
+ hide-details="auto"
281
+ />
282
+ </div>
283
+ <v-text-field
284
+ v-else-if="filter.type === 'date'"
285
+ v-model="runtimeValues[filter.key]"
286
+ :label="filter.label"
287
+ type="date"
288
+ clearable
289
+ variant="outlined"
290
+ density="comfortable"
291
+ hide-details="auto"
292
+ />
293
+ </template>
294
+ </div>
295
+ </v-card-text>
296
+ <v-card-actions class="justify-space-between">
297
+ <v-btn variant="text" @click="clearFilters">Clear all</v-btn>
298
+ <v-btn color="primary" variant="flat" @click="sheetOpen = false">Apply</v-btn>
299
+ </v-card-actions>
300
+ </v-card>
301
+ </v-dialog>
302
+ </section>
303
+ </template>
304
+
305
+ <style scoped>
306
+ .crud-list-filter-surface {
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: 0.75rem;
310
+ min-width: 0;
311
+ }
312
+
313
+ .crud-list-filter-surface__summary {
314
+ align-items: center;
315
+ display: flex;
316
+ flex-wrap: wrap;
317
+ gap: 0.75rem;
318
+ justify-content: space-between;
319
+ }
320
+
321
+ .crud-list-filter-surface__open {
322
+ min-height: 48px;
323
+ }
324
+
325
+ .crud-list-filter-surface__chips {
326
+ align-items: center;
327
+ display: flex;
328
+ flex-wrap: wrap;
329
+ gap: 0.5rem;
330
+ min-width: 0;
331
+ }
332
+
333
+ .crud-list-filter-surface__panel {
334
+ padding: 1rem;
335
+ }
336
+
337
+ .crud-list-filter-surface__controls {
338
+ display: grid;
339
+ gap: 0.75rem;
340
+ grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
341
+ }
342
+
343
+ .crud-list-filter-surface__controls--stacked {
344
+ grid-template-columns: 1fr;
345
+ }
346
+
347
+ .crud-list-filter-surface__control {
348
+ min-width: 0;
349
+ }
350
+
351
+ .crud-list-filter-surface__range {
352
+ display: grid;
353
+ gap: 0.75rem;
354
+ grid-template-columns: repeat(2, minmax(0, 1fr));
355
+ }
356
+
357
+ .crud-list-filter-surface__sheet {
358
+ margin: 0.75rem;
359
+ }
360
+
361
+ .crud-list-filter-surface :deep(.v-field),
362
+ .crud-list-filter-surface :deep(.v-btn),
363
+ .crud-list-filter-surface :deep(.v-selection-control) {
364
+ min-height: 48px;
365
+ }
366
+
367
+ @media (max-width: 640px) {
368
+ .crud-list-filter-surface__summary {
369
+ align-items: stretch;
370
+ flex-direction: column;
371
+ }
372
+
373
+ .crud-list-filter-surface__range {
374
+ grid-template-columns: 1fr;
375
+ }
376
+ }
377
+ </style>