@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
|
@@ -6,12 +6,17 @@ import CardGridSkeleton from "./CardGridSkeleton.vue";
|
|
|
6
6
|
import type { TableViewColumn } from "../../../composables/table-view/types/column";
|
|
7
7
|
import type {
|
|
8
8
|
TableViewCardConfig,
|
|
9
|
+
TableViewCardReorder,
|
|
9
10
|
TableViewDisplayContext,
|
|
10
11
|
} from "../../../composables/table-view/types/display";
|
|
11
12
|
import {
|
|
12
13
|
buildCardProps,
|
|
13
14
|
cardFieldColumns,
|
|
14
15
|
} from "../../composables/table-view/utils/card";
|
|
16
|
+
import {
|
|
17
|
+
REORDER_HANDLE_CLASS,
|
|
18
|
+
useReorderHandles,
|
|
19
|
+
} from "../../composables/table-view/useReorderHandles";
|
|
15
20
|
|
|
16
21
|
interface CardsDisplayProps {
|
|
17
22
|
context: TableViewDisplayContext<T>;
|
|
@@ -85,6 +90,22 @@ const FieldValue = (fieldProps: { column: TableViewColumn; item: T }) => {
|
|
|
85
90
|
};
|
|
86
91
|
|
|
87
92
|
const openItem = (item: T) => props.context.actions.open(item);
|
|
93
|
+
|
|
94
|
+
// A hand-ordered table (backend `reorder`): each card carries a move handle,
|
|
95
|
+
// and is where another card dragged by its handle drops.
|
|
96
|
+
const { draggedIndex, handleFor, drop, allowDrop } = useReorderHandles({
|
|
97
|
+
reorder: () => props.context.reorder,
|
|
98
|
+
count: () => props.context.items.length,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const cardReorder = (index: number): TableViewCardReorder | undefined =>
|
|
102
|
+
props.context.reorder
|
|
103
|
+
? {
|
|
104
|
+
enabled: props.context.reorder.enabled,
|
|
105
|
+
dragging: draggedIndex.value === index,
|
|
106
|
+
handle: handleFor(index),
|
|
107
|
+
}
|
|
108
|
+
: undefined;
|
|
88
109
|
</script>
|
|
89
110
|
|
|
90
111
|
<template>
|
|
@@ -107,30 +128,49 @@ const openItem = (item: T) => props.context.actions.open(item);
|
|
|
107
128
|
class="grid grid-cols-[repeat(auto-fill,minmax(min(230px,100%),1fr))] gap-3 px-[18px] pt-4 pb-[18px]"
|
|
108
129
|
>
|
|
109
130
|
<template v-if="cardComponent">
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
<!-- A wrapper out of the grid's layout, catching the drop on the
|
|
132
|
+
card whatever its root element. -->
|
|
133
|
+
<div
|
|
134
|
+
v-for="(item, index) in props.context.items"
|
|
113
135
|
:key="rowId(item)"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
class="contents"
|
|
137
|
+
@dragover="allowDrop"
|
|
138
|
+
@drop="drop(index)"
|
|
139
|
+
>
|
|
140
|
+
<component
|
|
141
|
+
:is="cardComponent"
|
|
142
|
+
v-bind="{
|
|
143
|
+
...(card?.component?.options ?? {}),
|
|
144
|
+
...buildCardProps(item, props.context),
|
|
145
|
+
reorder: cardReorder(index),
|
|
146
|
+
}"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
119
149
|
</template>
|
|
120
150
|
<template v-else>
|
|
121
151
|
<article
|
|
122
|
-
v-for="item in props.context.items"
|
|
152
|
+
v-for="(item, index) in props.context.items"
|
|
123
153
|
:key="rowId(item)"
|
|
124
154
|
class="group border-default hover:border-primary/35 @container cursor-pointer rounded-[10px] border bg-(--ui-bg) p-3.5 text-[12.5px] transition-colors focus-visible:outline-2 focus-visible:outline-(--dms-accent-line)"
|
|
125
155
|
:class="{
|
|
126
156
|
'border-primary ring-primary ring-1':
|
|
127
157
|
props.context.selection.isSelected(rowId(item)),
|
|
158
|
+
'opacity-50': draggedIndex === index,
|
|
128
159
|
}"
|
|
129
160
|
tabindex="0"
|
|
130
161
|
@click="openItem(item)"
|
|
131
162
|
@keydown.enter.self="openItem(item)"
|
|
163
|
+
@dragover="allowDrop"
|
|
164
|
+
@drop="drop(index)"
|
|
132
165
|
>
|
|
133
166
|
<header class="flex items-center gap-2.5">
|
|
167
|
+
<button
|
|
168
|
+
v-if="props.context.reorder"
|
|
169
|
+
v-bind="handleFor(index)"
|
|
170
|
+
:class="['-ms-1.5', REORDER_HANDLE_CLASS]"
|
|
171
|
+
>
|
|
172
|
+
<UIcon name="i-ph-dots-six-vertical" />
|
|
173
|
+
</button>
|
|
134
174
|
<span
|
|
135
175
|
class="bg-accented text-default grid size-7 shrink-0 place-items-center rounded-[7px] font-mono text-[10.5px] font-bold"
|
|
136
176
|
>
|
|
@@ -386,6 +386,8 @@ interface IdentityBadgeOption {
|
|
|
386
386
|
interface IdentityOptions {
|
|
387
387
|
avatarField?: string;
|
|
388
388
|
icon?: string;
|
|
389
|
+
/** Row field holding the tile's icon; `icon` when it is empty. */
|
|
390
|
+
iconField?: string;
|
|
389
391
|
subtitleField?: string;
|
|
390
392
|
subtitleTone?: string;
|
|
391
393
|
selfField?: string;
|
|
@@ -431,7 +433,7 @@ function renderIdentity(value: unknown, options: unknown, row: Row) {
|
|
|
431
433
|
| { key: string }
|
|
432
434
|
| string
|
|
433
435
|
| null,
|
|
434
|
-
icon: opts.icon,
|
|
436
|
+
icon: stringOf(readRowField(row, opts.iconField)) ?? opts.icon,
|
|
435
437
|
selfId: stringOf(readRowField(row, opts.selfField)),
|
|
436
438
|
selfLabel: opts.selfLabel ? processI18n(opts.selfLabel) : undefined,
|
|
437
439
|
badges,
|
|
@@ -51,8 +51,9 @@ export interface SaveBarState {
|
|
|
51
51
|
/** The submit shows but waits for a change. */
|
|
52
52
|
isSubmitHeld: boolean;
|
|
53
53
|
/**
|
|
54
|
-
* Nothing to offer: the bar
|
|
55
|
-
*
|
|
54
|
+
* Nothing to offer: the bar hides. The floating bar takes no space then
|
|
55
|
+
* (it overlays the form), a band keeps its place; either way its showing
|
|
56
|
+
* up never moves the content above it.
|
|
56
57
|
*/
|
|
57
58
|
isHidden: boolean;
|
|
58
59
|
}
|
|
@@ -21,6 +21,31 @@ export interface FormSectionsLayout {
|
|
|
21
21
|
sections: FormSectionLayout[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** A run of the side navigation's entries under one heading, or none. */
|
|
25
|
+
export interface FormSectionNavGroup {
|
|
26
|
+
label?: string;
|
|
27
|
+
sections: FormSectionLayout[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The side navigation's entries, in order, cut wherever the heading changes:
|
|
32
|
+
* consecutive sections naming the same `navGroup` share one heading.
|
|
33
|
+
*/
|
|
34
|
+
export function groupSectionsForNav(
|
|
35
|
+
sections: readonly FormSectionLayout[],
|
|
36
|
+
): FormSectionNavGroup[] {
|
|
37
|
+
const groups: FormSectionNavGroup[] = [];
|
|
38
|
+
for (const section of sections) {
|
|
39
|
+
const current = groups.at(-1);
|
|
40
|
+
if (current && current.label === section.navGroup) {
|
|
41
|
+
current.sections.push(section);
|
|
42
|
+
} else {
|
|
43
|
+
groups.push({ label: section.navGroup, sections: [section] });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return groups;
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
/** An error of the form, as UForm lists them: the name it went under. */
|
|
25
50
|
export interface FormErrorRef {
|
|
26
51
|
name?: string;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { nextTick, ref } from "vue";
|
|
2
|
+
import type {
|
|
3
|
+
TableViewDisplayReorder,
|
|
4
|
+
TableViewReorderHandle,
|
|
5
|
+
} from "../../../composables/table-view/types/display";
|
|
6
|
+
|
|
7
|
+
// One place back or forward: a row of the grid steps up and down, a card of
|
|
8
|
+
// the cards grid also left and right.
|
|
9
|
+
const STEP_BY_KEY: Record<string, number> = {
|
|
10
|
+
ArrowUp: -1,
|
|
11
|
+
ArrowLeft: -1,
|
|
12
|
+
ArrowDown: 1,
|
|
13
|
+
ArrowRight: 1,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DRAG_EFFECT = "move";
|
|
17
|
+
|
|
18
|
+
/** The look of a move handle, on a grid row and on a card. */
|
|
19
|
+
export const REORDER_HANDLE_CLASS =
|
|
20
|
+
"inline-flex size-6 shrink-0 cursor-grab items-center justify-center rounded text-dimmed hover:text-highlighted focus-visible:outline-2 focus-visible:outline-(--dms-accent-line) disabled:cursor-not-allowed disabled:opacity-40 [&>svg]:size-4";
|
|
21
|
+
|
|
22
|
+
export interface ReorderHandlesOptions {
|
|
23
|
+
reorder: () => TableViewDisplayReorder | undefined;
|
|
24
|
+
/** How many rows the display draws: a step past either end does nothing. */
|
|
25
|
+
count: () => number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The move handles of a hand-ordered display, the grid's and the cards'
|
|
30
|
+
* alike: the row dragged by its handle is dropped on another row, and the
|
|
31
|
+
* arrow keys on a handle move its row by one, keeping the focus on it.
|
|
32
|
+
*/
|
|
33
|
+
export function useReorderHandles(options: ReorderHandlesOptions) {
|
|
34
|
+
const { t } = useI18n();
|
|
35
|
+
const draggedIndex = ref<number | undefined>();
|
|
36
|
+
|
|
37
|
+
const isEnabled = () => !!options.reorder()?.enabled;
|
|
38
|
+
|
|
39
|
+
const drop = (index: number) => {
|
|
40
|
+
const from = draggedIndex.value;
|
|
41
|
+
draggedIndex.value = undefined;
|
|
42
|
+
if (from !== undefined && isEnabled()) options.reorder()!.move(from, index);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const allowDrop = (event: DragEvent) => {
|
|
46
|
+
if (isEnabled()) event.preventDefault();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const step = (index: number, delta: number) => {
|
|
50
|
+
const target = index + delta;
|
|
51
|
+
if (!isEnabled() || target < 0 || target >= options.count()) return false;
|
|
52
|
+
options.reorder()!.move(index, target);
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const startDrag = (index: number, event: DragEvent) => {
|
|
57
|
+
draggedIndex.value = index;
|
|
58
|
+
// Firefox starts a drag only once it carries data.
|
|
59
|
+
event.dataTransfer?.setData("text/plain", String(index));
|
|
60
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = DRAG_EFFECT;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The handle moves with its row (rows are keyed by id), and the browser
|
|
64
|
+
// drops the focus of an element it moves: give it back.
|
|
65
|
+
const stepByKey = (index: number, event: KeyboardEvent) => {
|
|
66
|
+
const delta = STEP_BY_KEY[event.key];
|
|
67
|
+
if (delta === undefined) return;
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
const handle = event.currentTarget as HTMLElement | null;
|
|
70
|
+
if (step(index, delta)) void nextTick(() => handle?.focus());
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleFor = (index: number): TableViewReorderHandle => {
|
|
74
|
+
const enabled = isEnabled();
|
|
75
|
+
return {
|
|
76
|
+
type: "button",
|
|
77
|
+
draggable: enabled,
|
|
78
|
+
disabled: !enabled,
|
|
79
|
+
"aria-label": t("dms.table.reorder.move"),
|
|
80
|
+
title: enabled
|
|
81
|
+
? t("dms.table.reorder.move")
|
|
82
|
+
: t("dms.table.reorder.disabled"),
|
|
83
|
+
onClick: (event) => event.stopPropagation(),
|
|
84
|
+
onDragstart: (event) => startDrag(index, event),
|
|
85
|
+
onDragend: () => {
|
|
86
|
+
draggedIndex.value = undefined;
|
|
87
|
+
},
|
|
88
|
+
onKeydown: (event) => stepByKey(index, event),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return { draggedIndex, handleFor, drop, allowDrop };
|
|
93
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
2
2
|
import type { ComputedRef, Ref } from "vue";
|
|
3
3
|
import type {
|
|
4
|
+
TableViewDisplayReorder,
|
|
4
5
|
TableViewListResponse,
|
|
5
6
|
TableViewReorderConfig,
|
|
6
7
|
} from "../../../composables/table-view/types";
|
|
7
|
-
import type { TableReorder } from "../../components/table/Table.vue";
|
|
8
8
|
import { moveRow, reorderEdits } from "./utils/reorder";
|
|
9
9
|
|
|
10
10
|
export interface TableReorderOptions<T> {
|
|
@@ -18,8 +18,8 @@ export interface TableReorderOptions<T> {
|
|
|
18
18
|
canEdit: ComputedRef<boolean>;
|
|
19
19
|
/** A search, a filter or a tab narrows the rows. */
|
|
20
20
|
isNarrowed: ComputedRef<boolean>;
|
|
21
|
-
/** The
|
|
22
|
-
|
|
21
|
+
/** The display shown draws the rows in their order: the grid, the cards. */
|
|
22
|
+
isOrderedDisplay: ComputedRef<boolean>;
|
|
23
23
|
/** Lists the rows again, after a move failed. */
|
|
24
24
|
refresh: () => Promise<void> | void;
|
|
25
25
|
onError: (error: unknown) => void;
|
|
@@ -28,8 +28,9 @@ export interface TableReorderOptions<T> {
|
|
|
28
28
|
/**
|
|
29
29
|
* Rows ordered by hand: a move updates the page at once and saves, by a
|
|
30
30
|
* partial edit, only the rows whose position changed. Moving is off while
|
|
31
|
-
* the rows are narrowed (the handle says how to turn it back on);
|
|
32
|
-
*
|
|
31
|
+
* the rows are narrowed (the handle says how to turn it back on); on a
|
|
32
|
+
* display that does not draw the rows in their order (the board, the
|
|
33
|
+
* groups), or for a caller who may not edit, there is no handle at all.
|
|
33
34
|
*/
|
|
34
35
|
export function useTableReorder<T>(options: TableReorderOptions<T>) {
|
|
35
36
|
const { reorder, data, rowIdKey } = options;
|
|
@@ -66,8 +67,8 @@ export function useTableReorder<T>(options: TableReorderOptions<T>) {
|
|
|
66
67
|
}
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
const state = computed<
|
|
70
|
-
if (!reorder || !options.
|
|
70
|
+
const state = computed<TableViewDisplayReorder | undefined>(() => {
|
|
71
|
+
if (!reorder || !options.isOrderedDisplay.value || !options.canEdit.value) {
|
|
71
72
|
return undefined;
|
|
72
73
|
}
|
|
73
74
|
return { enabled: !options.isNarrowed.value, move };
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -652,6 +652,25 @@ export const useTableRowActions = <T extends Data>(
|
|
|
652
652
|
};
|
|
653
653
|
};
|
|
654
654
|
|
|
655
|
+
const findShownRow = (id: string | undefined): Data | undefined =>
|
|
656
|
+
config.rowNavigation
|
|
657
|
+
?.rows()
|
|
658
|
+
.find(
|
|
659
|
+
(row) =>
|
|
660
|
+
String(getItemId(row, config.rowIdKey ?? DEFAULT_ROW_ID_KEY)) === id,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* What a fixed dialog words: the rows' count, and for one row shown in the
|
|
665
|
+
* table its own fields too (`{name}`). The count wins a clash, it is what
|
|
666
|
+
* every bulk dialog may name.
|
|
667
|
+
*/
|
|
668
|
+
const confirmRowFields = (ids: string[]): Data => {
|
|
669
|
+
const count = { count: ids.length };
|
|
670
|
+
const row = ids.length === 1 ? findShownRow(ids[0]) : undefined;
|
|
671
|
+
return row ? { ...row, ...count } : count;
|
|
672
|
+
};
|
|
673
|
+
|
|
655
674
|
/** Asks the declared confirmation of a bulk action, running it inside. */
|
|
656
675
|
const askDeclaredBulkConfirm = (
|
|
657
676
|
declared: ActionConfirm,
|
|
@@ -664,8 +683,7 @@ export const useTableRowActions = <T extends Data>(
|
|
|
664
683
|
id: ids[0],
|
|
665
684
|
[config.rowIdKey ?? DEFAULT_ROW_ID_KEY]: ids[0],
|
|
666
685
|
},
|
|
667
|
-
|
|
668
|
-
row: { count: ids.length },
|
|
686
|
+
row: confirmRowFields(ids),
|
|
669
687
|
fallback,
|
|
670
688
|
run,
|
|
671
689
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The counter a text field shows under `showCount`: the characters typed,
|
|
3
|
+
* out of `maxLength` when the field has one ("4 / 60").
|
|
4
|
+
*/
|
|
5
|
+
export function characterCountText(value: unknown, maxLength?: number): string {
|
|
6
|
+
const count = typeof value === "string" ? [...value].length : 0;
|
|
7
|
+
return maxLength === undefined ? String(count) : `${count} / ${maxLength}`;
|
|
8
|
+
}
|
|
@@ -213,7 +213,10 @@ const wrapperProps = computed(() =>
|
|
|
213
213
|
v-if="props.groupByDay"
|
|
214
214
|
class="flex items-center gap-2.5 px-[18px] pt-3.5 pb-1.5 after:h-px after:flex-1 after:bg-(--ui-border-muted)"
|
|
215
215
|
>
|
|
216
|
-
<USkeleton
|
|
216
|
+
<USkeleton
|
|
217
|
+
:aria-label="t('dms.a11y.loading')"
|
|
218
|
+
class="my-[1.5px] h-2.5 w-24"
|
|
219
|
+
/>
|
|
217
220
|
</div>
|
|
218
221
|
<div class="divide-y divide-(--ui-border-muted)">
|
|
219
222
|
<DmsRowSkeleton
|
|
@@ -67,6 +67,8 @@ const showSkeleton = computed(() => isLoading.value && !data.value);
|
|
|
67
67
|
const hasError = computed(
|
|
68
68
|
() => !isLoading.value && !data.value && Boolean(error.value),
|
|
69
69
|
);
|
|
70
|
+
|
|
71
|
+
const { t } = useI18n();
|
|
70
72
|
</script>
|
|
71
73
|
|
|
72
74
|
<template>
|
|
@@ -77,6 +79,7 @@ const hasError = computed(
|
|
|
77
79
|
aria-busy="true"
|
|
78
80
|
>
|
|
79
81
|
<USkeleton
|
|
82
|
+
:aria-label="t('dms.a11y.loading')"
|
|
80
83
|
v-for="width in SKELETON_LINE_WIDTHS"
|
|
81
84
|
:key="width"
|
|
82
85
|
class="h-3"
|
|
@@ -146,6 +146,8 @@ const hasStates = computed(() => cards.value.some((card) => !!card.state));
|
|
|
146
146
|
const isLeftOut = computed(
|
|
147
147
|
() => !!props.categoryId && !props.empty && cards.value.length === 0,
|
|
148
148
|
);
|
|
149
|
+
|
|
150
|
+
const { t } = useI18n();
|
|
149
151
|
</script>
|
|
150
152
|
|
|
151
153
|
<template>
|
|
@@ -180,14 +182,20 @@ const isLeftOut = computed(
|
|
|
180
182
|
class="dms-card flex flex-col gap-2.5 p-[18px]"
|
|
181
183
|
>
|
|
182
184
|
<div class="flex items-center gap-3">
|
|
183
|
-
<USkeleton
|
|
184
|
-
|
|
185
|
+
<USkeleton
|
|
186
|
+
:aria-label="t('dms.a11y.loading')"
|
|
187
|
+
class="size-9 rounded-[10px]"
|
|
188
|
+
/>
|
|
189
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3.5 w-28" />
|
|
185
190
|
</div>
|
|
186
191
|
<div class="grid h-[39px] content-center gap-[7.5px]">
|
|
187
|
-
<USkeleton class="h-3 w-4/5" />
|
|
188
|
-
<USkeleton class="h-3 w-3/5" />
|
|
192
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-4/5" />
|
|
193
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-3/5" />
|
|
189
194
|
</div>
|
|
190
|
-
<USkeleton
|
|
195
|
+
<USkeleton
|
|
196
|
+
:aria-label="t('dms.a11y.loading')"
|
|
197
|
+
class="my-[2.5px] h-3 w-28"
|
|
198
|
+
/>
|
|
191
199
|
</div>
|
|
192
200
|
</template>
|
|
193
201
|
<template v-else>
|
|
@@ -61,7 +61,10 @@ const attrs = useAttrs();
|
|
|
61
61
|
const theme = tv({
|
|
62
62
|
slots: {
|
|
63
63
|
root: "dms-card dms-card--interactive group flex flex-col gap-3 p-[18px] text-start",
|
|
64
|
-
|
|
64
|
+
// Two lines before the ellipsis: a title cut on its first line no longer
|
|
65
|
+
// says where the card leads, and the card already grows with its text.
|
|
66
|
+
title:
|
|
67
|
+
"text-highlighted line-clamp-2 min-w-0 text-sm font-[650] [overflow-wrap:anywhere]",
|
|
65
68
|
tag: `${MONO_TAG_CLASS} bg-elevated shrink-0 text-dimmed`,
|
|
66
69
|
badge: "shrink-0",
|
|
67
70
|
arrow:
|
|
@@ -78,6 +81,8 @@ const passthrough = computed(() => {
|
|
|
78
81
|
const { class: _class, ...rest } = attrs;
|
|
79
82
|
return rest;
|
|
80
83
|
});
|
|
84
|
+
|
|
85
|
+
const { t } = useI18n();
|
|
81
86
|
</script>
|
|
82
87
|
|
|
83
88
|
<template>
|
|
@@ -110,6 +115,7 @@ const passthrough = computed(() => {
|
|
|
110
115
|
</div>
|
|
111
116
|
|
|
112
117
|
<USkeleton
|
|
118
|
+
:aria-label="t('dms.a11y.loading')"
|
|
113
119
|
v-if="props.statePending && !props.state"
|
|
114
120
|
aria-hidden="true"
|
|
115
121
|
class="mt-auto h-[18px] w-28 self-start"
|
|
@@ -178,7 +178,11 @@ const hasMeta = computed(
|
|
|
178
178
|
<div
|
|
179
179
|
class="text-highlighted mt-2 text-[30px] leading-[1.1] font-[650] tracking-[-0.035em] tabular-nums"
|
|
180
180
|
>
|
|
181
|
-
<USkeleton
|
|
181
|
+
<USkeleton
|
|
182
|
+
:aria-label="t('dms.a11y.loading')"
|
|
183
|
+
v-if="isFirstLoad"
|
|
184
|
+
class="h-[33px] w-36"
|
|
185
|
+
/>
|
|
182
186
|
<span
|
|
183
187
|
v-else
|
|
184
188
|
class="transition-opacity"
|
|
@@ -193,8 +197,11 @@ const hasMeta = computed(
|
|
|
193
197
|
class="mt-1.5 flex h-5 items-center gap-2"
|
|
194
198
|
aria-hidden="true"
|
|
195
199
|
>
|
|
196
|
-
<USkeleton
|
|
197
|
-
|
|
200
|
+
<USkeleton
|
|
201
|
+
:aria-label="t('dms.a11y.loading')"
|
|
202
|
+
class="h-4 w-14 rounded-full"
|
|
203
|
+
/>
|
|
204
|
+
<USkeleton :aria-label="t('dms.a11y.loading')" class="h-3 w-20" />
|
|
198
205
|
</div>
|
|
199
206
|
<div
|
|
200
207
|
v-else-if="hasMeta"
|
|
@@ -3,6 +3,7 @@ import { nextTick, useTemplateRef, watch, type FunctionalComponent } from "vue";
|
|
|
3
3
|
import { DialogDescription, DialogTitle } from "reka-ui";
|
|
4
4
|
import DmsIconWell from "../icon-well/IconWell.vue";
|
|
5
5
|
import DmsModalEscHint from "../../build/components/modal/ModalEscHint.vue";
|
|
6
|
+
import DmsFormRequiredLegend from "../../build/components/form/FormRequiredLegend.vue";
|
|
6
7
|
import {
|
|
7
8
|
ConfirmActionError,
|
|
8
9
|
type ConfirmBodyRender,
|
|
@@ -174,6 +175,12 @@ const headerIcon = computed(() =>
|
|
|
174
175
|
: (props.icon ?? DEFAULT_ICONS[props.color] ?? FALLBACK_ICON),
|
|
175
176
|
);
|
|
176
177
|
|
|
178
|
+
// A field marked required says what its mark means, as a form does under
|
|
179
|
+
// its fields.
|
|
180
|
+
const hasRequiredFields = computed(() =>
|
|
181
|
+
props.fields.some((field) => field.required),
|
|
182
|
+
);
|
|
183
|
+
|
|
177
184
|
const hasBody = computed(
|
|
178
185
|
() =>
|
|
179
186
|
props.impact.length > 0 ||
|
|
@@ -398,6 +405,7 @@ function handleCancel() {
|
|
|
398
405
|
</template>
|
|
399
406
|
</template>
|
|
400
407
|
</UFormField>
|
|
408
|
+
<DmsFormRequiredLegend v-if="hasRequiredFields" />
|
|
401
409
|
|
|
402
410
|
<component
|
|
403
411
|
:is="bodyComponent"
|
|
@@ -954,8 +954,9 @@ onUnmounted(async () => {
|
|
|
954
954
|
</div>
|
|
955
955
|
|
|
956
956
|
<!-- One bar for every form (see `saveBarState`): the v2 floating bar
|
|
957
|
-
on a page, the footer band of a card, a drawer or a modal.
|
|
958
|
-
|
|
957
|
+
on a page, the footer band of a card, a drawer or a modal. The
|
|
958
|
+
floating bar overlays the form and takes no space while hidden; the
|
|
959
|
+
band holds its place. Neither moves anything when it shows. -->
|
|
959
960
|
<DmsSaveBar
|
|
960
961
|
v-if="canSave && !isInstant && !loadFailure"
|
|
961
962
|
:variant="usesSaveBar ? 'floating' : 'band'"
|
|
@@ -967,7 +968,7 @@ onUnmounted(async () => {
|
|
|
967
968
|
:save-label="usesSaveBar ? props.submitLabel : submitButtonLabel"
|
|
968
969
|
:cancellable="canCancel"
|
|
969
970
|
:resettable="hasChangeableFields"
|
|
970
|
-
:class="usesSaveBar ? 'mx-3
|
|
971
|
+
:class="usesSaveBar ? 'mx-3' : surfaceClasses.foot"
|
|
971
972
|
@discard="discardChanges"
|
|
972
973
|
@cancel="cancelForm"
|
|
973
974
|
/>
|
|
@@ -402,7 +402,12 @@ function nodeClasses(node: CascaderNode, isHighlighted: boolean): string[] {
|
|
|
402
402
|
class="w-52 space-y-2 p-2"
|
|
403
403
|
aria-busy="true"
|
|
404
404
|
>
|
|
405
|
-
<USkeleton
|
|
405
|
+
<USkeleton
|
|
406
|
+
:aria-label="t('dms.a11y.loading')"
|
|
407
|
+
v-for="i in 5"
|
|
408
|
+
:key="i"
|
|
409
|
+
class="h-6 w-full"
|
|
410
|
+
/>
|
|
406
411
|
</div>
|
|
407
412
|
|
|
408
413
|
<div
|
|
@@ -40,7 +40,11 @@ const isVNodeValue = computed(() => isVNode(formattedValue.value));
|
|
|
40
40
|
</script>
|
|
41
41
|
|
|
42
42
|
<template>
|
|
43
|
-
<USkeleton
|
|
43
|
+
<USkeleton
|
|
44
|
+
:aria-label="t('dms.a11y.loading')"
|
|
45
|
+
v-if="loading"
|
|
46
|
+
class="h-9 w-full"
|
|
47
|
+
/>
|
|
44
48
|
|
|
45
49
|
<Component
|
|
46
50
|
:is="dedicatedComponent"
|
|
@@ -4,16 +4,27 @@ import type {
|
|
|
4
4
|
InputNumberEmits,
|
|
5
5
|
InputNumberSlots,
|
|
6
6
|
} from "@nuxt/ui/components/InputNumber.vue";
|
|
7
|
+
import { computed } from "vue";
|
|
7
8
|
import { useForwardPropsEmits } from "reka-ui";
|
|
8
9
|
import { useControlError } from "../../../build/composables/form/useControlError";
|
|
9
10
|
|
|
10
11
|
defineOptions({ inheritAttrs: false });
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
interface InputNumberFieldProps extends InputNumberProps {
|
|
14
|
+
/** Unit after the field ("days"): an i18n key with `$`, or a literal. */
|
|
15
|
+
suffix?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<InputNumberFieldProps>();
|
|
13
19
|
const emits = defineEmits<InputNumberEmits>();
|
|
14
20
|
defineSlots<InputNumberSlots>();
|
|
15
21
|
|
|
16
22
|
const forwarded = useForwardPropsEmits(props, emits);
|
|
23
|
+
const inputProps = computed(() => {
|
|
24
|
+
const { suffix: _suffix, ...rest } = forwarded.value;
|
|
25
|
+
return rest;
|
|
26
|
+
});
|
|
27
|
+
const { processI18n } = useTranslation();
|
|
17
28
|
|
|
18
29
|
// v2 number fields keep the value left-aligned with a compact caret stepper.
|
|
19
30
|
const DEFAULT_ORIENTATION = "vertical";
|
|
@@ -33,12 +44,25 @@ function onIncomingText(event: InputEvent): void {
|
|
|
33
44
|
</script>
|
|
34
45
|
|
|
35
46
|
<template>
|
|
36
|
-
|
|
47
|
+
<!-- The unit sits after the field rather than inside it, where the
|
|
48
|
+
stepper already takes the end of the box. -->
|
|
49
|
+
<div
|
|
50
|
+
:class="props.suffix ? 'flex items-center gap-2' : 'contents'"
|
|
51
|
+
@beforeinput.capture="onIncomingText"
|
|
52
|
+
>
|
|
37
53
|
<UInputNumber
|
|
38
|
-
v-bind="{ ...
|
|
54
|
+
v-bind="{ ...inputProps, ...$attrs }"
|
|
55
|
+
:class="props.suffix && 'min-w-0 flex-1'"
|
|
39
56
|
:orientation="props.orientation ?? DEFAULT_ORIENTATION"
|
|
40
57
|
:increment="{ color: 'neutral' }"
|
|
41
58
|
:decrement="{ color: 'neutral' }"
|
|
42
59
|
/>
|
|
60
|
+
<span
|
|
61
|
+
v-if="props.suffix"
|
|
62
|
+
class="text-muted shrink-0 text-[13px]"
|
|
63
|
+
data-field-suffix
|
|
64
|
+
>
|
|
65
|
+
{{ processI18n(props.suffix) }}
|
|
66
|
+
</span>
|
|
43
67
|
</div>
|
|
44
68
|
</template>
|
|
@@ -4,15 +4,37 @@ import type {
|
|
|
4
4
|
InputEmits,
|
|
5
5
|
InputSlots,
|
|
6
6
|
} from "@nuxt/ui/components/Input.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 InputTextProps extends InputProps {
|
|
12
|
+
/** Longest value accepted; the counter reads against it. */
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
/** Shows the characters typed at the end of the field ("4 / 60"). */
|
|
15
|
+
showCount?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<InputTextProps>();
|
|
10
19
|
const emits = defineEmits<InputEmits>();
|
|
11
20
|
defineSlots<InputSlots>();
|
|
12
21
|
|
|
13
22
|
const forwarded = useForwardPropsEmits(props, emits);
|
|
23
|
+
const inputProps = 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
|
-
<UInput v-bind="
|
|
33
|
+
<UInput v-bind="inputProps">
|
|
34
|
+
<template v-if="props.showCount" #trailing>
|
|
35
|
+
<span class="text-dimmed text-xs tabular-nums" data-character-count>
|
|
36
|
+
{{ countText }}
|
|
37
|
+
</span>
|
|
38
|
+
</template>
|
|
39
|
+
</UInput>
|
|
18
40
|
</template>
|