@revenexx/studio-shared 0.3.2 → 0.3.3
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/module.d.mts +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +9 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +57 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +57 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +14 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +14 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +59 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +59 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +42 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +42 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +27 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +6 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +6 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +8 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +8 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +23 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +23 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +22 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +22 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +36 -3
- package/dist/runtime/components/DataTable/DataTable.vue +252 -27
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +36 -3
- package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +16 -0
- package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
- package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +16 -0
- package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +20 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +20 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +5 -1
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +5 -1
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +5 -1
- package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +5 -1
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +2 -2
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +2 -2
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +2 -2
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +2 -2
- package/dist/runtime/components/Shared/SortHeader.vue +6 -2
- package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -7
- package/dist/runtime/components/Shared/TabBar.vue +3 -2
- package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -7
- package/dist/runtime/composables/useDashboards.d.ts +27 -0
- package/dist/runtime/composables/useDashboards.js +143 -0
- package/dist/runtime/composables/useSavedViews.d.ts +44 -0
- package/dist/runtime/composables/useSavedViews.js +44 -0
- package/dist/runtime/composables/useStickyListView.d.ts +60 -0
- package/dist/runtime/composables/useStickyListView.js +32 -0
- package/dist/runtime/lib/canonicalJson.d.ts +43 -0
- package/dist/runtime/lib/canonicalJson.js +38 -0
- package/dist/runtime/lib/dashboardIcons.d.ts +4 -0
- package/dist/runtime/lib/dashboardIcons.js +50 -0
- package/dist/runtime/lib/facets.d.ts +200 -0
- package/dist/runtime/lib/facets.js +171 -0
- package/dist/runtime/lib/gridTracks.d.ts +55 -0
- package/dist/runtime/lib/gridTracks.js +29 -0
- package/dist/runtime/lib/widgetRegistry.d.ts +124 -0
- package/dist/runtime/lib/widgetRegistry.js +66 -0
- package/dist/runtime/types/dashboard.d.ts +95 -0
- package/dist/runtime/types/dashboard.js +2 -0
- package/dist/types.d.mts +8 -0
- package/package.json +3 -2
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
2
|
+
import { CheckCircle, RoundSortVertical, SortFromBottomToTop, SortFromTopToBottom } from "#solar-icons";
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
5
5
|
DropdownMenu,
|
|
6
6
|
DropdownMenuContent,
|
|
7
7
|
DropdownMenuItem,
|
|
8
|
+
DropdownMenuLabel,
|
|
8
9
|
DropdownMenuTrigger
|
|
9
10
|
} from "@revenexx/studio";
|
|
10
11
|
const props = defineProps({
|
|
11
|
-
columns: { type: Array, required: true }
|
|
12
|
+
columns: { type: Array, required: true },
|
|
13
|
+
size: { type: String, required: false, default: "sm" }
|
|
12
14
|
});
|
|
13
15
|
const sort = defineModel({ type: Object, ...{ required: true } });
|
|
14
16
|
const t = useT();
|
|
17
|
+
const sorted = computed(() => Boolean(sort.value.column && sort.value.direction));
|
|
18
|
+
const icon = computed(() => {
|
|
19
|
+
if (!sorted.value) return RoundSortVertical;
|
|
20
|
+
return sort.value.direction === "asc" ? SortFromBottomToTop : SortFromTopToBottom;
|
|
21
|
+
});
|
|
15
22
|
const label = computed(() => {
|
|
16
23
|
const col = props.columns.find((c) => c.key === sort.value.column);
|
|
17
|
-
if (!col || !
|
|
24
|
+
if (!col || !sorted.value) return t("shared.datatable.sortmenu.label.default");
|
|
18
25
|
return `${col.label} ${sort.value.direction === "asc" ? "\u2191" : "\u2193"}`;
|
|
19
26
|
});
|
|
27
|
+
function isActive(key, direction) {
|
|
28
|
+
return sort.value.column === key && sort.value.direction === direction;
|
|
29
|
+
}
|
|
20
30
|
function pick(key, direction) {
|
|
21
31
|
sort.value = { column: key, direction };
|
|
22
32
|
}
|
|
@@ -25,21 +35,39 @@ function pick(key, direction) {
|
|
|
25
35
|
<template>
|
|
26
36
|
<DropdownMenu>
|
|
27
37
|
<DropdownMenuTrigger as-child>
|
|
28
|
-
<Button
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
<Button
|
|
39
|
+
:variant="sorted ? 'default' : 'outline'"
|
|
40
|
+
:size="size === 'sm' ? 'icon-sm' : 'icon'"
|
|
41
|
+
:data-active="sorted"
|
|
42
|
+
:aria-label="t('shared.datatable.sortmenu.aria', { sort: label })"
|
|
43
|
+
:title="t('shared.datatable.sortmenu.aria', { sort: label })"
|
|
44
|
+
>
|
|
45
|
+
<!-- Violet while idle like the filter trigger and the column picker
|
|
46
|
+
beside it; on the filled face the icon takes the button's own
|
|
47
|
+
foreground, or it would be violet on violet. -->
|
|
48
|
+
<span class="flex shrink-0 items-center" :class="sorted ? '' : 'text-primary'">
|
|
49
|
+
<component :is="icon" weight="Linear" :class="size === 'sm' ? 'size-3.5' : 'size-4'" />
|
|
50
|
+
</span>
|
|
32
51
|
</Button>
|
|
33
52
|
</DropdownMenuTrigger>
|
|
34
|
-
<DropdownMenuContent align="start" class="w-
|
|
53
|
+
<DropdownMenuContent align="start" class="w-56">
|
|
54
|
+
<DropdownMenuLabel>{{ t("shared.datatable.sortmenu.heading") }}</DropdownMenuLabel>
|
|
35
55
|
<template v-for="col in columns" :key="col.key">
|
|
36
56
|
<DropdownMenuItem class="gap-2" @select="pick(col.key, 'asc')">
|
|
37
|
-
<
|
|
38
|
-
<span
|
|
57
|
+
<SortFromBottomToTop weight="Linear" :size="13" class="shrink-0 text-muted-foreground" />
|
|
58
|
+
<span
|
|
59
|
+
class="min-w-0 flex-1 truncate"
|
|
60
|
+
:class="isActive(col.key, 'asc') ? 'font-medium text-primary' : ''"
|
|
61
|
+
>{{ col.label }}</span>
|
|
62
|
+
<CheckCircle v-if="isActive(col.key, 'asc')" :size="14" weight="Bold" class="shrink-0 text-primary" />
|
|
39
63
|
</DropdownMenuItem>
|
|
40
64
|
<DropdownMenuItem class="gap-2" @select="pick(col.key, 'desc')">
|
|
41
|
-
<
|
|
42
|
-
<span
|
|
65
|
+
<SortFromTopToBottom weight="Linear" :size="13" class="shrink-0 text-muted-foreground" />
|
|
66
|
+
<span
|
|
67
|
+
class="min-w-0 flex-1 truncate"
|
|
68
|
+
:class="isActive(col.key, 'desc') ? 'font-medium text-primary' : ''"
|
|
69
|
+
>{{ col.label }}</span>
|
|
70
|
+
<CheckCircle v-if="isActive(col.key, 'desc')" :size="14" weight="Bold" class="shrink-0 text-primary" />
|
|
43
71
|
</DropdownMenuItem>
|
|
44
72
|
</template>
|
|
45
73
|
</DropdownMenuContent>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { DataTableColumn, SortState } from './types.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
columns: DataTableColumn[];
|
|
4
|
+
/** Matches the row it sits in; `sm` is the 32px square its neighbours are. */
|
|
5
|
+
size?: 'sm' | 'default';
|
|
4
6
|
};
|
|
5
7
|
type __VLS_PublicProps = __VLS_Props & {
|
|
6
8
|
modelValue: SortState;
|
|
@@ -9,5 +11,7 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {},
|
|
|
9
11
|
"update:modelValue": (value: SortState) => any;
|
|
10
12
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
11
13
|
"onUpdate:modelValue"?: ((value: SortState) => any) | undefined;
|
|
12
|
-
}>, {
|
|
14
|
+
}>, {
|
|
15
|
+
size: "sm" | "default";
|
|
16
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
17
|
export default _default;
|
|
@@ -6,11 +6,11 @@ type __VLS_Props = {
|
|
|
6
6
|
kinds?: string[];
|
|
7
7
|
};
|
|
8
8
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
-
select: (assets: StorageAsset[]) => any;
|
|
10
9
|
"update:open": (value: boolean) => any;
|
|
10
|
+
select: (assets: StorageAsset[]) => any;
|
|
11
11
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
|
-
onSelect?: ((assets: StorageAsset[]) => any) | undefined;
|
|
13
12
|
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
13
|
+
onSelect?: ((assets: StorageAsset[]) => any) | undefined;
|
|
14
14
|
}>, {
|
|
15
15
|
multiple: boolean;
|
|
16
16
|
kinds: string[];
|
|
@@ -6,11 +6,11 @@ type __VLS_Props = {
|
|
|
6
6
|
kinds?: string[];
|
|
7
7
|
};
|
|
8
8
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
-
select: (assets: StorageAsset[]) => any;
|
|
10
9
|
"update:open": (value: boolean) => any;
|
|
10
|
+
select: (assets: StorageAsset[]) => any;
|
|
11
11
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
|
-
onSelect?: ((assets: StorageAsset[]) => any) | undefined;
|
|
13
12
|
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
13
|
+
onSelect?: ((assets: StorageAsset[]) => any) | undefined;
|
|
14
14
|
}>, {
|
|
15
15
|
multiple: boolean;
|
|
16
16
|
kinds: string[];
|
|
@@ -14,16 +14,16 @@ type __VLS_Props = {
|
|
|
14
14
|
folderName: string | null;
|
|
15
15
|
};
|
|
16
16
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
"update:open": (value: boolean) => any;
|
|
17
18
|
done: (summary: {
|
|
18
19
|
created: number;
|
|
19
20
|
failed: number;
|
|
20
21
|
}) => any;
|
|
21
|
-
"update:open": (value: boolean) => any;
|
|
22
22
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
23
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
23
24
|
onDone?: ((summary: {
|
|
24
25
|
created: number;
|
|
25
26
|
failed: number;
|
|
26
27
|
}) => any) | undefined;
|
|
27
|
-
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
28
28
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
29
|
export default _default;
|
|
@@ -14,16 +14,16 @@ type __VLS_Props = {
|
|
|
14
14
|
folderName: string | null;
|
|
15
15
|
};
|
|
16
16
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
"update:open": (value: boolean) => any;
|
|
17
18
|
done: (summary: {
|
|
18
19
|
created: number;
|
|
19
20
|
failed: number;
|
|
20
21
|
}) => any;
|
|
21
|
-
"update:open": (value: boolean) => any;
|
|
22
22
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
23
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
23
24
|
onDone?: ((summary: {
|
|
24
25
|
created: number;
|
|
25
26
|
failed: number;
|
|
26
27
|
}) => any) | undefined;
|
|
27
|
-
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
28
28
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
29
|
export default _default;
|
|
@@ -18,11 +18,15 @@ const sortIcon = computed(() => {
|
|
|
18
18
|
<template>
|
|
19
19
|
<button
|
|
20
20
|
type="button"
|
|
21
|
-
class="group inline-flex cursor-pointer items-center gap-1.5 -m-1 p-1 rounded-md text-left transition-colors hover:text-primary"
|
|
21
|
+
class="group inline-flex min-w-0 max-w-full cursor-pointer items-center gap-1.5 -m-1 p-1 rounded-md text-left transition-colors hover:text-primary"
|
|
22
22
|
:class="alignRight ? 'flex-row-reverse w-full justify-end' : ''"
|
|
23
23
|
@click="emit('toggle', column)"
|
|
24
24
|
>
|
|
25
|
-
|
|
25
|
+
<!-- The label truncates. Without this the button is as wide as its words
|
|
26
|
+
and a heading longer than its column simply prints over the next one —
|
|
27
|
+
"SortierungGeändert" on a category list, with no hint that two columns
|
|
28
|
+
are involved. The cell carries the full text as its `title`. -->
|
|
29
|
+
<span class="min-w-0 truncate"><slot /></span>
|
|
26
30
|
<component
|
|
27
31
|
:is="sortIcon"
|
|
28
32
|
weight="Linear"
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Underline tab bar — the standard cockpit
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Underline tab bar — the standard cockpit tabs. A full-width hairline sits
|
|
3
|
+
* behind the row; the active/hover indicator is a 2px bar spanning exactly the
|
|
4
|
+
* label width (inset-x-2.5 = the button's px-2.5), sitting on the baseline over
|
|
5
|
+
* the hairline. Active = primary; hover = soft primary. Tabs scroll
|
|
6
|
+
* horizontally on overflow. To bleed the line to the panel edges, pass e.g.
|
|
7
|
+
* `class="-mx-4 px-4"` from a padded page.
|
|
8
|
+
*
|
|
9
|
+
* ## `sticky`
|
|
10
|
+
*
|
|
11
|
+
* On by default, because the original job was a detail page: the row glues
|
|
12
|
+
* itself to the bottom of the sticky `PageHeader` (hence the magic `57px`) and
|
|
13
|
+
* frosts its background so content scrolls through.
|
|
14
|
+
*
|
|
15
|
+
* Off for a row INSIDE a panel — the queue tabs between a DataTable's toolbar
|
|
16
|
+
* and its header, say. There the offset is wrong (there is no page header above
|
|
17
|
+
* it) and the frosted layer is worse than nothing, because the panel already
|
|
18
|
+
* has a background of its own and the blur only muddies the rows scrolling past
|
|
19
|
+
* underneath. Same tabs, same indicator, same hairline; just not pinned.
|
|
8
20
|
*/
|
|
9
21
|
export interface TabItem {
|
|
10
22
|
key: string;
|
|
@@ -14,10 +26,14 @@ export interface TabItem {
|
|
|
14
26
|
type __VLS_Props = {
|
|
15
27
|
tabs: TabItem[];
|
|
16
28
|
modelValue: string;
|
|
29
|
+
/** Pin to the page header. Off for a bar inside a panel. */
|
|
30
|
+
sticky?: boolean;
|
|
17
31
|
};
|
|
18
32
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
33
|
"update:modelValue": (key: string) => any;
|
|
20
34
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
21
35
|
"onUpdate:modelValue"?: ((key: string) => any) | undefined;
|
|
22
|
-
}>, {
|
|
36
|
+
}>, {
|
|
37
|
+
sticky: boolean;
|
|
38
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
39
|
export default _default;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineProps({
|
|
3
3
|
tabs: { type: Array, required: true },
|
|
4
|
-
modelValue: { type: String, required: true }
|
|
4
|
+
modelValue: { type: String, required: true },
|
|
5
|
+
sticky: { type: Boolean, required: false, default: true }
|
|
5
6
|
});
|
|
6
7
|
defineEmits(["update:modelValue"]);
|
|
7
8
|
</script>
|
|
@@ -11,7 +12,7 @@ defineEmits(["update:modelValue"]);
|
|
|
11
12
|
pt-4 pb-3 + text-xl line + 1px border) so header and tabs stay glued as
|
|
12
13
|
one unit while the panel scrolls. Glassy frosted bg (blur + low opacity)
|
|
13
14
|
lets content scroll through. -->
|
|
14
|
-
<div class="sticky top-[57px] z-10 bg-white/40 backdrop-blur-xl">
|
|
15
|
+
<div :class="sticky ? 'sticky top-[57px] z-10 bg-white/40 backdrop-blur-xl' : 'relative'">
|
|
15
16
|
<!-- Pull the row out by the tab padding so the first/last label sits flush
|
|
16
17
|
with the surrounding content (headline) edges. -->
|
|
17
18
|
<div class="relative z-10 -mx-2.5 flex gap-1 overflow-x-auto">
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Underline tab bar — the standard cockpit
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Underline tab bar — the standard cockpit tabs. A full-width hairline sits
|
|
3
|
+
* behind the row; the active/hover indicator is a 2px bar spanning exactly the
|
|
4
|
+
* label width (inset-x-2.5 = the button's px-2.5), sitting on the baseline over
|
|
5
|
+
* the hairline. Active = primary; hover = soft primary. Tabs scroll
|
|
6
|
+
* horizontally on overflow. To bleed the line to the panel edges, pass e.g.
|
|
7
|
+
* `class="-mx-4 px-4"` from a padded page.
|
|
8
|
+
*
|
|
9
|
+
* ## `sticky`
|
|
10
|
+
*
|
|
11
|
+
* On by default, because the original job was a detail page: the row glues
|
|
12
|
+
* itself to the bottom of the sticky `PageHeader` (hence the magic `57px`) and
|
|
13
|
+
* frosts its background so content scrolls through.
|
|
14
|
+
*
|
|
15
|
+
* Off for a row INSIDE a panel — the queue tabs between a DataTable's toolbar
|
|
16
|
+
* and its header, say. There the offset is wrong (there is no page header above
|
|
17
|
+
* it) and the frosted layer is worse than nothing, because the panel already
|
|
18
|
+
* has a background of its own and the blur only muddies the rows scrolling past
|
|
19
|
+
* underneath. Same tabs, same indicator, same hairline; just not pinned.
|
|
8
20
|
*/
|
|
9
21
|
export interface TabItem {
|
|
10
22
|
key: string;
|
|
@@ -14,10 +26,14 @@ export interface TabItem {
|
|
|
14
26
|
type __VLS_Props = {
|
|
15
27
|
tabs: TabItem[];
|
|
16
28
|
modelValue: string;
|
|
29
|
+
/** Pin to the page header. Off for a bar inside a panel. */
|
|
30
|
+
sticky?: boolean;
|
|
17
31
|
};
|
|
18
32
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
33
|
"update:modelValue": (key: string) => any;
|
|
20
34
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
21
35
|
"onUpdate:modelValue"?: ((key: string) => any) | undefined;
|
|
22
|
-
}>, {
|
|
36
|
+
}>, {
|
|
37
|
+
sticky: boolean;
|
|
38
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
39
|
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue';
|
|
2
|
+
import type { Dashboard, DashboardScope, DashboardWritePayload } from '../types/dashboard.js';
|
|
3
|
+
export declare class DashboardApiError extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
constructor(status: number, message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface UseDashboardsOptions {
|
|
8
|
+
/** Where the dashboard routes live. The cockpit serves them from Nitro. */
|
|
9
|
+
base?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function useDashboards(scope: DashboardScope, options?: UseDashboardsOptions): {
|
|
12
|
+
scope: DashboardScope;
|
|
13
|
+
dashboards: ComputedRef<Dashboard[]>;
|
|
14
|
+
defaultId: ComputedRef<string | null>;
|
|
15
|
+
loading: ComputedRef<boolean>;
|
|
16
|
+
saving: ComputedRef<boolean>;
|
|
17
|
+
error: ComputedRef<string | null>;
|
|
18
|
+
load: (force?: boolean) => Promise<Dashboard[]>;
|
|
19
|
+
resolve: (slug: string | null) => Dashboard | null;
|
|
20
|
+
create: (payload: DashboardWritePayload & {
|
|
21
|
+
name: string;
|
|
22
|
+
}) => Promise<Dashboard>;
|
|
23
|
+
save: (id: string, payload: DashboardWritePayload) => Promise<Dashboard>;
|
|
24
|
+
remove: (id: string) => Promise<void>;
|
|
25
|
+
duplicate: (id: string) => Promise<Dashboard>;
|
|
26
|
+
setDefault: (id: string | null) => Promise<void>;
|
|
27
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
export class DashboardApiError extends Error {
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "DashboardApiError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function useDashboards(scope, options = {}) {
|
|
10
|
+
const base = options.base ?? "/api/cockpit";
|
|
11
|
+
const auth = usePlatformAuth();
|
|
12
|
+
const tenant = usePlatformTenant();
|
|
13
|
+
const key = (name) => `studio.dashboards.${scope}.${name}`;
|
|
14
|
+
const dashboards = useState(key("list"), () => []);
|
|
15
|
+
const defaultId = useState(key("defaultId"), () => null);
|
|
16
|
+
const loaded = useState(key("loaded"), () => false);
|
|
17
|
+
const loading = useState(key("loading"), () => false);
|
|
18
|
+
const saving = useState(key("saving"), () => false);
|
|
19
|
+
const error = useState(key("error"), () => null);
|
|
20
|
+
const inFlight = ref(null);
|
|
21
|
+
async function headers() {
|
|
22
|
+
const token = await auth.getToken();
|
|
23
|
+
if (!token) throw new DashboardApiError(401, "Not signed in.");
|
|
24
|
+
if (!tenant.slug.value) await tenant.load();
|
|
25
|
+
const slug = tenant.slug.value;
|
|
26
|
+
if (!slug) throw new DashboardApiError(400, "No active tenant.");
|
|
27
|
+
return { Authorization: `Bearer ${token}`, "X-Revenexx-Tenant": slug };
|
|
28
|
+
}
|
|
29
|
+
function fail(err) {
|
|
30
|
+
const e = err;
|
|
31
|
+
const status = e.status ?? e.statusCode ?? e.response?.status ?? 0;
|
|
32
|
+
const message = e.data?.statusMessage ?? e.data?.message ?? e.statusMessage ?? e.message ?? "The dashboard service did not answer.";
|
|
33
|
+
throw new DashboardApiError(status, message);
|
|
34
|
+
}
|
|
35
|
+
async function call(path, init = {}) {
|
|
36
|
+
try {
|
|
37
|
+
return await $fetch(`${base}${path}`, {
|
|
38
|
+
method: init.method ?? "GET",
|
|
39
|
+
headers: await headers(),
|
|
40
|
+
query: init.query,
|
|
41
|
+
body: init.body
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err instanceof DashboardApiError) throw err;
|
|
45
|
+
fail(err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function replace(updated) {
|
|
49
|
+
const idx = dashboards.value.findIndex((d) => d.id === updated.id);
|
|
50
|
+
dashboards.value = idx === -1 ? [...dashboards.value, updated] : dashboards.value.map((d) => d.id === updated.id ? updated : d);
|
|
51
|
+
}
|
|
52
|
+
async function run(force) {
|
|
53
|
+
loading.value = true;
|
|
54
|
+
error.value = null;
|
|
55
|
+
try {
|
|
56
|
+
const res = await call("/dashboards", { query: { scope } });
|
|
57
|
+
dashboards.value = res.dashboards;
|
|
58
|
+
defaultId.value = res.defaultDashboardId;
|
|
59
|
+
loaded.value = true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
error.value = err.message;
|
|
62
|
+
if (force) throw err;
|
|
63
|
+
} finally {
|
|
64
|
+
loading.value = false;
|
|
65
|
+
}
|
|
66
|
+
return dashboards.value;
|
|
67
|
+
}
|
|
68
|
+
async function load(force = false) {
|
|
69
|
+
if (inFlight.value) return inFlight.value;
|
|
70
|
+
if (loaded.value && !force) return dashboards.value;
|
|
71
|
+
inFlight.value = run(force);
|
|
72
|
+
try {
|
|
73
|
+
return await inFlight.value;
|
|
74
|
+
} finally {
|
|
75
|
+
inFlight.value = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function create(payload) {
|
|
79
|
+
saving.value = true;
|
|
80
|
+
try {
|
|
81
|
+
const created = await call("/dashboards", { method: "POST", body: { ...payload, scope } });
|
|
82
|
+
replace(created);
|
|
83
|
+
return created;
|
|
84
|
+
} finally {
|
|
85
|
+
saving.value = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function save(id, payload) {
|
|
89
|
+
const current = dashboards.value.find((d) => d.id === id);
|
|
90
|
+
saving.value = true;
|
|
91
|
+
try {
|
|
92
|
+
const updated = await call(`/dashboards/${id}`, {
|
|
93
|
+
method: "PATCH",
|
|
94
|
+
// The version travels with every write: a stale one is a 409 rather
|
|
95
|
+
// than a silent overwrite of the other tab's edit.
|
|
96
|
+
body: { ...payload, version: payload.version ?? current?.version }
|
|
97
|
+
});
|
|
98
|
+
replace(updated);
|
|
99
|
+
return updated;
|
|
100
|
+
} finally {
|
|
101
|
+
saving.value = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function remove(id) {
|
|
105
|
+
await call(`/dashboards/${id}`, { method: "DELETE" });
|
|
106
|
+
dashboards.value = dashboards.value.filter((d) => d.id !== id);
|
|
107
|
+
if (defaultId.value === id) defaultId.value = null;
|
|
108
|
+
}
|
|
109
|
+
async function duplicate(id) {
|
|
110
|
+
const copy = await call(`/dashboards/${id}/duplicate`, { method: "POST" });
|
|
111
|
+
replace(copy);
|
|
112
|
+
return copy;
|
|
113
|
+
}
|
|
114
|
+
async function setDefault(id) {
|
|
115
|
+
await call(`/dashboards/${id ?? "none"}/default`, { method: "PUT", body: { scope } });
|
|
116
|
+
defaultId.value = id;
|
|
117
|
+
dashboards.value = dashboards.value.map((d) => ({ ...d, isDefault: d.id === id }));
|
|
118
|
+
}
|
|
119
|
+
function resolve(slug) {
|
|
120
|
+
const list = dashboards.value;
|
|
121
|
+
if (!list.length) return null;
|
|
122
|
+
if (slug) {
|
|
123
|
+
const named = list.find((d) => d.slug === slug);
|
|
124
|
+
if (named) return named;
|
|
125
|
+
}
|
|
126
|
+
return list.find((d) => d.id === defaultId.value) ?? list[0] ?? null;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
scope,
|
|
130
|
+
dashboards: computed(() => dashboards.value),
|
|
131
|
+
defaultId: computed(() => defaultId.value),
|
|
132
|
+
loading: computed(() => loading.value),
|
|
133
|
+
saving: computed(() => saving.value),
|
|
134
|
+
error: computed(() => error.value),
|
|
135
|
+
load,
|
|
136
|
+
resolve,
|
|
137
|
+
create,
|
|
138
|
+
save,
|
|
139
|
+
remove,
|
|
140
|
+
duplicate,
|
|
141
|
+
setDefault
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved-views storage provider — the DEFAULT (PE-608).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a seam and not just an API call
|
|
5
|
+
*
|
|
6
|
+
* `SavedViewsMenu` ships inside `@revenexx/studio-shared`, which is published
|
|
7
|
+
* into hosts that have no cockpit backend: the standalone Commerce Studio dev
|
|
8
|
+
* host, and anything else that installs the module for its DataTable. So the
|
|
9
|
+
* component cannot reach for `/api/cockpit/saved-views` — it would 404 in every
|
|
10
|
+
* host but one, and the control would be permanently broken there.
|
|
11
|
+
*
|
|
12
|
+
* Same shape as `usePlatformAuth`, `usePlatformTenant` and `useMediaLibrary`:
|
|
13
|
+
* this file is the default, registered at `priority: -1`, and the cockpit ships
|
|
14
|
+
* a same-named app-level composable that wins. Here, views are kept in
|
|
15
|
+
* `localStorage`, which is exactly what the menu did before — so a bare host
|
|
16
|
+
* behaves as it always has, and only the cockpit gains the account-backed copy.
|
|
17
|
+
*
|
|
18
|
+
* ## Why `id` exists in a localStorage implementation
|
|
19
|
+
*
|
|
20
|
+
* The server keys a view by a uuid and the menu deletes by it. Making the local
|
|
21
|
+
* store agree means the component has one code path instead of two, and a
|
|
22
|
+
* `name`-keyed delete cannot be quietly wrong about which of two views with the
|
|
23
|
+
* same name it removed. The id is derived from the surface and the name, which
|
|
24
|
+
* is stable across reloads and is all the uniqueness a browser-local store has.
|
|
25
|
+
*/
|
|
26
|
+
/** One named snapshot of how a list was narrowed. */
|
|
27
|
+
export interface StoredSavedView {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
snapshot: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface SavedViewsStore {
|
|
33
|
+
/** Read this surface's views. Never throws — a failure is an empty list. */
|
|
34
|
+
list: (surface: string) => Promise<StoredSavedView[]>;
|
|
35
|
+
/** Save under a name, replacing what that name held. */
|
|
36
|
+
save: (surface: string, name: string, snapshot: unknown) => Promise<StoredSavedView>;
|
|
37
|
+
remove: (surface: string, id: string) => Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Does this store survive a new browser? The menu says so in one line, so
|
|
40
|
+
* "saved to your account" and "saved in this browser only" stay honest.
|
|
41
|
+
*/
|
|
42
|
+
readonly durable: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function useSavedViews(): SavedViewsStore;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function localId(surface, name) {
|
|
2
|
+
return `local:${surface}::${name}`;
|
|
3
|
+
}
|
|
4
|
+
function readAll(surface) {
|
|
5
|
+
try {
|
|
6
|
+
if (typeof window === "undefined") return [];
|
|
7
|
+
const raw = window.localStorage.getItem(surface);
|
|
8
|
+
if (!raw) return [];
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
if (!Array.isArray(parsed)) return [];
|
|
11
|
+
return parsed.filter((v) => !!v && typeof v === "object" && typeof v.name === "string").map((v) => ({ id: localId(surface, v.name), name: v.name, snapshot: v.snapshot }));
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeAll(surface, views) {
|
|
17
|
+
try {
|
|
18
|
+
if (typeof window === "undefined") return;
|
|
19
|
+
window.localStorage.setItem(surface, JSON.stringify(
|
|
20
|
+
views.map((v) => ({ name: v.name, snapshot: v.snapshot }))
|
|
21
|
+
));
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function useSavedViews() {
|
|
26
|
+
return {
|
|
27
|
+
durable: false,
|
|
28
|
+
async list(surface) {
|
|
29
|
+
return readAll(surface);
|
|
30
|
+
},
|
|
31
|
+
async save(surface, name, snapshot) {
|
|
32
|
+
const views = readAll(surface);
|
|
33
|
+
const entry = { id: localId(surface, name), name, snapshot };
|
|
34
|
+
const at = views.findIndex((v) => v.name === name);
|
|
35
|
+
if (at >= 0) views.splice(at, 1, entry);
|
|
36
|
+
else views.push(entry);
|
|
37
|
+
writeAll(surface, views);
|
|
38
|
+
return entry;
|
|
39
|
+
},
|
|
40
|
+
async remove(surface, id) {
|
|
41
|
+
writeAll(surface, readAll(surface).filter((v) => v.id !== id));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The narrowing a list was left in, kept across a page change (PE-608).
|
|
3
|
+
*
|
|
4
|
+
* Leaving a list to open a record — or to check another one — and coming back
|
|
5
|
+
* to an unfiltered table is the small tax every operator pays repeatedly: they
|
|
6
|
+
* re-tick the same facets, re-pick the same sort, and do it again the next
|
|
7
|
+
* time. The state is already a snapshot (it is what a saved view stores), so
|
|
8
|
+
* remembering the last one per surface costs nothing new.
|
|
9
|
+
*
|
|
10
|
+
* ## Why `sessionStorage` and not `localStorage`
|
|
11
|
+
*
|
|
12
|
+
* This is the decision worth arguing, because both are defensible and only one
|
|
13
|
+
* of them is safe by default.
|
|
14
|
+
*
|
|
15
|
+
* A sticky filter is a known way to lose people. An operator opens Orders, sees
|
|
16
|
+
* three rows out of twenty-three thousand, and concludes the data is gone —
|
|
17
|
+
* because the filter that did it was set last Tuesday and nothing on screen
|
|
18
|
+
* looks like an answer to "why is this list short". That is a support ticket,
|
|
19
|
+
* and a scary one.
|
|
20
|
+
*
|
|
21
|
+
* Two things make it safe here rather than merely convenient:
|
|
22
|
+
*
|
|
23
|
+
* 1. **It is visible.** The chip strip under the toolbar names every applied
|
|
24
|
+
* value and the trigger is lit. A restored narrowing announces itself; the
|
|
25
|
+
* version of this feature that is dangerous is the one you cannot see.
|
|
26
|
+
* 2. **It ends with the tab.** `sessionStorage` survives a navigation and a
|
|
27
|
+
* reload — which is what "come back to the list" means — and is gone when
|
|
28
|
+
* the tab closes. Tomorrow morning is a fresh list. `localStorage` would
|
|
29
|
+
* carry Friday's filter into Monday, and `localStorage` is also what the
|
|
30
|
+
* saved views deliberately stopped being: a durable narrowing is a thing
|
|
31
|
+
* you NAME and store on your account, not something a list does to you.
|
|
32
|
+
*
|
|
33
|
+
* So: sticky within a session, explicit across them. Swapping the two is one
|
|
34
|
+
* identifier if that trade ever reads differently.
|
|
35
|
+
*
|
|
36
|
+
* ## What it does not do
|
|
37
|
+
*
|
|
38
|
+
* It does not write the URL. A link to a list is a link to the list, not to how
|
|
39
|
+
* one person last left it — pasting a narrowed URL into a ticket and having a
|
|
40
|
+
* colleague see something else is worse than either behaviour here. The queue
|
|
41
|
+
* pills are the linkable half, and they already carry `?queue=` on purpose.
|
|
42
|
+
*/
|
|
43
|
+
export interface StickyListView<T> {
|
|
44
|
+
/**
|
|
45
|
+
* The snapshot this surface was last left in, or `null`.
|
|
46
|
+
*
|
|
47
|
+
* Read once, deliberately: a surface restores on mount and owns its state
|
|
48
|
+
* from then on. A reactive value here would fight the surface for it.
|
|
49
|
+
*/
|
|
50
|
+
read: () => T | null;
|
|
51
|
+
/** Remember the current snapshot. Call it whenever the surface's state moves. */
|
|
52
|
+
write: (snapshot: T) => void;
|
|
53
|
+
/** Forget this surface's state — what a "reset" should also do. */
|
|
54
|
+
clear: () => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @param key A stable identifier for the surface — the same route pattern the
|
|
58
|
+
* saved views are keyed by, so two lists never share a memory.
|
|
59
|
+
*/
|
|
60
|
+
export declare function useStickyListView<T>(key: string): StickyListView<T>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const VERSION = 1;
|
|
2
|
+
export function useStickyListView(key) {
|
|
3
|
+
const storageKey = `commerce.listState.${key}`;
|
|
4
|
+
return {
|
|
5
|
+
read() {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof window === "undefined") return null;
|
|
8
|
+
const raw = window.sessionStorage.getItem(storageKey);
|
|
9
|
+
if (!raw) return null;
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (!parsed || parsed.v !== VERSION) return null;
|
|
12
|
+
return parsed.snapshot ?? null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
write(snapshot) {
|
|
18
|
+
try {
|
|
19
|
+
if (typeof window === "undefined") return;
|
|
20
|
+
window.sessionStorage.setItem(storageKey, JSON.stringify({ v: VERSION, snapshot }));
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
clear() {
|
|
25
|
+
try {
|
|
26
|
+
if (typeof window === "undefined") return;
|
|
27
|
+
window.sessionStorage.removeItem(storageKey);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|