@jskit-ai/http-web 0.1.1

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 (110) hide show
  1. package/fixtures/crud-list-date-filters/App.vue +107 -0
  2. package/fixtures/crud-list-date-filters/index.html +12 -0
  3. package/fixtures/crud-list-date-filters/main.js +24 -0
  4. package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
  5. package/package.json +150 -0
  6. package/src/client/bulkActions.js +47 -0
  7. package/src/client/components/CrudAddEditScreen.vue +186 -0
  8. package/src/client/components/CrudDeleteAction.vue +93 -0
  9. package/src/client/components/CrudListBulkActionSurface.vue +126 -0
  10. package/src/client/components/CrudListDateFilterControl.vue +188 -0
  11. package/src/client/components/CrudListFilterSurface.vue +388 -0
  12. package/src/client/components/CrudListRecordActionMenu.vue +120 -0
  13. package/src/client/components/CrudListScreen.vue +501 -0
  14. package/src/client/components/CrudViewScreen.vue +197 -0
  15. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  16. package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
  17. package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
  18. package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
  19. package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
  20. package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
  21. package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
  22. package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
  23. package/src/client/composables/internal/useOperationScope.js +144 -0
  24. package/src/client/composables/records/useAddEdit.js +224 -0
  25. package/src/client/composables/records/useCrudAddEdit.js +227 -0
  26. package/src/client/composables/records/useCrudList.js +101 -0
  27. package/src/client/composables/records/useCrudView.js +47 -0
  28. package/src/client/composables/records/useList.js +500 -0
  29. package/src/client/composables/records/useView.js +164 -0
  30. package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
  31. package/src/client/composables/runtime/listUiRuntime.js +123 -0
  32. package/src/client/composables/runtime/modelStateHelpers.js +90 -0
  33. package/src/client/composables/runtime/operationAdapters.js +34 -0
  34. package/src/client/composables/runtime/operationUiHelpers.js +125 -0
  35. package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
  36. package/src/client/composables/runtime/useAddEditCore.js +112 -0
  37. package/src/client/composables/runtime/useCommandCore.js +123 -0
  38. package/src/client/composables/runtime/useEndpointResource.js +199 -0
  39. package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
  40. package/src/client/composables/runtime/useListCore.js +112 -0
  41. package/src/client/composables/runtime/useUiFeedback.js +98 -0
  42. package/src/client/composables/runtime/useViewCore.js +93 -0
  43. package/src/client/composables/runtime/viewUiRuntime.js +96 -0
  44. package/src/client/composables/support/errorMessageHelpers.js +66 -0
  45. package/src/client/composables/support/listQueryParamSupport.js +473 -0
  46. package/src/client/composables/support/listSearchSupport.js +70 -0
  47. package/src/client/composables/support/refValueHelpers.js +19 -0
  48. package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
  49. package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
  50. package/src/client/composables/support/routeTemplateHelpers.js +285 -0
  51. package/src/client/composables/support/scopeHelpers.js +145 -0
  52. package/src/client/composables/useAccess.js +109 -0
  53. package/src/client/composables/useCommand.js +118 -0
  54. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  55. package/src/client/composables/useCrudDeleteAction.js +160 -0
  56. package/src/client/composables/useCrudListBulkActions.js +147 -0
  57. package/src/client/composables/useCrudListFilterLookups.js +146 -0
  58. package/src/client/composables/useCrudListFilters.js +339 -0
  59. package/src/client/composables/useCrudListParentTitle.js +151 -0
  60. package/src/client/composables/useCrudListRowActions.js +144 -0
  61. package/src/client/composables/useCrudListScreen.js +237 -0
  62. package/src/client/composables/useCrudViewScreen.js +77 -0
  63. package/src/client/composables/usePagedCollection.js +181 -0
  64. package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
  65. package/src/client/composables/useScopeRuntime.js +135 -0
  66. package/src/client/filters.js +15 -0
  67. package/src/client/index.js +24 -0
  68. package/src/client/lib/httpClient.js +47 -0
  69. package/src/client/lib/permissions.js +34 -0
  70. package/src/client/rowActions.js +47 -0
  71. package/src/client/support/contractGuards.js +34 -0
  72. package/src/client/support/crudListDateFilterSupport.js +91 -0
  73. package/test/addEditUiRuntime.test.js +110 -0
  74. package/test/crudBindingSupport.test.js +110 -0
  75. package/test/crudHttpClientSupport.test.js +77 -0
  76. package/test/crudJsonApiTransportSupport.test.js +179 -0
  77. package/test/crudListBulkActionSurface.test.js +27 -0
  78. package/test/crudListDateFilterSupport.test.js +92 -0
  79. package/test/crudListDateFilterSurface.browser.test.js +182 -0
  80. package/test/crudListFilterSurface.test.js +90 -0
  81. package/test/crudLookupFieldRuntime.test.js +264 -0
  82. package/test/crudScreenComponents.test.js +110 -0
  83. package/test/errorIntentContract.test.js +31 -0
  84. package/test/errorMessageHelpers.test.js +28 -0
  85. package/test/exportsContract.test.js +68 -0
  86. package/test/listQueryParamSupport.test.js +223 -0
  87. package/test/listUiRuntime.test.js +102 -0
  88. package/test/operationValidationHelpers.test.js +64 -0
  89. package/test/packageBoundary.test.js +31 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/refValueHelpers.test.js +14 -0
  92. package/test/requestTransportOptions.test.js +457 -0
  93. package/test/resourceLoadStateHelpers.test.js +189 -0
  94. package/test/routeTemplateHelpers.test.js +85 -0
  95. package/test/scopeHelpers.test.js +57 -0
  96. package/test/useAddEditCore.test.js +124 -0
  97. package/test/useAddEditRequestQueryParams.test.js +190 -0
  98. package/test/useCrudAddEdit.test.js +415 -0
  99. package/test/useCrudDeleteAction.test.js +139 -0
  100. package/test/useCrudListBulkActions.test.js +65 -0
  101. package/test/useCrudListFilterLookups.test.js +114 -0
  102. package/test/useCrudListFilters.test.js +293 -0
  103. package/test/useCrudListParentTitle.test.js +262 -0
  104. package/test/useCrudListRowActions.test.js +77 -0
  105. package/test/useListRouteInitialization.test.js +337 -0
  106. package/test/useListSearchSupport.test.js +61 -0
  107. package/test/usePagedCollection.test.js +53 -0
  108. package/test/useViewRequestQueryParams.test.js +20 -0
  109. package/test/viewCoreLoading.test.js +44 -0
  110. package/test/viewUiRuntime.test.js +95 -0
