@omnitend/dashboard-for-laravel 0.26.0 → 0.28.0
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.
- package/dist/components/charts/chartTheme.d.ts +1 -0
- package/dist/components/extended/DXTable.vue.d.ts +9 -44
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +80 -0
- package/dist/components/extended/DXTablePagination.vue.d.ts +27 -0
- package/dist/composables/useResourceEditor.d.ts +74 -0
- package/dist/dashboard-for-laravel.js +12343 -12491
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +8 -8
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +2 -2
- package/docs/public/api-reference.json +193 -7
- package/docs/public/docs-map.md +2 -2
- package/package.json +2 -2
- package/resources/css/theme.scss +168 -7
- package/resources/js/components/charts/DXBarChart.vue +1 -1
- package/resources/js/components/charts/DXDoughnutChart.vue +1 -1
- package/resources/js/components/charts/DXLineChart.vue +1 -1
- package/resources/js/components/charts/chartTheme.ts +19 -16
- package/resources/js/components/extended/DXTable.vue +243 -1008
- package/resources/js/components/extended/DXTableEditorModal.vue +247 -0
- package/resources/js/components/extended/DXTablePagination.vue +126 -0
- package/resources/js/composables/defineForm.ts +14 -2
- package/resources/js/composables/useResourceEditor.ts +542 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Internal edit/create modal for `DXTable` (#129). Renders the `DModal` +
|
|
4
|
+
`DXForm` and maps DXTable's `edit-value`/`edit-span`/`tab-*` slots onto
|
|
5
|
+
DXForm's `value`/`span`/`tab-*` slots, providing the row context and a `close`
|
|
6
|
+
handle. Extracted so `DXTable` is a thin façade over the table render, the
|
|
7
|
+
pagination, the editor logic (`useResourceEditor`) and this modal — rather than
|
|
8
|
+
owning all four.
|
|
9
|
+
|
|
10
|
+
The edit-modal slots reach here forwarded from DXTable (consumer → DXTable →
|
|
11
|
+
here → DXForm). This component computes which keyed slots the consumer actually
|
|
12
|
+
provided from its OWN `$slots`, so the slot-plumbing lives entirely with the
|
|
13
|
+
modal, not the composable.
|
|
14
|
+
-->
|
|
15
|
+
<template>
|
|
16
|
+
<DModal
|
|
17
|
+
:model-value="show"
|
|
18
|
+
:title="title"
|
|
19
|
+
:size="size"
|
|
20
|
+
@update:model-value="emit('update:show', $event)"
|
|
21
|
+
>
|
|
22
|
+
<!-- Loading the full record for edit (showUrl fetch) -->
|
|
23
|
+
<div
|
|
24
|
+
v-if="loading"
|
|
25
|
+
class="dx-edit-loading d-flex align-items-center gap-2 text-muted mb-3"
|
|
26
|
+
>
|
|
27
|
+
<DSpinner small />
|
|
28
|
+
<span>Loading…</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Edit/create form (tabbed when tabs provided, flat otherwise).
|
|
32
|
+
|
|
33
|
+
Keyed per modal open: the form OBJECT is created once and then
|
|
34
|
+
reseeded in place for each row, so without a key Vue reuses the
|
|
35
|
+
same DXField instances across records — and any per-field UI
|
|
36
|
+
state rides along with them. That leaked a revealed password
|
|
37
|
+
from one row into the next. The key gives each record a fresh
|
|
38
|
+
field subtree; the data still lives in `form`, so nothing
|
|
39
|
+
is lost by remounting. -->
|
|
40
|
+
<DXForm
|
|
41
|
+
v-if="form"
|
|
42
|
+
:key="formInstanceKey"
|
|
43
|
+
:active-tab="activeTab"
|
|
44
|
+
:form="form"
|
|
45
|
+
:fields="fields"
|
|
46
|
+
:tabs="tabs"
|
|
47
|
+
:context="item ?? undefined"
|
|
48
|
+
:show-submit="false"
|
|
49
|
+
@update:active-tab="emit('update:activeTab', $event)"
|
|
50
|
+
@submit="emit('save')"
|
|
51
|
+
>
|
|
52
|
+
<!-- Forward DXTable's edit-value(key) → DXForm value(key) -->
|
|
53
|
+
<template
|
|
54
|
+
v-for="key in editValueSlotKeys"
|
|
55
|
+
:key="`ev-${key}`"
|
|
56
|
+
#[`value(${key})`]="sp"
|
|
57
|
+
>
|
|
58
|
+
<!--
|
|
59
|
+
@slot Custom input for field `<key>` in the edit/create modal, forwarded to DXForm. Name it `edit-value(<fieldKey>)`.
|
|
60
|
+
@binding {object} item The row being edited (null in create mode).
|
|
61
|
+
@binding {any} value The current field value.
|
|
62
|
+
@binding {Function} update Call with a new value to update the field.
|
|
63
|
+
@binding {object} field The field definition.
|
|
64
|
+
-->
|
|
65
|
+
<slot
|
|
66
|
+
:name="`edit-value(${key})`"
|
|
67
|
+
:item="item"
|
|
68
|
+
:value="sp.value"
|
|
69
|
+
:update="sp.update"
|
|
70
|
+
:field="sp.field"
|
|
71
|
+
/>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<!-- Forward edit-span(key) → span(key) -->
|
|
75
|
+
<template
|
|
76
|
+
v-for="key in editSpanSlotKeys"
|
|
77
|
+
:key="`es-${key}`"
|
|
78
|
+
#[`span(${key})`]="sp"
|
|
79
|
+
>
|
|
80
|
+
<!--
|
|
81
|
+
@slot Full-width custom content for field `<key>` in the edit/create modal, forwarded to DXForm's span slot. Name it `edit-span(<fieldKey>)`.
|
|
82
|
+
@binding {object} item The row being edited (null in create mode).
|
|
83
|
+
@binding {any} value The current field value.
|
|
84
|
+
@binding {Function} update Call with a new value to update the field.
|
|
85
|
+
@binding {Function} close Call to close the edit modal.
|
|
86
|
+
-->
|
|
87
|
+
<slot
|
|
88
|
+
:name="`edit-span(${key})`"
|
|
89
|
+
:item="item"
|
|
90
|
+
:value="sp.value"
|
|
91
|
+
:update="sp.update"
|
|
92
|
+
:close="close"
|
|
93
|
+
/>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<!-- Forward tab-content / tab-before / tab-after slots -->
|
|
97
|
+
<template
|
|
98
|
+
v-for="key in tabContentSlotKeys"
|
|
99
|
+
:key="`tc-${key}`"
|
|
100
|
+
#[`tab-content(${key})`]="sp"
|
|
101
|
+
>
|
|
102
|
+
<!--
|
|
103
|
+
@slot Replaces the auto-rendered fields of edit-modal tab `<key>` with custom content. Name it `tab-content(<tabKey>)`.
|
|
104
|
+
@binding {object} item The row being edited (null in create mode).
|
|
105
|
+
@binding {object} tab The tab definition.
|
|
106
|
+
-->
|
|
107
|
+
<slot :name="`tab-content(${key})`" :item="item" :tab="sp.tab" />
|
|
108
|
+
</template>
|
|
109
|
+
<template
|
|
110
|
+
v-for="key in tabBeforeSlotKeys"
|
|
111
|
+
:key="`tb-${key}`"
|
|
112
|
+
#[`tab-before(${key})`]="sp"
|
|
113
|
+
>
|
|
114
|
+
<!--
|
|
115
|
+
@slot Custom content rendered before the fields of edit-modal tab `<key>`. Name it `tab-before(<tabKey>)`.
|
|
116
|
+
@binding {object} item The row being edited (null in create mode).
|
|
117
|
+
@binding {object} tab The tab definition.
|
|
118
|
+
-->
|
|
119
|
+
<slot :name="`tab-before(${key})`" :item="item" :tab="sp.tab" />
|
|
120
|
+
</template>
|
|
121
|
+
<template
|
|
122
|
+
v-for="key in tabAfterSlotKeys"
|
|
123
|
+
:key="`taf-${key}`"
|
|
124
|
+
#[`tab-after(${key})`]="sp"
|
|
125
|
+
>
|
|
126
|
+
<!--
|
|
127
|
+
@slot Custom content rendered after the fields of edit-modal tab `<key>`. Name it `tab-after(<tabKey>)`.
|
|
128
|
+
@binding {object} item The row being edited (null in create mode).
|
|
129
|
+
@binding {object} tab The tab definition.
|
|
130
|
+
-->
|
|
131
|
+
<slot :name="`tab-after(${key})`" :item="item" :tab="sp.tab" />
|
|
132
|
+
</template>
|
|
133
|
+
</DXForm>
|
|
134
|
+
|
|
135
|
+
<template #footer>
|
|
136
|
+
<div class="d-flex justify-content-between w-100">
|
|
137
|
+
<div>
|
|
138
|
+
<DButton
|
|
139
|
+
v-if="deleteUrl && !isCreateMode"
|
|
140
|
+
variant="danger"
|
|
141
|
+
:loading="pendingAction === 'delete'"
|
|
142
|
+
loading-text="Deleting..."
|
|
143
|
+
:disabled="form?.processing || loading"
|
|
144
|
+
@click="emit('delete')"
|
|
145
|
+
>
|
|
146
|
+
Delete
|
|
147
|
+
</DButton>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="d-flex gap-2">
|
|
150
|
+
<DButton variant="secondary" @click="close">
|
|
151
|
+
Cancel
|
|
152
|
+
</DButton>
|
|
153
|
+
<DButton
|
|
154
|
+
variant="primary"
|
|
155
|
+
:loading="pendingAction === 'save'"
|
|
156
|
+
:loading-text="isCreateMode ? 'Creating...' : 'Saving...'"
|
|
157
|
+
:disabled="form?.processing || loading"
|
|
158
|
+
@click="emit('save')"
|
|
159
|
+
>
|
|
160
|
+
{{ isCreateMode ? 'Create' : 'Save Changes' }}
|
|
161
|
+
</DButton>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
</DModal>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
<script setup lang="ts">
|
|
169
|
+
import { computed, useSlots } from "vue";
|
|
170
|
+
import DModal from "../base/DModal.vue";
|
|
171
|
+
import DButton from "../base/DButton.vue";
|
|
172
|
+
import DSpinner from "../base/DSpinner.vue";
|
|
173
|
+
import DXForm from "./DXForm.vue";
|
|
174
|
+
import type { EditTab } from "./DXTable.vue";
|
|
175
|
+
|
|
176
|
+
interface Props {
|
|
177
|
+
/** Whether the modal is open (v-model:show). */
|
|
178
|
+
show: boolean;
|
|
179
|
+
/** Modal title. */
|
|
180
|
+
title: string;
|
|
181
|
+
/** Modal size. */
|
|
182
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
183
|
+
/** True while the full record is loading via showUrl. */
|
|
184
|
+
loading: boolean;
|
|
185
|
+
/** The edit/create form (a useForm instance), or null before first open. */
|
|
186
|
+
form: any;
|
|
187
|
+
/** Bumped per modal open to remount the DXForm subtree (fresh DXField state). */
|
|
188
|
+
formInstanceKey: number;
|
|
189
|
+
/** Active tab index (v-model:active-tab). */
|
|
190
|
+
activeTab: number;
|
|
191
|
+
/** Field definitions passed to DXForm. */
|
|
192
|
+
fields: any[];
|
|
193
|
+
/** Tab definitions passed to DXForm. */
|
|
194
|
+
tabs?: EditTab[];
|
|
195
|
+
/** The row being edited (null in create mode) — the slot/context row. */
|
|
196
|
+
item: any;
|
|
197
|
+
/** Whether the modal is in create mode (vs edit). */
|
|
198
|
+
isCreateMode: boolean;
|
|
199
|
+
/** Which action is in flight, so Save/Delete show their own loading label. */
|
|
200
|
+
pendingAction: 'save' | 'delete' | null;
|
|
201
|
+
/** Delete endpoint — gates the Delete button. */
|
|
202
|
+
deleteUrl?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const props = defineProps<Props>();
|
|
206
|
+
|
|
207
|
+
const emit = defineEmits<{
|
|
208
|
+
/** v-model:show — the modal open state changed. */
|
|
209
|
+
'update:show': [value: boolean];
|
|
210
|
+
/** v-model:active-tab — the active tab changed. */
|
|
211
|
+
'update:activeTab': [value: number];
|
|
212
|
+
/** Save was requested (footer button or DXForm submit). */
|
|
213
|
+
save: [];
|
|
214
|
+
/** Cancel/close was requested. */
|
|
215
|
+
cancel: [];
|
|
216
|
+
/** Delete was requested. */
|
|
217
|
+
delete: [];
|
|
218
|
+
}>();
|
|
219
|
+
|
|
220
|
+
// Close the modal — shared by the Cancel button and the `close` binding handed
|
|
221
|
+
// to consumers' edit-span slots.
|
|
222
|
+
const close = () => emit('cancel');
|
|
223
|
+
|
|
224
|
+
// Which keyed slots the consumer actually provided (computed from THIS
|
|
225
|
+
// component's slots — they're forwarded here from DXTable). Forwarding only the
|
|
226
|
+
// provided ones stops DXForm mistaking an always-present wrapper for a real
|
|
227
|
+
// custom-value override.
|
|
228
|
+
const slots = useSlots();
|
|
229
|
+
const fieldKeys = computed<string[]>(() => (props.fields ?? []).map((field: any) => field.key));
|
|
230
|
+
const tabKeys = computed<string[]>(() => (props.tabs ?? []).map((tab) => tab.key));
|
|
231
|
+
|
|
232
|
+
const editValueSlotKeys = computed(() =>
|
|
233
|
+
fieldKeys.value.filter((key) => !!slots[`edit-value(${key})`]),
|
|
234
|
+
);
|
|
235
|
+
const editSpanSlotKeys = computed(() =>
|
|
236
|
+
fieldKeys.value.filter((key) => !!slots[`edit-span(${key})`]),
|
|
237
|
+
);
|
|
238
|
+
const tabContentSlotKeys = computed(() =>
|
|
239
|
+
tabKeys.value.filter((key) => !!slots[`tab-content(${key})`]),
|
|
240
|
+
);
|
|
241
|
+
const tabBeforeSlotKeys = computed(() =>
|
|
242
|
+
tabKeys.value.filter((key) => !!slots[`tab-before(${key})`]),
|
|
243
|
+
);
|
|
244
|
+
const tabAfterSlotKeys = computed(() =>
|
|
245
|
+
tabKeys.value.filter((key) => !!slots[`tab-after(${key})`]),
|
|
246
|
+
);
|
|
247
|
+
</script>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Internal pagination + per-page footer for `DXTable`. Renders the pager, the
|
|
4
|
+
per-page selector, and the "X to Y out of Z items" info line (with the
|
|
5
|
+
"Filtered from N" note when a column filter is active).
|
|
6
|
+
|
|
7
|
+
Extracted because `DXTable` used to carry three byte-identical copies of this
|
|
8
|
+
footer — one per data mode (client-side / inertia / provider) — that differed
|
|
9
|
+
only in which `PaginationData` object and page-change handler they used, so
|
|
10
|
+
every footer fix had to be written three times (#123). `DXTable` now derives
|
|
11
|
+
the active pagination + handler and renders this once.
|
|
12
|
+
|
|
13
|
+
It owns its own `:deep(.pagination …)` styling (rather than leaving it in
|
|
14
|
+
`DXTable`), because a scoped `:deep()` in `DXTable` would not reach the pager
|
|
15
|
+
once it lives in a child component — the scope-id doesn't cross the boundary.
|
|
16
|
+
Anchoring the rules on this component's own `.dx-table-pagination` root (a
|
|
17
|
+
plain element it owns) gives the scope-id a deterministic host.
|
|
18
|
+
-->
|
|
19
|
+
<template>
|
|
20
|
+
<div class="dx-table-pagination mt-3">
|
|
21
|
+
<!-- Top row: Pagination and Per-page selector -->
|
|
22
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
23
|
+
<!-- Pagination controls (only when multiple pages) -->
|
|
24
|
+
<DPagination
|
|
25
|
+
v-if="showPagination && pagination.total > pagination.per_page"
|
|
26
|
+
:model-value="pagination.current_page"
|
|
27
|
+
:total-rows="pagination.total"
|
|
28
|
+
:per-page="pagination.per_page"
|
|
29
|
+
size="sm"
|
|
30
|
+
@update:model-value="emit('page-change', $event as number)"
|
|
31
|
+
/>
|
|
32
|
+
<div v-else></div>
|
|
33
|
+
|
|
34
|
+
<!-- Per-page selector -->
|
|
35
|
+
<div v-if="showPerPageSelector" class="d-flex align-items-center gap-2">
|
|
36
|
+
<label for="perPageSelect" class="mb-0 small text-muted">Per page</label>
|
|
37
|
+
<DFormSelect
|
|
38
|
+
id="perPageSelect"
|
|
39
|
+
:model-value="perPage"
|
|
40
|
+
:options="perPageOptions.map(n => ({ value: n, text: n.toString() }))"
|
|
41
|
+
size="sm"
|
|
42
|
+
style="width: 85px;"
|
|
43
|
+
@update:model-value="emit('per-page-change', $event)"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Bottom row: Info text -->
|
|
49
|
+
<div class="small text-muted">
|
|
50
|
+
<div>
|
|
51
|
+
<template v-if="pagination.total > pagination.per_page">
|
|
52
|
+
{{ pagination.from }} to {{ pagination.to }} out of {{ pagination.total }} {{ pagination.total === 1 ? singularItemName : pluralItemName }}.
|
|
53
|
+
</template>
|
|
54
|
+
<template v-else-if="pagination.total === 1">
|
|
55
|
+
{{ pagination.total }} {{ singularItemName }}.
|
|
56
|
+
</template>
|
|
57
|
+
<template v-else>
|
|
58
|
+
{{ pagination.total }} {{ pluralItemName }}.
|
|
59
|
+
</template>
|
|
60
|
+
</div>
|
|
61
|
+
<div v-if="hasActiveFilters && pagination.total_unfiltered">
|
|
62
|
+
<small>Filtered from {{ pagination.total_unfiltered }} {{ pagination.total_unfiltered === 1 ? singularItemName : pluralItemName }}.</small>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import DPagination from "../base/DPagination.vue";
|
|
70
|
+
import DFormSelect from "../base/DFormSelect.vue";
|
|
71
|
+
import type { PaginationData } from "./DXTable.vue";
|
|
72
|
+
|
|
73
|
+
interface Props {
|
|
74
|
+
/** The active pagination metadata (client-side / inertia / provider). */
|
|
75
|
+
pagination: PaginationData;
|
|
76
|
+
/** The effective page size, shown in the selector. */
|
|
77
|
+
perPage: number;
|
|
78
|
+
/** Page sizes offered by the selector. */
|
|
79
|
+
perPageOptions: number[];
|
|
80
|
+
/** Whether to render the pager (still hidden when there is only one page). */
|
|
81
|
+
showPagination: boolean;
|
|
82
|
+
/** Whether to render the per-page selector. */
|
|
83
|
+
showPerPageSelector: boolean;
|
|
84
|
+
/** Singular item noun for the info line (e.g. "product"). */
|
|
85
|
+
singularItemName: string;
|
|
86
|
+
/** Plural item noun for the info line (e.g. "products"). */
|
|
87
|
+
pluralItemName: string;
|
|
88
|
+
/** Whether a column filter is active — gates the "Filtered from N" note. */
|
|
89
|
+
hasActiveFilters: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
defineProps<Props>();
|
|
93
|
+
|
|
94
|
+
const emit = defineEmits<{
|
|
95
|
+
/** The user picked a page. Payload is the new 1-based page number. */
|
|
96
|
+
'page-change': [page: number];
|
|
97
|
+
/**
|
|
98
|
+
* The per-page selector changed. Payload is the raw DFormSelect model value
|
|
99
|
+
* (the parent's `handlePerPageChange` narrows it — DFormSelect mirrors BVN's
|
|
100
|
+
* model, which can be a scalar, null, or array).
|
|
101
|
+
*/
|
|
102
|
+
'per-page-change': [value: number | string | null | (number | string)[]];
|
|
103
|
+
}>();
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
/* Pagination button sizing to match form controls. Anchored on this component's
|
|
108
|
+
own `.dx-table-pagination` root so the scope-id has a plain-element host — a
|
|
109
|
+
`:deep()` rule that targeted the pager from DXTable would not reach it once
|
|
110
|
+
the pager lives here (the scope-id doesn't cross the component boundary). */
|
|
111
|
+
.dx-table-pagination :deep(.pagination) {
|
|
112
|
+
margin-bottom: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.dx-table-pagination :deep(.pagination-sm .page-link) {
|
|
116
|
+
min-width: 2.25rem;
|
|
117
|
+
height: auto;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Make disabled pagination buttons more subtle */
|
|
121
|
+
.dx-table-pagination :deep(.pagination .page-item.disabled .page-link) {
|
|
122
|
+
background-color: transparent;
|
|
123
|
+
border-color: transparent;
|
|
124
|
+
opacity: 0.3;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
@@ -65,10 +65,22 @@ export function defineForm<const TFields extends readonly FormFieldDefinition[]>
|
|
|
65
65
|
): DefineFormReturn<InferFormData<TFields>> {
|
|
66
66
|
type FormData = InferFormData<TFields>;
|
|
67
67
|
|
|
68
|
-
// Extract initial data from field definitions
|
|
68
|
+
// Extract initial data from field definitions.
|
|
69
|
+
//
|
|
70
|
+
// #125: `field.default ?? getDefaultValueForType(type)` cannot express a
|
|
71
|
+
// *null* default — a select whose "none" option is `value: null` (which is
|
|
72
|
+
// what the column stores) seeded '' (the type default), matched no option,
|
|
73
|
+
// and rendered blank. This is the same bug #122 fixed in DXTable. Decide on
|
|
74
|
+
// definedness, not nullishness, so null survives; an absent (or explicitly
|
|
75
|
+
// `undefined`) default still falls back to the type default rather than
|
|
76
|
+
// seeding `undefined`, which would drop the key from the JSON payload. This
|
|
77
|
+
// matches DXRepeater's `!== undefined` rule, so all seeding sites agree.
|
|
69
78
|
const initialData = fieldDefinitions.reduce(
|
|
70
79
|
(acc, field) => {
|
|
71
|
-
acc[field.key] =
|
|
80
|
+
acc[field.key] =
|
|
81
|
+
field.default !== undefined
|
|
82
|
+
? field.default
|
|
83
|
+
: getDefaultValueForType(field.type);
|
|
72
84
|
return acc;
|
|
73
85
|
},
|
|
74
86
|
{} as Record<string, any>,
|