@antelopejs/dms 0.7.4 → 0.7.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.
- package/CHANGELOG.md +37 -0
- package/dist/implementations/dms-base/export-jobs.d.ts +4 -1
- package/dist/implementations/dms-base/export-jobs.js +10 -6
- package/dist/implementations/dms-base/export-jobs.js.map +1 -1
- package/dist/implementations/dms-base/search-route.js +20 -11
- package/dist/implementations/dms-base/search-route.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +2 -0
- package/dist/test/attachment-host/capabilities.js +11 -0
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +23 -5
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +29 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +14 -0
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.js +49 -0
- package/dist/test/unit/interfaces/dms-base/export-jobs-binding.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.js +105 -0
- package/dist/test/unit/interfaces/dms-base/table-view-hidden-columns.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +16 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/dist/test/unit/request-tenant-guards.test.js +38 -0
- package/dist/test/unit/request-tenant-guards.test.js.map +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/accounts.vue +9 -3
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +6 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +6 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/CommandPaletteAssistantAnswer.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageSkeleton.vue +13 -4
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RolePreviewBar.vue +9 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +5 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSummaryRow.vue +4 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/RoleEditor.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityEmail.vue +4 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityPassword.vue +4 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +19 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/theme/AppearanceDeveloper.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/theme/CssVariablesList.vue +6 -3
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +20 -5
- package/frontend-vue/layers/dms-ui/app/build/components/chart/internal/ChartSkeleton.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +10 -2
- package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +7 -2
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +87 -40
- package/frontend-vue/layers/dms-ui/app/build/components/skeleton/RowSkeleton.vue +8 -1
- package/frontend-vue/layers/dms-ui/app/build/components/stat/StatCell.vue +54 -13
- package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +9 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +22 -40
- package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +22 -5
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +49 -9
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/ExpandedRowDetail.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +3 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +3 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +25 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useReorderHandles.ts +93 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableReorder.ts +8 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +20 -2
- package/frontend-vue/layers/dms-ui/app/build/utils/characterCount.ts +8 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +3 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +13 -5
- package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +7 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +10 -3
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +4 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +27 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +24 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/RichText.vue +13 -4
- package/frontend-vue/layers/dms-ui/app/components/form/components/Textarea.vue +27 -2
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +8 -4
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -4
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +15 -4
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +69 -42
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +15 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +8 -2
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +45 -9
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +51 -0
- package/frontend-vue/tests/confirm-dialog-fields.test.ts +12 -0
- package/frontend-vue/tests/drawer-target-direction.test.ts +1 -0
- package/frontend-vue/tests/field-suffix-count.test.ts +120 -0
- package/frontend-vue/tests/form-section-nav-groups.test.ts +116 -0
- package/frontend-vue/tests/nav-card-badge.test.ts +5 -1
- package/frontend-vue/tests/nuxt-ui-aria-labels.test.ts +58 -0
- package/frontend-vue/tests/page-fill-height.test.ts +2 -2
- package/frontend-vue/tests/save-bar.test.ts +49 -0
- package/frontend-vue/tests/stat-group-detail-lines.test.ts +63 -0
- package/frontend-vue/tests/tab-fit.test.ts +99 -0
- package/frontend-vue/tests/table-cell-sublines.test.ts +15 -0
- package/frontend-vue/tests/table-cell-wrap.test.ts +27 -3
- package/frontend-vue/tests/table-row-action-errors.test.ts +26 -0
- package/frontend-vue/tests/table-row-hover.test.ts +3 -1
- package/frontend-vue/tests/table-scroll-area.test.ts +3 -1
- package/frontend-vue/tests/table-view-card-props.test.ts +8 -2
- package/frontend-vue/tests/table-view-cards-reorder.test.ts +250 -0
- package/frontend-vue/tests/table-view-reorder.test.ts +2 -2
- package/frontend-vue/tests/top-list-card-actions.test.ts +91 -0
- package/package.json +2 -2
- package/skills/dms/SKILL.md +14 -1
|
@@ -314,10 +314,19 @@ const toolbarButtons = computed(() => [
|
|
|
314
314
|
padding, so the swap to the editor moves nothing below it. -->
|
|
315
315
|
<template #fallback>
|
|
316
316
|
<div class="min-h-[140px] px-3 py-2.5" aria-busy="true">
|
|
317
|
-
<USkeleton
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
<USkeleton
|
|
318
|
+
:aria-label="t('dms.a11y.loading')"
|
|
319
|
+
class="mb-4 h-8 w-3/4"
|
|
320
|
+
/>
|
|
321
|
+
<USkeleton
|
|
322
|
+
:aria-label="t('dms.a11y.loading')"
|
|
323
|
+
class="mb-3 h-4 w-full"
|
|
324
|
+
/>
|
|
325
|
+
<USkeleton
|
|
326
|
+
:aria-label="t('dms.a11y.loading')"
|
|
327
|
+
class="mb-3 h-4 w-5/6"
|
|
328
|
+
/>
|
|
329
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-4 w-4/5" />
|
|
321
330
|
</div>
|
|
322
331
|
</template>
|
|
323
332
|
</DmsClientOnly>
|
|
@@ -4,15 +4,40 @@ import type {
|
|
|
4
4
|
TextareaEmits,
|
|
5
5
|
TextareaSlots,
|
|
6
6
|
} from "@nuxt/ui/components/Textarea.vue";
|
|
7
|
+
import { computed } from "vue";
|
|
7
8
|
import { useForwardPropsEmits } from "reka-ui";
|
|
9
|
+
import { characterCountText } from "../../../build/utils/characterCount";
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
interface TextareaFieldProps extends TextareaProps {
|
|
12
|
+
/** Longest value accepted; the counter reads against it. */
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
/** Shows the characters typed under the field ("4 / 60"). */
|
|
15
|
+
showCount?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<TextareaFieldProps>();
|
|
10
19
|
const emits = defineEmits<TextareaEmits>();
|
|
11
20
|
defineSlots<TextareaSlots>();
|
|
12
21
|
|
|
13
22
|
const forwarded = useForwardPropsEmits(props, emits);
|
|
23
|
+
const textareaProps = computed(() => {
|
|
24
|
+
const { showCount: _showCount, ...rest } = forwarded.value;
|
|
25
|
+
return rest;
|
|
26
|
+
});
|
|
27
|
+
const countText = computed(() =>
|
|
28
|
+
characterCountText(props.modelValue, props.maxLength),
|
|
29
|
+
);
|
|
14
30
|
</script>
|
|
15
31
|
|
|
16
32
|
<template>
|
|
17
|
-
<
|
|
33
|
+
<div v-if="props.showCount" class="grid gap-1">
|
|
34
|
+
<UTextarea v-bind="textareaProps" class="w-full" />
|
|
35
|
+
<span
|
|
36
|
+
class="text-dimmed justify-self-end text-xs tabular-nums"
|
|
37
|
+
data-character-count
|
|
38
|
+
>
|
|
39
|
+
{{ countText }}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
<UTextarea v-else v-bind="textareaProps" />
|
|
18
43
|
</template>
|
|
@@ -82,7 +82,7 @@ const COLUMN_CLASSES: Record<number, string> = {
|
|
|
82
82
|
3: "gap-x-8 sm:grid-cols-2 lg:grid-cols-3 [&>div:first-child]:border-t-0 sm:[&>div:nth-child(2)]:border-t-0 lg:[&>div:nth-child(3)]:border-t-0",
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
const { locale } = useI18n();
|
|
85
|
+
const { locale, t } = useI18n();
|
|
86
86
|
|
|
87
87
|
const columnClass = computed(
|
|
88
88
|
() =>
|
|
@@ -185,10 +185,10 @@ function keyOf(item: KeyValueItem, index: number): string {
|
|
|
185
185
|
<template v-if="showPlaceholders">
|
|
186
186
|
<div v-for="index in props.skeletonCount" :key="index" :class="rowClass">
|
|
187
187
|
<dt class="min-w-[120px] flex-none">
|
|
188
|
-
<USkeleton class="h-3 w-20" />
|
|
188
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-20" />
|
|
189
189
|
</dt>
|
|
190
190
|
<dd class="ms-auto">
|
|
191
|
-
<USkeleton class="h-3 w-24" />
|
|
191
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-24" />
|
|
192
192
|
</dd>
|
|
193
193
|
</div>
|
|
194
194
|
</template>
|
|
@@ -202,7 +202,11 @@ function keyOf(item: KeyValueItem, index: number): string {
|
|
|
202
202
|
<dd
|
|
203
203
|
class="ms-auto flex min-w-0 flex-wrap items-center justify-end gap-x-1.5 gap-y-0.5 text-end"
|
|
204
204
|
>
|
|
205
|
-
<USkeleton
|
|
205
|
+
<USkeleton
|
|
206
|
+
:aria-label="t('dms.a11y.loading')"
|
|
207
|
+
v-if="props.loading || item.loading"
|
|
208
|
+
class="h-3 w-24"
|
|
209
|
+
/>
|
|
206
210
|
<slot
|
|
207
211
|
v-else-if="slots.value"
|
|
208
212
|
name="value"
|
|
@@ -196,7 +196,11 @@ const isStat = computed(() => props.variant === "stat");
|
|
|
196
196
|
/>
|
|
197
197
|
</div>
|
|
198
198
|
<!-- The value's placeholder takes its 33px line. -->
|
|
199
|
-
<USkeleton
|
|
199
|
+
<USkeleton
|
|
200
|
+
:aria-label="t('dms.a11y.loading')"
|
|
201
|
+
v-if="isFirstLoad"
|
|
202
|
+
class="mt-1 h-[33px] w-32"
|
|
203
|
+
/>
|
|
200
204
|
<div
|
|
201
205
|
v-else-if="hasError"
|
|
202
206
|
class="mt-auto flex items-center justify-between gap-2"
|
|
@@ -242,13 +246,17 @@ const isStat = computed(() => props.variant === "stat");
|
|
|
242
246
|
>
|
|
243
247
|
<div class="grid">
|
|
244
248
|
<div v-if="showDelta" class="flex h-6 items-center">
|
|
245
|
-
<USkeleton class="h-3 w-[58px]" />
|
|
249
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-[58px]" />
|
|
246
250
|
</div>
|
|
247
251
|
<div v-if="footnote" class="flex h-4 items-center">
|
|
248
|
-
<USkeleton class="h-2.5 w-28" />
|
|
252
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-2.5 w-28" />
|
|
249
253
|
</div>
|
|
250
254
|
</div>
|
|
251
|
-
<USkeleton
|
|
255
|
+
<USkeleton
|
|
256
|
+
:aria-label="t('dms.a11y.loading')"
|
|
257
|
+
v-if="showSparkline"
|
|
258
|
+
class="ml-auto h-[34px] w-24"
|
|
259
|
+
/>
|
|
252
260
|
</div>
|
|
253
261
|
<!-- The trend's line holds its place, as in the skeleton, while there is
|
|
254
262
|
no change to show (no comparison period): no "0%" stand-in, and
|
|
@@ -143,12 +143,23 @@ const Wrapper = props.card ? resolveComponent("DmsCard") : "div";
|
|
|
143
143
|
<span v-if="props.label" class="text-toned">
|
|
144
144
|
{{ translate(props.label) }}
|
|
145
145
|
</span>
|
|
146
|
-
<USkeleton
|
|
147
|
-
|
|
146
|
+
<USkeleton
|
|
147
|
+
:aria-label="t('dms.a11y.loading')"
|
|
148
|
+
v-else
|
|
149
|
+
class="h-3 w-1/3"
|
|
150
|
+
/>
|
|
151
|
+
<USkeleton
|
|
152
|
+
:aria-label="t('dms.a11y.loading')"
|
|
153
|
+
class="ms-auto h-3 w-12"
|
|
154
|
+
/>
|
|
148
155
|
</span>
|
|
149
|
-
<USkeleton
|
|
156
|
+
<USkeleton
|
|
157
|
+
:aria-label="t('dms.a11y.loading')"
|
|
158
|
+
class="w-full rounded-full"
|
|
159
|
+
:class="skeletonTrackClass"
|
|
160
|
+
/>
|
|
150
161
|
<span v-if="skeletonHasLegend" class="flex h-4 items-center">
|
|
151
|
-
<USkeleton class="h-2.5 w-40" />
|
|
162
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-2.5 w-40" />
|
|
152
163
|
</span>
|
|
153
164
|
</div>
|
|
154
165
|
<div v-else-if="hasError" class="grid min-w-0 gap-1.5" role="alert">
|
|
@@ -9,9 +9,10 @@ import { trackFloatingSaveBar } from "../../build/composables/form/floatingSaveB
|
|
|
9
9
|
import DmsUnsavedStatus from "../../build/components/form/UnsavedStatus.vue";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* `floating`: the v2 save bar, a card
|
|
13
|
-
* while the form runs past it. `band`: the
|
|
14
|
-
* drawer or a modal (its surface classes come
|
|
12
|
+
* `floating`: the v2 save bar, a card floating over the bottom of the panel
|
|
13
|
+
* while the form runs past it, taking no space while hidden. `band`: the
|
|
14
|
+
* footer band of a form card, a drawer or a modal (its surface classes come
|
|
15
|
+
* from the form).
|
|
15
16
|
*/
|
|
16
17
|
export type SaveBarVariant = "floating" | "band";
|
|
17
18
|
|
|
@@ -66,26 +67,50 @@ const SECONDARY_LABELS: Record<FormKind, string> = {
|
|
|
66
67
|
action: "dms.button.reset",
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
// Its states have the same height,
|
|
70
|
-
//
|
|
70
|
+
// Its states have the same height, so switching between them never moves the
|
|
71
|
+
// content around it. The floating bar overlays the form: its root is a sticky
|
|
72
|
+
// anchor at the form's end that takes no space while the bar hides, and the
|
|
73
|
+
// bar's height plus its gaps while it shows, so the last field scrolls clear
|
|
74
|
+
// of it and nothing above it moves. The band, a footer, keeps its place.
|
|
71
75
|
const theme = tv({
|
|
72
76
|
slots: {
|
|
73
|
-
root: "
|
|
77
|
+
root: "",
|
|
78
|
+
bar: "flex items-center",
|
|
74
79
|
actions: "ms-auto flex shrink-0 items-center gap-2",
|
|
75
80
|
},
|
|
76
81
|
variants: {
|
|
77
82
|
variant: {
|
|
78
83
|
floating: {
|
|
79
|
-
root: "
|
|
84
|
+
root: "pointer-events-none sticky bottom-1 z-5 h-0 [--dms-save-bar-h:50px]",
|
|
85
|
+
bar: "border-accented pointer-events-auto absolute inset-x-0 bottom-3 h-(--dms-save-bar-h) gap-3 rounded-[10px] border bg-(--ui-bg) ps-4 pe-3 text-[13px] shadow-lg transition-[opacity,translate,visibility] duration-200 ease-out",
|
|
80
86
|
},
|
|
81
87
|
band: {
|
|
82
|
-
root: "
|
|
88
|
+
root: "transition-[opacity,translate,visibility] duration-200 ease-out",
|
|
89
|
+
bar: "gap-2",
|
|
83
90
|
},
|
|
84
91
|
},
|
|
85
92
|
hidden: {
|
|
86
|
-
true:
|
|
93
|
+
true: "",
|
|
94
|
+
false: "",
|
|
87
95
|
},
|
|
88
96
|
},
|
|
97
|
+
compoundVariants: [
|
|
98
|
+
{
|
|
99
|
+
variant: "floating",
|
|
100
|
+
hidden: false,
|
|
101
|
+
class: { root: "h-[calc(var(--dms-save-bar-h)+2rem)]" },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
variant: "floating",
|
|
105
|
+
hidden: true,
|
|
106
|
+
class: { bar: "invisible translate-y-2 opacity-0" },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
variant: "band",
|
|
110
|
+
hidden: true,
|
|
111
|
+
class: { root: "invisible translate-y-2 opacity-0" },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
89
114
|
});
|
|
90
115
|
|
|
91
116
|
const BUTTONS = {
|
|
@@ -112,7 +137,7 @@ trackFloatingSaveBar(
|
|
|
112
137
|
() => props.variant === "floating" && !state.value.isHidden,
|
|
113
138
|
);
|
|
114
139
|
|
|
115
|
-
// Empty while there is nothing to save: the hidden bar keeps its line.
|
|
140
|
+
// Empty while there is nothing to save: the hidden bar keeps its one line.
|
|
116
141
|
const statusText = computed(() => {
|
|
117
142
|
if (!props.dirty) return "";
|
|
118
143
|
const changed = props.changes.map((label) => processI18n(label)).join(", ");
|
|
@@ -133,39 +158,41 @@ const saveText = computed(() => {
|
|
|
133
158
|
:inert="state.isHidden || undefined"
|
|
134
159
|
:aria-hidden="state.isHidden || undefined"
|
|
135
160
|
>
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<div :class="ui.actions()">
|
|
142
|
-
<UButton
|
|
143
|
-
v-if="state.secondary === 'discard'"
|
|
144
|
-
:label="t(SECONDARY_LABELS[props.kind])"
|
|
145
|
-
color="neutral"
|
|
146
|
-
:variant="buttons.discardVariant"
|
|
147
|
-
:size="buttons.size"
|
|
148
|
-
:disabled="props.saving"
|
|
149
|
-
@click="emit('discard')"
|
|
150
|
-
/>
|
|
151
|
-
<UButton
|
|
152
|
-
v-else-if="state.secondary === 'cancel'"
|
|
153
|
-
:label="t('dms.button.cancel')"
|
|
154
|
-
color="neutral"
|
|
155
|
-
variant="outline"
|
|
156
|
-
:size="buttons.size"
|
|
157
|
-
@click="emit('cancel')"
|
|
158
|
-
/>
|
|
159
|
-
<UButton
|
|
160
|
-
v-if="state.showsSubmit"
|
|
161
|
-
:label="saveText"
|
|
162
|
-
:size="buttons.size"
|
|
163
|
-
:loading="props.saving"
|
|
164
|
-
:disabled="state.isSubmitHeld"
|
|
165
|
-
:type="props.form ? 'submit' : 'button'"
|
|
166
|
-
:form="props.form"
|
|
167
|
-
@click="props.form ? undefined : emit('save')"
|
|
161
|
+
<div :class="ui.bar()">
|
|
162
|
+
<DmsUnsavedStatus
|
|
163
|
+
v-if="state.showsStatus"
|
|
164
|
+
:text="statusText"
|
|
165
|
+
:is-band="props.variant === 'band'"
|
|
168
166
|
/>
|
|
167
|
+
<div :class="ui.actions()">
|
|
168
|
+
<UButton
|
|
169
|
+
v-if="state.secondary === 'discard'"
|
|
170
|
+
:label="t(SECONDARY_LABELS[props.kind])"
|
|
171
|
+
color="neutral"
|
|
172
|
+
:variant="buttons.discardVariant"
|
|
173
|
+
:size="buttons.size"
|
|
174
|
+
:disabled="props.saving"
|
|
175
|
+
@click="emit('discard')"
|
|
176
|
+
/>
|
|
177
|
+
<UButton
|
|
178
|
+
v-else-if="state.secondary === 'cancel'"
|
|
179
|
+
:label="t('dms.button.cancel')"
|
|
180
|
+
color="neutral"
|
|
181
|
+
variant="outline"
|
|
182
|
+
:size="buttons.size"
|
|
183
|
+
@click="emit('cancel')"
|
|
184
|
+
/>
|
|
185
|
+
<UButton
|
|
186
|
+
v-if="state.showsSubmit"
|
|
187
|
+
:label="saveText"
|
|
188
|
+
:size="buttons.size"
|
|
189
|
+
:loading="props.saving"
|
|
190
|
+
:disabled="state.isSubmitHeld"
|
|
191
|
+
:type="props.form ? 'submit' : 'button'"
|
|
192
|
+
:form="props.form"
|
|
193
|
+
@click="props.form ? undefined : emit('save')"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
169
196
|
</div>
|
|
170
197
|
</div>
|
|
171
198
|
</template>
|
|
@@ -21,6 +21,7 @@ const props = withDefaults(defineProps<TabProps>(), {
|
|
|
21
21
|
stateKey: "tab",
|
|
22
22
|
badgesUrl: undefined,
|
|
23
23
|
realtimeTopic: undefined,
|
|
24
|
+
fit: false,
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
// The badges a module counts for a record's tabs, in one request, again when
|
|
@@ -72,10 +73,23 @@ const ROOT_CLASSES: Record<AxeOrientation, string> = {
|
|
|
72
73
|
[AxeOrientation.vertical]: "max-sm:flex-col max-sm:items-stretch",
|
|
73
74
|
};
|
|
74
75
|
|
|
76
|
+
// A fitted pill list hugs its tabs: the triggers stop growing to share the
|
|
77
|
+
// full width. Link tabs never grow, and a vertical list has its own width.
|
|
78
|
+
const FIT_CLASSES: Record<TabVariant, { list: string; trigger: string }> = {
|
|
79
|
+
[TabVariant.pill]: { list: "w-fit max-w-full", trigger: "grow-0" },
|
|
80
|
+
[TabVariant.link]: { list: "", trigger: "" },
|
|
81
|
+
};
|
|
82
|
+
|
|
75
83
|
const panelClass = computed(() => PANEL_CLASSES[props.orientation]);
|
|
84
|
+
const fitClasses = computed(() =>
|
|
85
|
+
props.fit && props.orientation === AxeOrientation.horizontal
|
|
86
|
+
? FIT_CLASSES[props.variant]
|
|
87
|
+
: undefined,
|
|
88
|
+
);
|
|
76
89
|
const tabsUi = computed(() => ({
|
|
77
90
|
root: ROOT_CLASSES[props.orientation],
|
|
78
|
-
list: LIST_CLASSES[props.orientation],
|
|
91
|
+
list: [LIST_CLASSES[props.orientation], fitClasses.value?.list],
|
|
92
|
+
trigger: fitClasses.value?.trigger,
|
|
79
93
|
// The panel may shrink below its content's width (a wide table scrolls
|
|
80
94
|
// inside it instead of pushing the page sideways).
|
|
81
95
|
content: "min-w-0",
|
|
@@ -451,6 +451,7 @@ const FieldValue = (fieldProps: { column: TableViewColumn; item: T }) => {
|
|
|
451
451
|
{{ col.label }}
|
|
452
452
|
</span>
|
|
453
453
|
<USkeleton
|
|
454
|
+
:aria-label="t('dms.a11y.loading')"
|
|
454
455
|
v-if="isBoardLoading && !cells[col.value]"
|
|
455
456
|
aria-hidden="true"
|
|
456
457
|
class="ms-auto h-[16.5px] w-5 shrink-0 rounded-[5px]"
|
|
@@ -478,17 +479,26 @@ const FieldValue = (fieldProps: { column: TableViewColumn; item: T }) => {
|
|
|
478
479
|
class="border-default grid gap-2 rounded-lg border bg-(--dms-surface-card) px-3 pt-2.5 pb-[11px] shadow-xs"
|
|
479
480
|
>
|
|
480
481
|
<div class="-mb-0.5 flex min-h-5 items-center">
|
|
481
|
-
<USkeleton
|
|
482
|
+
<USkeleton
|
|
483
|
+
:aria-label="t('dms.a11y.loading')"
|
|
484
|
+
class="h-2.5 w-10"
|
|
485
|
+
/>
|
|
482
486
|
</div>
|
|
483
487
|
<div class="flex h-[17px] items-center">
|
|
484
|
-
<USkeleton
|
|
488
|
+
<USkeleton
|
|
489
|
+
:aria-label="t('dms.a11y.loading')"
|
|
490
|
+
class="h-3 w-3/4"
|
|
491
|
+
/>
|
|
485
492
|
</div>
|
|
486
493
|
<div
|
|
487
494
|
v-for="fieldColumn in cardColumns"
|
|
488
495
|
:key="fieldColumn.id"
|
|
489
496
|
class="-mt-0.5 flex h-[18px] items-center"
|
|
490
497
|
>
|
|
491
|
-
<USkeleton
|
|
498
|
+
<USkeleton
|
|
499
|
+
:aria-label="t('dms.a11y.loading')"
|
|
500
|
+
class="h-2.5 w-1/2"
|
|
501
|
+
/>
|
|
492
502
|
</div>
|
|
493
503
|
</div>
|
|
494
504
|
</div>
|
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
} from "../../composables/table-view/types";
|
|
38
38
|
import {
|
|
39
39
|
TABLE_DISPLAY_ID,
|
|
40
|
+
CARDS_DISPLAY_ID,
|
|
40
41
|
KANBAN_DISPLAY_ID,
|
|
41
42
|
GROUPED_DISPLAY_ID,
|
|
42
43
|
TableViewEvents,
|
|
@@ -1019,7 +1020,9 @@ const accumulation = computed<TableAccumulation | undefined>(() =>
|
|
|
1019
1020
|
: undefined,
|
|
1020
1021
|
);
|
|
1021
1022
|
|
|
1022
|
-
// Rows ordered by hand: moving is off while anything narrows the rows
|
|
1023
|
+
// Rows ordered by hand: moving is off while anything narrows the rows, and
|
|
1024
|
+
// only the displays drawing the rows in their order move them.
|
|
1025
|
+
const ORDERED_DISPLAY_IDS = new Set([TABLE_DISPLAY_ID, CARDS_DISPLAY_ID]);
|
|
1023
1026
|
const { reorderState } = useTableReorder<T>({
|
|
1024
1027
|
reorder: reorderConfig,
|
|
1025
1028
|
data,
|
|
@@ -1033,7 +1036,9 @@ const { reorderState } = useTableReorder<T>({
|
|
|
1033
1036
|
!!effectiveGlobalFilter.value ||
|
|
1034
1037
|
activeTabFilters.value.length > 0,
|
|
1035
1038
|
),
|
|
1036
|
-
|
|
1039
|
+
isOrderedDisplay: computed(() =>
|
|
1040
|
+
ORDERED_DISPLAY_IDS.has(activeDisplayId.value),
|
|
1041
|
+
),
|
|
1037
1042
|
refresh: () => refresh(),
|
|
1038
1043
|
onError: (failure) =>
|
|
1039
1044
|
toast.add({
|
|
@@ -1742,6 +1747,7 @@ const displayContext = computed(
|
|
|
1742
1747
|
pageId,
|
|
1743
1748
|
options: optionsForDisplay(activeDisplayId.value),
|
|
1744
1749
|
refresh: refreshAll,
|
|
1750
|
+
reorder: reorderState.value,
|
|
1745
1751
|
table: displayTable.value,
|
|
1746
1752
|
}),
|
|
1747
1753
|
);
|
|
@@ -3,6 +3,9 @@ import { computed, nextTick, onMounted, ref, watch } from "vue";
|
|
|
3
3
|
import { useMounted } from "@vueuse/core";
|
|
4
4
|
import DmsEmptyState from "../empty-state/EmptyState.vue";
|
|
5
5
|
import DmsSectionHeader from "../section-header/SectionHeader.vue";
|
|
6
|
+
import DmsBlockActions, {
|
|
7
|
+
type BlockAction,
|
|
8
|
+
} from "../../build/components/blocks/BlockActions.vue";
|
|
6
9
|
// Imported rather than resolved from the registry: a registered component is
|
|
7
10
|
// a lazy chunk of its own, and a list whose rows only arrive with its data
|
|
8
11
|
// would draw an empty card for the moment that chunk takes.
|
|
@@ -42,6 +45,8 @@ interface Props extends DefaultComponentProps {
|
|
|
42
45
|
maxHeight?: string;
|
|
43
46
|
staticItems?: TopListItem[];
|
|
44
47
|
emptyLabel?: string;
|
|
48
|
+
/** Link buttons in the card head, after the period badge. */
|
|
49
|
+
actions?: BlockAction[];
|
|
45
50
|
/**
|
|
46
51
|
* Placeholder rows while `fetchUrl` loads: the "top N" it answers.
|
|
47
52
|
* Optional. Defaults to 10, enough to fill the default `maxHeight`.
|
|
@@ -90,6 +95,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
90
95
|
currencyCode: "EUR",
|
|
91
96
|
maxHeight: DEFAULT_MAX_HEIGHT,
|
|
92
97
|
skeletonCount: SKELETON_ROW_COUNT,
|
|
98
|
+
actions: () => [],
|
|
93
99
|
});
|
|
94
100
|
|
|
95
101
|
const { locale, t } = useI18n();
|
|
@@ -247,11 +253,33 @@ const gridStyle = computed(() => ({
|
|
|
247
253
|
:title="processI18n(title)"
|
|
248
254
|
:description="description ? processI18n(description) : undefined"
|
|
249
255
|
>
|
|
250
|
-
<template v-if="periodScope" #trailing>
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
<template v-if="periodScope || props.actions.length" #trailing>
|
|
257
|
+
<template v-if="periodScope">
|
|
258
|
+
<UBadge
|
|
259
|
+
v-if="badgeLabel"
|
|
260
|
+
:color="badgeColor"
|
|
261
|
+
variant="soft"
|
|
262
|
+
size="sm"
|
|
263
|
+
>
|
|
264
|
+
{{ badgeLabel }}
|
|
265
|
+
</UBadge>
|
|
266
|
+
<USkeleton
|
|
267
|
+
:aria-label="t('dms.a11y.loading')"
|
|
268
|
+
v-else
|
|
269
|
+
class="h-5 w-16 rounded-[5px]"
|
|
270
|
+
aria-hidden="true"
|
|
271
|
+
/>
|
|
272
|
+
</template>
|
|
273
|
+
<!-- Quiet links, as in an ActivityFeed head: the rows stay the
|
|
274
|
+
card's content. -->
|
|
275
|
+
<DmsBlockActions
|
|
276
|
+
v-if="props.actions.length"
|
|
277
|
+
:actions="props.actions"
|
|
278
|
+
size="xs"
|
|
279
|
+
lead-variant="ghost"
|
|
280
|
+
lead-color="neutral"
|
|
281
|
+
rest-variant="ghost"
|
|
282
|
+
/>
|
|
255
283
|
</template>
|
|
256
284
|
</DmsSectionHeader>
|
|
257
285
|
|
|
@@ -308,15 +336,23 @@ const gridStyle = computed(() => ({
|
|
|
308
336
|
class="flex h-14 items-center gap-3 px-[18px]"
|
|
309
337
|
>
|
|
310
338
|
<USkeleton
|
|
339
|
+
:aria-label="t('dms.a11y.loading')"
|
|
311
340
|
v-if="showRank"
|
|
312
341
|
class="size-[26px] shrink-0 rounded-[7px]"
|
|
313
342
|
/>
|
|
314
343
|
<div class="grid flex-1 gap-1.5">
|
|
315
|
-
<USkeleton class="h-3 w-2/5" />
|
|
316
|
-
<USkeleton
|
|
344
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-2/5" />
|
|
345
|
+
<USkeleton
|
|
346
|
+
:aria-label="t('dms.a11y.loading')"
|
|
347
|
+
class="h-2.5 w-1/4"
|
|
348
|
+
/>
|
|
317
349
|
</div>
|
|
318
|
-
<USkeleton class="h-3 w-14" />
|
|
319
|
-
<USkeleton
|
|
350
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-14" />
|
|
351
|
+
<USkeleton
|
|
352
|
+
:aria-label="t('dms.a11y.loading')"
|
|
353
|
+
v-if="showDelta"
|
|
354
|
+
class="h-3 w-[46px]"
|
|
355
|
+
/>
|
|
320
356
|
</div>
|
|
321
357
|
</div>
|
|
322
358
|
</div>
|
|
@@ -31,6 +31,11 @@ export interface TabProps extends TabComponentProps {
|
|
|
31
31
|
badgesUrl?: string;
|
|
32
32
|
/** Topics whose events make the tab set read `badgesUrl` again. */
|
|
33
33
|
realtimeTopic?: string | string[];
|
|
34
|
+
/**
|
|
35
|
+
* A horizontal `pill` list as wide as its tabs, not the full width; a
|
|
36
|
+
* `link` list already sizes its tabs to their labels.
|
|
37
|
+
*/
|
|
38
|
+
fit?: boolean;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** The badges `badgesUrl` answers with, by tab slot. */
|
|
@@ -95,6 +95,47 @@ export interface TableViewDisplayActions<T> {
|
|
|
95
95
|
custom: (action: CustomRowAction) => void;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Rows ordered by hand (backend `reorder`), as a display moves them. Set on
|
|
100
|
+
* the displays that draw the rows in their order (the grid and `cards`), for
|
|
101
|
+
* a caller who may edit them.
|
|
102
|
+
*/
|
|
103
|
+
export interface TableViewDisplayReorder {
|
|
104
|
+
/** Off while a search, a filter or a tab narrows the rows. */
|
|
105
|
+
enabled: boolean;
|
|
106
|
+
/** Moves the row at `from` to `to`, both indexes into `items`. */
|
|
107
|
+
move: (from: number, to: number) => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The attributes and listeners of a move handle: `v-bind` them on a
|
|
112
|
+
* `<button>`. Dragged, it moves its row onto the card it is dropped on; with
|
|
113
|
+
* it focused, the arrow keys move the row one place back (up, left) or
|
|
114
|
+
* forward (down, right).
|
|
115
|
+
*/
|
|
116
|
+
export interface TableViewReorderHandle {
|
|
117
|
+
type: "button";
|
|
118
|
+
draggable: boolean;
|
|
119
|
+
disabled: boolean;
|
|
120
|
+
"aria-label": string;
|
|
121
|
+
/** Says how to move the row, or, while moving is off, how to turn it on. */
|
|
122
|
+
title: string;
|
|
123
|
+
onClick: (event: MouseEvent) => void;
|
|
124
|
+
onDragstart: (event: DragEvent) => void;
|
|
125
|
+
onDragend: () => void;
|
|
126
|
+
onKeydown: (event: KeyboardEvent) => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A card's move handle, on a hand-ordered `cards` display. */
|
|
130
|
+
export interface TableViewCardReorder {
|
|
131
|
+
/** Off while a search, a filter or a tab narrows the rows. */
|
|
132
|
+
enabled: boolean;
|
|
133
|
+
/** The card is the one being dragged. */
|
|
134
|
+
dragging: boolean;
|
|
135
|
+
/** Bind on the element the card is moved by. */
|
|
136
|
+
handle: TableViewReorderHandle;
|
|
137
|
+
}
|
|
138
|
+
|
|
98
139
|
/**
|
|
99
140
|
* The card the `kanban` and `cards` displays draw (backend `card` option):
|
|
100
141
|
* the columns it shows, or a component drawing it whole.
|
|
@@ -127,6 +168,11 @@ export interface TableViewCardProps<T = Record<string, unknown>> {
|
|
|
127
168
|
open: () => void;
|
|
128
169
|
/** Kanban only: the value of the column the card sits in. */
|
|
129
170
|
groupValue?: string;
|
|
171
|
+
/**
|
|
172
|
+
* `cards` only, on a hand-ordered table (backend `reorder`) and for a
|
|
173
|
+
* caller who may edit: the card's move handle, to draw somewhere on it.
|
|
174
|
+
*/
|
|
175
|
+
reorder?: TableViewCardReorder;
|
|
130
176
|
}
|
|
131
177
|
|
|
132
178
|
/**
|
|
@@ -182,6 +228,11 @@ export interface TableViewDisplayContext<T = Record<string, unknown>> {
|
|
|
182
228
|
options?: Record<string, unknown>;
|
|
183
229
|
/** Re-fetch the table view (shared list + tab counts + active display). */
|
|
184
230
|
refresh: () => Promise<void> | void;
|
|
231
|
+
/**
|
|
232
|
+
* Rows ordered by hand (backend `reorder`): set on the grid and the
|
|
233
|
+
* `cards` display, for a caller who may edit the rows.
|
|
234
|
+
*/
|
|
235
|
+
reorder?: TableViewDisplayReorder;
|
|
185
236
|
|
|
186
237
|
/**
|
|
187
238
|
* Escape hatch: the TanStack table instance, present when the display is
|
|
@@ -231,6 +231,18 @@ describe("ConfirmModal fields", () => {
|
|
|
231
231
|
expect(closed).toHaveBeenCalledWith(true);
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
it("says what the required mark means under the fields", async () => {
|
|
235
|
+
mount({ title: "Delete?", fields: [reassign] });
|
|
236
|
+
await flush();
|
|
237
|
+
expect(host.textContent).toContain("dms.form.required_legend");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("draws no legend when no field is required", async () => {
|
|
241
|
+
mount({ title: "Delete?", fields: [{ ...reassign, required: false }] });
|
|
242
|
+
await flush();
|
|
243
|
+
expect(host.textContent).not.toContain("dms.form.required_legend");
|
|
244
|
+
});
|
|
245
|
+
|
|
234
246
|
it("flags a required field left empty instead of confirming", async () => {
|
|
235
247
|
const onConfirm = vi.fn();
|
|
236
248
|
mount({ title: "Delete?", fields: [reassign], onConfirm });
|
|
@@ -184,6 +184,7 @@ describe("the drawer container", () => {
|
|
|
184
184
|
clearGuards: vi.fn(),
|
|
185
185
|
}));
|
|
186
186
|
vi.stubGlobal("useDmsRoute", () => reactive({ path: "/" }));
|
|
187
|
+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
|
|
187
188
|
});
|
|
188
189
|
|
|
189
190
|
it("slides in from the bottom, full width, by default", async () => {
|