@@ -0,0 +1,126 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useDisplay } from "vuetify";
4
+
5
+ const props = defineProps({
6
+ runtime: {
7
+ type: Object,
8
+ default: null
9
+ },
10
+ title: {
11
+ type: String,
12
+ default: "Selection"
13
+ }
14
+ });
15
+
16
+ const display = useDisplay();
17
+ const isCompactLayout = computed(() => {
18
+ const displayName = String(display?.name?.value || "").trim().toLowerCase();
19
+ return displayName === "xs" || displayName === "sm";
20
+ });
21
+ const actions = computed(() => Array.isArray(props.runtime?.actions) ? props.runtime.actions : []);
22
+ const selectedCount = computed(() => Number(props.runtime?.selectedCount?.value || 0));
23
+ const shouldRender = computed(() =>
24
+ Boolean(props.runtime?.hasActions?.value) &&
25
+ Boolean(props.runtime?.hasSelection?.value) &&
26
+ selectedCount.value > 0
27
+ );
28
+
29
+ function clearSelection() {
30
+ props.runtime?.clearSelection?.();
31
+ }
32
+
33
+ function execute(action = {}) {
34
+ props.runtime?.execute?.(action);
35
+ }
36
+
37
+ function isActionDisabled(action = {}) {
38
+ return Boolean(props.runtime?.isActionDisabled?.(action));
39
+ }
40
+
41
+ function isActionExecuting(action = {}) {
42
+ return Boolean(props.runtime?.isActionExecuting?.(action));
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <section v-if="shouldRender" class="crud-list-bulk-action-surface">
48
+ <div class="crud-list-bulk-action-surface__summary">
49
+ <div class="crud-list-bulk-action-surface__copy">
50
+ <span class="text-overline text-medium-emphasis">{{ title }}</span>
51
+ <strong>{{ selectedCount }} selected</strong>
52
+ </div>
53
+ <v-btn size="small" variant="text" @click="clearSelection">Clear</v-btn>
54
+ </div>
55
+
56
+ <div v-if="!isCompactLayout" class="crud-list-bulk-action-surface__actions">
57
+ <v-btn
58
+ v-for="action in actions"
59
+ :key="action.key"
60
+ :color="action.color"
61
+ :variant="action.variant"
62
+ :prepend-icon="action.icon || undefined"
63
+ :disabled="isActionDisabled(action)"
64
+ :loading="isActionExecuting(action)"
65
+ @click="execute(action)"
66
+ >
67
+ {{ action.label }}
68
+ </v-btn>
69
+ </div>
70
+
71
+ <v-menu v-else location="bottom end">
72
+ <template #activator="{ props: menuProps }">
73
+ <v-btn v-bind="menuProps" color="primary" variant="tonal">Bulk actions</v-btn>
74
+ </template>
75
+ <v-list density="compact" min-width="180">
76
+ <v-list-item
77
+ v-for="action in actions"
78
+ :key="action.key"
79
+ :title="action.label"
80
+ :prepend-icon="action.icon || undefined"
81
+ :disabled="isActionDisabled(action)"
82
+ @click="execute(action)"
83
+ />
84
+ </v-list>
85
+ </v-menu>
86
+ </section>
87
+ </template>
88
+
89
+ <style scoped>
90
+ .crud-list-bulk-action-surface {
91
+ align-items: center;
92
+ background: rgba(var(--v-theme-primary), 0.08);
93
+ border-block: 1px solid rgba(var(--v-theme-primary), 0.18);
94
+ display: flex;
95
+ gap: 0.75rem;
96
+ justify-content: space-between;
97
+ min-width: 0;
98
+ padding: 0.75rem 1rem;
99
+ }
100
+
101
+ .crud-list-bulk-action-surface__summary,
102
+ .crud-list-bulk-action-surface__actions {
103
+ align-items: center;
104
+ display: flex;
105
+ flex-wrap: wrap;
106
+ gap: 0.5rem;
107
+ min-width: 0;
108
+ }
109
+
110
+ .crud-list-bulk-action-surface__copy {
111
+ display: grid;
112
+ gap: 0.1rem;
113
+ min-width: 0;
114
+ }
115
+
116
+ .crud-list-bulk-action-surface :deep(.v-btn) {
117
+ min-height: 48px;
118
+ }
119
+
120
+ @media (max-width: 640px) {
121
+ .crud-list-bulk-action-surface {
122
+ align-items: stretch;
123
+ flex-direction: column;
124
+ }
125
+ }
126
+ </style>
@@ -0,0 +1,188 @@
1
+ <script setup>
2
+ import { computed, nextTick, ref, useAttrs, useId } from "vue";
3
+ import { useLocale } from "vuetify";
4
+ import {
5
+ parseDateOnlyValue,
6
+ formatDateOnlyValue,
7
+ formatDateOnlyDisplay
8
+ } from "../support/crudListDateFilterSupport.js";
9
+
10
+ defineOptions({
11
+ inheritAttrs: false
12
+ });
13
+
14
+ const props = defineProps({
15
+ modelValue: {
16
+ type: String,
17
+ default: ""
18
+ },
19
+ label: {
20
+ type: String,
21
+ required: true
22
+ },
23
+ controlId: {
24
+ type: String,
25
+ default: ""
26
+ },
27
+ placeholder: {
28
+ type: String,
29
+ default: "Select date"
30
+ },
31
+ disabled: {
32
+ type: Boolean,
33
+ default: false
34
+ },
35
+ readonly: {
36
+ type: Boolean,
37
+ default: false
38
+ }
39
+ });
40
+
41
+ const emit = defineEmits(["update:modelValue"]);
42
+ const attrs = useAttrs();
43
+ const { current: currentLocale } = useLocale();
44
+ const generatedId = useId();
45
+ const menuOpen = ref(false);
46
+ const textFieldRef = ref(null);
47
+
48
+ const resolvedControlId = computed(() => props.controlId || `crud-list-date-${generatedId}`);
49
+ const pickerId = computed(() => `${resolvedControlId.value}-picker`);
50
+ const pickerValue = computed(() => parseDateOnlyValue(props.modelValue));
51
+ const displayValue = computed(() => formatDateOnlyDisplay(props.modelValue, {
52
+ locale: currentLocale?.value
53
+ }));
54
+ const pickerTitle = computed(() => `${props.label} calendar`);
55
+
56
+ function restoreFocus() {
57
+ void nextTick(() => {
58
+ textFieldRef.value?.focus?.();
59
+ });
60
+ }
61
+
62
+ function openMenu() {
63
+ if (props.disabled || props.readonly) {
64
+ return;
65
+ }
66
+ menuOpen.value = true;
67
+ }
68
+
69
+ function closeMenu() {
70
+ menuOpen.value = false;
71
+ restoreFocus();
72
+ }
73
+
74
+ function clearDate() {
75
+ if (props.disabled || props.readonly) {
76
+ return;
77
+ }
78
+ emit("update:modelValue", "");
79
+ closeMenu();
80
+ }
81
+
82
+ function selectDate(value) {
83
+ const nextValue = formatDateOnlyValue(value);
84
+ if (!nextValue) {
85
+ return;
86
+ }
87
+ emit("update:modelValue", nextValue);
88
+ closeMenu();
89
+ }
90
+
91
+ function handleActivatorKeydown(event) {
92
+ if (!["Enter", " ", "ArrowDown"].includes(event.key)) {
93
+ return;
94
+ }
95
+ event.preventDefault();
96
+ openMenu();
97
+ }
98
+ </script>
99
+
100
+ <template>
101
+ <v-menu
102
+ v-model="menuOpen"
103
+ :close-on-content-click="false"
104
+ :open-on-click="false"
105
+ location="bottom start"
106
+ :offset="6"
107
+ min-width="0"
108
+ >
109
+ <template #activator="{ props: activatorProps }">
110
+ <v-text-field
111
+ v-bind="{ ...attrs, ...activatorProps }"
112
+ :id="resolvedControlId"
113
+ ref="textFieldRef"
114
+ :model-value="displayValue"
115
+ :label="label"
116
+ :placeholder="placeholder"
117
+ :aria-label="label"
118
+ :aria-controls="pickerId"
119
+ :aria-expanded="String(menuOpen)"
120
+ aria-haspopup="dialog"
121
+ :data-date-filter-control="resolvedControlId"
122
+ append-inner-icon="$calendar"
123
+ :disabled="disabled"
124
+ :clearable="Boolean(modelValue) && !disabled && !readonly"
125
+ persistent-clear
126
+ persistent-placeholder
127
+ readonly
128
+ inputmode="none"
129
+ variant="outlined"
130
+ density="comfortable"
131
+ hide-details="auto"
132
+ class="crud-list-date-filter-control"
133
+ @click:control="openMenu"
134
+ @click:append-inner="openMenu"
135
+ @click:clear.stop="clearDate"
136
+ @keydown="handleActivatorKeydown"
137
+ />
138
+ </template>
139
+
140
+ <v-card
141
+ :id="pickerId"
142
+ :aria-label="pickerTitle"
143
+ role="dialog"
144
+ class="crud-list-date-filter-control__picker"
145
+ @keydown.esc.stop.prevent="closeMenu"
146
+ >
147
+ <v-date-picker
148
+ :model-value="pickerValue"
149
+ :title="pickerTitle"
150
+ :header="label"
151
+ :disabled="disabled || readonly"
152
+ color="primary"
153
+ show-adjacent-months
154
+ width="100%"
155
+ @update:model-value="selectDate"
156
+ />
157
+ <v-divider />
158
+ <v-card-actions>
159
+ <v-btn
160
+ variant="text"
161
+ :disabled="disabled || readonly || !modelValue"
162
+ @click="clearDate"
163
+ >
164
+ Clear
165
+ </v-btn>
166
+ <v-spacer />
167
+ <v-btn color="primary" variant="text" @click="closeMenu">Close</v-btn>
168
+ </v-card-actions>
169
+ </v-card>
170
+ </v-menu>
171
+ </template>
172
+
173
+ <style scoped>
174
+ .crud-list-date-filter-control {
175
+ min-width: 0;
176
+ width: 100%;
177
+ }
178
+
179
+ .crud-list-date-filter-control__picker {
180
+ max-width: calc(100vw - 2rem);
181
+ overflow: hidden;
182
+ width: 22rem;
183
+ }
184
+
185
+ .crud-list-date-filter-control__picker :deep(.v-date-picker) {
186
+ max-width: 100%;
187
+ }
188
+ </style>
@@ -0,0 +1,388 @@
1
+ <script setup>
2
+ import { computed, ref } from "vue";
3
+ import { useDisplay } from "vuetify";
4
+ import CrudListDateFilterControl from "./CrudListDateFilterControl.vue";
5
+
6
+ const props = defineProps({
7
+ filters: {
8
+ type: Object,
9
+ default: () => ({})
10
+ },
11
+ runtime: {
12
+ type: Object,
13
+ default: null
14
+ },
15
+ title: {
16
+ type: String,
17
+ default: "Filters"
18
+ }
19
+ });
20
+
21
+ const display = useDisplay();
22
+ const sheetOpen = ref(false);
23
+
24
+ const filterEntries = computed(() =>
25
+ Object.values(props.filters && typeof props.filters === "object" && !Array.isArray(props.filters)
26
+ ? props.filters
27
+ : {})
28
+ .filter((filter) => filter?.key && filter?.type)
29
+ );
30
+ const runtimeValues = computed(() => props.runtime?.values || {});
31
+ const activeChips = computed(() => Array.isArray(props.runtime?.activeChips?.value) ? props.runtime.activeChips.value : []);
32
+ const hasActiveFilters = computed(() => Boolean(props.runtime?.hasActiveFilters?.value));
33
+ const shouldRender = computed(() => filterEntries.value.length > 0 && Boolean(props.runtime?.values));
34
+ const isCompactLayout = computed(() => {
35
+ const displayName = String(display?.name?.value || "").trim().toLowerCase();
36
+ return displayName === "xs" || displayName === "sm";
37
+ });
38
+
39
+ function optionItems(filter = {}) {
40
+ return Array.isArray(filter.options) ? filter.options : [];
41
+ }
42
+
43
+ function placeholder(filter = {}, fallback = "") {
44
+ return String(filter?.ui?.placeholder || fallback || "").trim();
45
+ }
46
+
47
+ function clearChip(chip = {}) {
48
+ props.runtime?.clearChip?.(chip);
49
+ }
50
+
51
+ function clearFilters() {
52
+ props.runtime?.clearFilters?.();
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <section v-if="shouldRender" class="crud-list-filter-surface">
58
+ <div class="crud-list-filter-surface__summary">
59
+ <v-btn
60
+ v-if="isCompactLayout"
61
+ color="primary"
62
+ variant="tonal"
63
+ class="crud-list-filter-surface__open"
64
+ @click="sheetOpen = true"
65
+ >
66
+ {{ title }}
67
+ <v-chip v-if="activeChips.length > 0" class="ml-2" size="x-small" color="primary" variant="flat">
68
+ {{ activeChips.length }}
69
+ </v-chip>
70
+ </v-btn>
71
+
72
+ <div v-if="hasActiveFilters" class="crud-list-filter-surface__chips">
73
+ <v-chip
74
+ v-for="chip in activeChips"
75
+ :key="chip.id"
76
+ closable
77
+ size="small"
78
+ variant="tonal"
79
+ @click:close="clearChip(chip)"
80
+ >
81
+ {{ chip.label }}
82
+ </v-chip>
83
+ <v-btn size="small" variant="text" @click="clearFilters">Clear all</v-btn>
84
+ </div>
85
+ </div>
86
+
87
+ <v-sheet v-if="!isCompactLayout" rounded="lg" border class="crud-list-filter-surface__panel">
88
+ <div class="crud-list-filter-surface__controls">
89
+ <template v-for="filter in filterEntries" :key="filter.key">
90
+ <v-switch
91
+ v-if="filter.type === 'flag'"
92
+ v-model="runtimeValues[filter.key]"
93
+ :label="filter.label"
94
+ color="primary"
95
+ density="comfortable"
96
+ hide-details
97
+ class="crud-list-filter-surface__control"
98
+ />
99
+ <v-select
100
+ v-else-if="filter.type === 'enum' || filter.type === 'enumMany' || filter.type === 'presence'"
101
+ v-model="runtimeValues[filter.key]"
102
+ :items="optionItems(filter)"
103
+ :label="filter.label"
104
+ :placeholder="placeholder(filter)"
105
+ :multiple="filter.type === 'enumMany'"
106
+ :chips="filter.type === 'enumMany'"
107
+ :closable-chips="filter.type === 'enumMany'"
108
+ clearable
109
+ item-title="label"
110
+ item-value="value"
111
+ variant="outlined"
112
+ density="comfortable"
113
+ hide-details="auto"
114
+ class="crud-list-filter-surface__control"
115
+ />
116
+ <v-combobox
117
+ v-else-if="filter.type === 'recordIdMany'"
118
+ v-model="runtimeValues[filter.key]"
119
+ :label="filter.label"
120
+ :placeholder="placeholder(filter, 'Enter ids')"
121
+ multiple
122
+ chips
123
+ closable-chips
124
+ clearable
125
+ variant="outlined"
126
+ density="comfortable"
127
+ hide-details="auto"
128
+ class="crud-list-filter-surface__control"
129
+ />
130
+ <v-text-field
131
+ v-else-if="filter.type === 'recordId'"
132
+ v-model="runtimeValues[filter.key]"
133
+ :label="filter.label"
134
+ :placeholder="placeholder(filter, 'Enter id')"
135
+ clearable
136
+ variant="outlined"
137
+ density="comfortable"
138
+ hide-details="auto"
139
+ class="crud-list-filter-surface__control"
140
+ />
141
+ <div
142
+ v-else-if="filter.type === 'dateRange'"
143
+ class="crud-list-filter-surface__range crud-list-filter-surface__range--date"
144
+ role="group"
145
+ :aria-label="`${filter.label} date range`"
146
+ :data-filter-key="filter.key"
147
+ data-filter-type="date-range"
148
+ >
149
+ <CrudListDateFilterControl
150
+ v-model="runtimeValues[filter.key].from"
151
+ :label="`${filter.label} from`"
152
+ :control-id="`${filter.key}-from`"
153
+ :placeholder="placeholder(filter, 'Select start date')"
154
+ />
155
+ <CrudListDateFilterControl
156
+ v-model="runtimeValues[filter.key].to"
157
+ :label="`${filter.label} to`"
158
+ :control-id="`${filter.key}-to`"
159
+ :placeholder="placeholder(filter, 'Select end date')"
160
+ />
161
+ </div>
162
+ <div v-else-if="filter.type === 'numberRange'" class="crud-list-filter-surface__range">
163
+ <v-text-field
164
+ v-model="runtimeValues[filter.key].min"
165
+ :label="`${filter.label} min`"
166
+ type="number"
167
+ variant="outlined"
168
+ density="comfortable"
169
+ hide-details="auto"
170
+ />
171
+ <v-text-field
172
+ v-model="runtimeValues[filter.key].max"
173
+ :label="`${filter.label} max`"
174
+ type="number"
175
+ variant="outlined"
176
+ density="comfortable"
177
+ hide-details="auto"
178
+ />
179
+ </div>
180
+ <CrudListDateFilterControl
181
+ v-else-if="filter.type === 'date'"
182
+ v-model="runtimeValues[filter.key]"
183
+ :label="filter.label"
184
+ :control-id="filter.key"
185
+ :placeholder="placeholder(filter, 'Select date')"
186
+ class="crud-list-filter-surface__control"
187
+ />
188
+ </template>
189
+ </div>
190
+ </v-sheet>
191
+
192
+ <v-dialog v-model="sheetOpen" max-width="640" location="bottom">
193
+ <v-card rounded="lg" class="crud-list-filter-surface__sheet">
194
+ <v-card-title class="d-flex align-center justify-space-between">
195
+ <span>{{ title }}</span>
196
+ <v-btn variant="text" @click="sheetOpen = false">Close</v-btn>
197
+ </v-card-title>
198
+ <v-card-text>
199
+ <div class="crud-list-filter-surface__controls crud-list-filter-surface__controls--stacked">
200
+ <template v-for="filter in filterEntries" :key="filter.key">
201
+ <v-switch
202
+ v-if="filter.type === 'flag'"
203
+ v-model="runtimeValues[filter.key]"
204
+ :label="filter.label"
205
+ color="primary"
206
+ density="comfortable"
207
+ hide-details
208
+ />
209
+ <v-select
210
+ v-else-if="filter.type === 'enum' || filter.type === 'enumMany' || filter.type === 'presence'"
211
+ v-model="runtimeValues[filter.key]"
212
+ :items="optionItems(filter)"
213
+ :label="filter.label"
214
+ :placeholder="placeholder(filter)"
215
+ :multiple="filter.type === 'enumMany'"
216
+ :chips="filter.type === 'enumMany'"
217
+ :closable-chips="filter.type === 'enumMany'"
218
+ clearable
219
+ item-title="label"
220
+ item-value="value"
221
+ variant="outlined"
222
+ density="comfortable"
223
+ hide-details="auto"
224
+ />
225
+ <v-combobox
226
+ v-else-if="filter.type === 'recordIdMany'"
227
+ v-model="runtimeValues[filter.key]"
228
+ :label="filter.label"
229
+ :placeholder="placeholder(filter, 'Enter ids')"
230
+ multiple
231
+ chips
232
+ closable-chips
233
+ clearable
234
+ variant="outlined"
235
+ density="comfortable"
236
+ hide-details="auto"
237
+ />
238
+ <v-text-field
239
+ v-else-if="filter.type === 'recordId'"
240
+ v-model="runtimeValues[filter.key]"
241
+ :label="filter.label"
242
+ :placeholder="placeholder(filter, 'Enter id')"
243
+ clearable
244
+ variant="outlined"
245
+ density="comfortable"
246
+ hide-details="auto"
247
+ />
248
+ <div
249
+ v-else-if="filter.type === 'dateRange'"
250
+ class="crud-list-filter-surface__range crud-list-filter-surface__range--date"
251
+ role="group"
252
+ :aria-label="`${filter.label} date range`"
253
+ :data-filter-key="filter.key"
254
+ data-filter-type="date-range"
255
+ >
256
+ <CrudListDateFilterControl
257
+ v-model="runtimeValues[filter.key].from"
258
+ :label="`${filter.label} from`"
259
+ :control-id="`${filter.key}-from`"
260
+ :placeholder="placeholder(filter, 'Select start date')"
261
+ />
262
+ <CrudListDateFilterControl
263
+ v-model="runtimeValues[filter.key].to"
264
+ :label="`${filter.label} to`"
265
+ :control-id="`${filter.key}-to`"
266
+ :placeholder="placeholder(filter, 'Select end date')"
267
+ />
268
+ </div>
269
+ <div v-else-if="filter.type === 'numberRange'" class="crud-list-filter-surface__range">
270
+ <v-text-field
271
+ v-model="runtimeValues[filter.key].min"
272
+ :label="`${filter.label} min`"
273
+ type="number"
274
+ variant="outlined"
275
+ density="comfortable"
276
+ hide-details="auto"
277
+ />
278
+ <v-text-field
279
+ v-model="runtimeValues[filter.key].max"
280
+ :label="`${filter.label} max`"
281
+ type="number"
282
+ variant="outlined"
283
+ density="comfortable"
284
+ hide-details="auto"
285
+ />
286
+ </div>
287
+ <CrudListDateFilterControl
288
+ v-else-if="filter.type === 'date'"
289
+ v-model="runtimeValues[filter.key]"
290
+ :label="filter.label"
291
+ :control-id="filter.key"
292
+ :placeholder="placeholder(filter, 'Select date')"
293
+ />
294
+ </template>
295
+ </div>
296
+ </v-card-text>
297
+ <v-card-actions class="justify-space-between">
298
+ <v-btn variant="text" @click="clearFilters">Clear all</v-btn>
299
+ <v-btn color="primary" variant="flat" @click="sheetOpen = false">Apply</v-btn>
300
+ </v-card-actions>
301
+ </v-card>
302
+ </v-dialog>
303
+ </section>
304
+ </template>
305
+
306
+ <style scoped>
307
+ .crud-list-filter-surface {
308
+ display: flex;
309
+ flex-direction: column;
310
+ gap: 0.75rem;
311
+ min-width: 0;
312
+ }
313
+
314
+ .crud-list-filter-surface__summary {
315
+ align-items: center;
316
+ display: flex;
317
+ flex-wrap: wrap;
318
+ gap: 0.75rem;
319
+ justify-content: space-between;
320
+ }
321
+
322
+ .crud-list-filter-surface__open {
323
+ min-height: 48px;
324
+ }
325
+
326
+ .crud-list-filter-surface__chips {
327
+ align-items: center;
328
+ display: flex;
329
+ flex-wrap: wrap;
330
+ gap: 0.5rem;
331
+ min-width: 0;
332
+ }
333
+
334
+ .crud-list-filter-surface__panel {
335
+ padding: 1rem;
336
+ }
337
+
338
+ .crud-list-filter-surface__controls {
339
+ display: grid;
340
+ gap: 0.75rem;
341
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
342
+ }
343
+
344
+ .crud-list-filter-surface__controls--stacked {
345
+ grid-template-columns: 1fr;
346
+ }
347
+
348
+ .crud-list-filter-surface__control {
349
+ min-width: 0;
350
+ }
351
+
352
+ .crud-list-filter-surface__range {
353
+ display: grid;
354
+ gap: 0.75rem;
355
+ grid-template-columns: repeat(2, minmax(0, 1fr));
356
+ min-width: 0;
357
+ }
358
+
359
+ .crud-list-filter-surface__range--date {
360
+ grid-column: span 2;
361
+ grid-template-columns: repeat(2, minmax(14rem, 1fr));
362
+ }
363
+
364
+ .crud-list-filter-surface__controls--stacked .crud-list-filter-surface__range--date {
365
+ grid-column: auto;
366
+ }
367
+
368
+ .crud-list-filter-surface__sheet {
369
+ margin: 0.75rem;
370
+ }
371
+
372
+ .crud-list-filter-surface :deep(.v-field),
373
+ .crud-list-filter-surface :deep(.v-btn),
374
+ .crud-list-filter-surface :deep(.v-selection-control) {
375
+ min-height: 48px;
376
+ }
377
+
378
+ @media (max-width: 640px) {
379
+ .crud-list-filter-surface__summary {
380
+ align-items: stretch;
381
+ flex-direction: column;
382
+ }
383
+
384
+ .crud-list-filter-surface__range {
385
+ grid-template-columns: 1fr;
386
+ }
387
+ }
388
+ </style>