@revenexx/studio-shared 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -12
- package/dist/module.d.mts +15 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +58 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +12 -0
- package/{components → dist/runtime/components}/DataTable/ColumnsMenu.vue +13 -19
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +12 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +87 -0
- package/dist/runtime/components/DataTable/DataTable.vue +510 -0
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +87 -0
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +21 -0
- package/dist/runtime/components/DataTable/FilterMenu.vue +46 -0
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +21 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +189 -0
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -0
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +13 -0
- package/dist/runtime/components/DataTable/SortMenu.vue +47 -0
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +13 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +20 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue +26 -0
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +20 -0
- package/dist/runtime/components/DataTable/types.d.ts +104 -0
- package/dist/runtime/components/DataTable/types.js +0 -0
- package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +17 -0
- package/dist/runtime/components/Media/MediaFolderTree.vue +65 -0
- package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +17 -0
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Media/MediaPickerDialog.vue +163 -0
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +29 -0
- package/dist/runtime/components/Media/MediaUploader.vue +161 -0
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +29 -0
- package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +30 -0
- package/{components → dist/runtime/components}/Shared/BuildLogsFlyout.vue +37 -54
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +30 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +17 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue +75 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +17 -0
- package/dist/runtime/components/Shared/KpiTile.d.vue.ts +13 -0
- package/{components → dist/runtime/components}/Shared/KpiTile.vue +11 -19
- package/dist/runtime/components/Shared/KpiTile.vue.d.ts +13 -0
- package/dist/runtime/components/Shared/ListSurface.d.vue.ts +38 -0
- package/dist/runtime/components/Shared/ListSurface.vue +27 -0
- package/dist/runtime/components/Shared/ListSurface.vue.d.ts +38 -0
- package/dist/runtime/components/Shared/PageHeader.d.vue.ts +23 -0
- package/{components → dist/runtime/components}/Shared/PageHeader.vue +6 -8
- package/dist/runtime/components/Shared/PageHeader.vue.d.ts +23 -0
- package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +52 -0
- package/dist/runtime/components/Shared/RecordLayout.vue +63 -0
- package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +52 -0
- package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +26 -0
- package/dist/runtime/components/Shared/RecordRailCard.vue +20 -0
- package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +26 -0
- package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +41 -0
- package/dist/runtime/components/Shared/SidebarNav.vue +48 -0
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +41 -0
- package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +15 -0
- package/dist/runtime/components/Shared/SidebarNavLink.vue +49 -0
- package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +15 -0
- package/dist/runtime/components/Shared/SortHeader.d.vue.ts +23 -0
- package/dist/runtime/components/Shared/SortHeader.vue +35 -0
- package/dist/runtime/components/Shared/SortHeader.vue.d.ts +23 -0
- package/dist/runtime/components/Shared/StateCard.d.vue.ts +32 -0
- package/{components → dist/runtime/components}/Shared/StateCard.vue +21 -36
- package/dist/runtime/components/Shared/StateCard.vue.d.ts +32 -0
- package/dist/runtime/components/Shared/StudioCard.d.vue.ts +15 -0
- package/dist/runtime/components/Shared/StudioCard.vue +19 -0
- package/dist/runtime/components/Shared/StudioCard.vue.d.ts +15 -0
- package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -0
- package/{components → dist/runtime/components}/Shared/TabBar.vue +7 -16
- package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -0
- package/dist/runtime/components/Shared/ViewTab.d.vue.ts +7 -0
- package/{components → dist/runtime/components}/Shared/ViewTab.vue +7 -9
- package/dist/runtime/components/Shared/ViewTab.vue.d.ts +7 -0
- package/dist/runtime/components/Shared/Wizard.d.vue.ts +47 -0
- package/dist/runtime/components/Shared/Wizard.vue +66 -0
- package/dist/runtime/components/Shared/Wizard.vue.d.ts +47 -0
- package/dist/runtime/composables/useListRail.d.ts +9 -0
- package/dist/runtime/composables/useListRail.js +19 -0
- package/dist/runtime/composables/useMediaLibrary.d.ts +41 -0
- package/dist/runtime/composables/useMediaLibrary.js +36 -0
- package/dist/runtime/composables/usePlatformAuth.d.ts +35 -0
- package/dist/runtime/composables/usePlatformAuth.js +17 -0
- package/dist/runtime/composables/usePlatformTalkback.d.ts +29 -0
- package/dist/runtime/composables/usePlatformTalkback.js +54 -0
- package/dist/runtime/composables/usePlatformTenant.d.ts +14 -0
- package/dist/runtime/composables/usePlatformTenant.js +18 -0
- package/dist/runtime/composables/useT.d.ts +11 -0
- package/dist/runtime/composables/useT.js +8 -0
- package/dist/runtime/composables/useTalkbackChannels.d.ts +78 -0
- package/dist/runtime/composables/useTalkbackChannels.js +52 -0
- package/dist/runtime/lib/formatBridge.d.ts +50 -0
- package/dist/runtime/lib/formatBridge.js +54 -0
- package/dist/runtime/lib/i18nBridge.d.ts +31 -0
- package/dist/runtime/lib/i18nBridge.js +17 -0
- package/dist/runtime/lib/isoLists.d.ts +61 -0
- package/dist/runtime/lib/isoLists.js +62 -0
- package/dist/runtime/lib/sidebarNav.d.ts +34 -0
- package/dist/runtime/lib/sidebarNav.js +22 -0
- package/dist/runtime/lib/talkback.d.ts +59 -0
- package/dist/runtime/lib/talkback.js +49 -0
- package/dist/runtime/types/nav.d.ts +37 -0
- package/dist/runtime/types/nav.js +0 -0
- package/dist/runtime/types/storage.d.ts +67 -0
- package/dist/runtime/types/storage.js +0 -0
- package/dist/types.d.mts +27 -0
- package/package.json +41 -13
- package/components/DataTable/DataTable.vue +0 -518
- package/components/DataTable/FilterMenu.vue +0 -40
- package/components/DataTable/types.ts +0 -82
- package/components/Shared/GitDeploymentSettings.vue +0 -87
- package/components/Shared/SidebarNav.vue +0 -100
- package/components/Shared/SortHeader.vue +0 -50
- package/components/Shared/StudioCard.vue +0 -24
- package/components/Shared/Wizard.vue +0 -80
- package/composables/usePlatformAuth.ts +0 -35
- package/composables/usePlatformTenant.ts +0 -35
- package/index.ts +0 -16
- package/nuxt.config.ts +0 -23
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { AltArrowDown, Filter, Magnifier as Search, SidebarMinimalistic } from "#solar-icons";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuSub,
|
|
10
|
+
DropdownMenuSubContent,
|
|
11
|
+
DropdownMenuSubTrigger,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
Input
|
|
14
|
+
} from "@revenexx/studio";
|
|
15
|
+
import SortHeader from "../Shared/SortHeader.vue";
|
|
16
|
+
import ColumnsMenu from "./ColumnsMenu.vue";
|
|
17
|
+
import FilterMenu from "./FilterMenu.vue";
|
|
18
|
+
import SavedViewsMenu from "./SavedViewsMenu.vue";
|
|
19
|
+
import SortMenu from "./SortMenu.vue";
|
|
20
|
+
import ViewModeSwitch from "./ViewModeSwitch.vue";
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
data: { type: Array, required: true },
|
|
23
|
+
columns: { type: Array, required: true },
|
|
24
|
+
rowKey: { type: Function, required: true, skipCheck: true },
|
|
25
|
+
search: { type: [Boolean, Object], required: false, default: true },
|
|
26
|
+
columnsMenu: { type: Boolean, required: false, default: true },
|
|
27
|
+
filter: { type: [Boolean, Object], required: false, default: false },
|
|
28
|
+
filterTabs: { type: [Object, Array], required: false },
|
|
29
|
+
savedViews: { type: [Object, null], required: false, default: null },
|
|
30
|
+
viewModes: { type: [Array, null], required: false, default: null },
|
|
31
|
+
rail: { type: [Boolean, Object], required: false, default: false },
|
|
32
|
+
pagination: { type: [Boolean, Object], required: false, default: true },
|
|
33
|
+
defaultSort: { type: Object, required: false },
|
|
34
|
+
serverMode: { type: Boolean, required: false, default: false },
|
|
35
|
+
total: { type: Number, required: false },
|
|
36
|
+
loading: { type: Boolean, required: false, default: false },
|
|
37
|
+
emptyText: { type: String, required: false },
|
|
38
|
+
groupBy: { type: [Function, null], required: false, default: null },
|
|
39
|
+
groupEmptyLabel: { type: String, required: false }
|
|
40
|
+
});
|
|
41
|
+
const emit = defineEmits(["rowClick", "stateChange"]);
|
|
42
|
+
const t = useT();
|
|
43
|
+
const query = ref("");
|
|
44
|
+
const sort = defineModel("sort", { type: Object, ...{
|
|
45
|
+
default: () => ({
|
|
46
|
+
column: null,
|
|
47
|
+
direction: null
|
|
48
|
+
})
|
|
49
|
+
} });
|
|
50
|
+
const page = defineModel("page", { type: Number, ...{ default: 0 } });
|
|
51
|
+
const viewMode = defineModel("viewMode", { type: String, ...{ default: "list" } });
|
|
52
|
+
const railOpen = defineModel("railOpen", { type: Boolean, ...{ default: true } });
|
|
53
|
+
if (props.defaultSort && sort.value.column == null) {
|
|
54
|
+
sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction };
|
|
55
|
+
}
|
|
56
|
+
const pageSize = ref(
|
|
57
|
+
typeof props.pagination === "object" && props.pagination?.pageSize ? props.pagination.pageSize : 20
|
|
58
|
+
);
|
|
59
|
+
const pageSizeOptions = computed(() => Array.from(/* @__PURE__ */ new Set([10, 20, 50, 100, pageSize.value])).sort((a, b) => a - b));
|
|
60
|
+
const visibleKeys = ref(
|
|
61
|
+
new Set(props.columns.filter((c) => c.visible !== false).map((c) => c.key))
|
|
62
|
+
);
|
|
63
|
+
function toggleColumn(key) {
|
|
64
|
+
const next = new Set(visibleKeys.value);
|
|
65
|
+
if (next.has(key)) next.delete(key);
|
|
66
|
+
else next.add(key);
|
|
67
|
+
visibleKeys.value = next;
|
|
68
|
+
}
|
|
69
|
+
const visibleColumns = computed(
|
|
70
|
+
() => props.columns.filter((c) => visibleKeys.value.has(c.key))
|
|
71
|
+
);
|
|
72
|
+
const menuColumns = computed(() => props.columns);
|
|
73
|
+
function searchOf(col, row) {
|
|
74
|
+
if (col.searchGetter) return col.searchGetter(row).toLowerCase();
|
|
75
|
+
if (col.sortGetter) {
|
|
76
|
+
const v = col.sortGetter(row);
|
|
77
|
+
return v == null ? "" : String(v).toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
const raw = row[col.key];
|
|
80
|
+
return raw == null ? "" : String(raw).toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
const filterDims = computed(() => {
|
|
83
|
+
const raw = props.filterTabs;
|
|
84
|
+
if (!raw) return [];
|
|
85
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
86
|
+
return arr.map((f, i) => ({ key: f.key ?? f.label ?? `dim${i}`, label: f.label ?? t("shared.datatable.filter.dimension.label.default"), getter: f.getter, options: f.options, allLabel: f.allLabel }));
|
|
87
|
+
});
|
|
88
|
+
const activeFilters = ref({});
|
|
89
|
+
const anyFilterActive = computed(() => Object.values(activeFilters.value).some(Boolean));
|
|
90
|
+
function setFilter(key, value) {
|
|
91
|
+
activeFilters.value = { ...activeFilters.value, [key]: value };
|
|
92
|
+
}
|
|
93
|
+
function dimCounts(d) {
|
|
94
|
+
const m = {};
|
|
95
|
+
for (const row of props.data) {
|
|
96
|
+
const k = String(d.getter(row) ?? "");
|
|
97
|
+
m[k] = (m[k] ?? 0) + 1;
|
|
98
|
+
}
|
|
99
|
+
return m;
|
|
100
|
+
}
|
|
101
|
+
const filtered = computed(() => {
|
|
102
|
+
if (props.serverMode || !filterDims.value.length) return props.data;
|
|
103
|
+
const active = filterDims.value.filter((d) => activeFilters.value[d.key]);
|
|
104
|
+
if (!active.length) return props.data;
|
|
105
|
+
return props.data.filter((row) => active.every((d) => String(d.getter(row) ?? "") === activeFilters.value[d.key]));
|
|
106
|
+
});
|
|
107
|
+
const searched = computed(() => {
|
|
108
|
+
if (props.serverMode) return props.data;
|
|
109
|
+
const q = query.value.trim().toLowerCase();
|
|
110
|
+
if (!q) return filtered.value;
|
|
111
|
+
return filtered.value.filter(
|
|
112
|
+
(row) => props.columns.some((col) => searchOf(col, row).includes(q))
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
function sortKey(col, row) {
|
|
116
|
+
if (col.sortGetter) return col.sortGetter(row);
|
|
117
|
+
const raw = row[col.key];
|
|
118
|
+
return raw;
|
|
119
|
+
}
|
|
120
|
+
const sorted = computed(() => {
|
|
121
|
+
if (props.serverMode) return searched.value;
|
|
122
|
+
const col = sort.value.column;
|
|
123
|
+
const dir = sort.value.direction;
|
|
124
|
+
if (!col || !dir) return searched.value;
|
|
125
|
+
const c = props.columns.find((x) => x.key === col);
|
|
126
|
+
if (!c) return searched.value;
|
|
127
|
+
const sign = dir === "asc" ? 1 : -1;
|
|
128
|
+
return [...searched.value].sort((a, b) => {
|
|
129
|
+
const av = sortKey(c, a);
|
|
130
|
+
const bv = sortKey(c, b);
|
|
131
|
+
const aNull = av == null || av === "";
|
|
132
|
+
const bNull = bv == null || bv === "";
|
|
133
|
+
if (aNull && bNull) return 0;
|
|
134
|
+
if (aNull) return 1;
|
|
135
|
+
if (bNull) return -1;
|
|
136
|
+
const aCmp = typeof av === "boolean" ? av ? 1 : 0 : av;
|
|
137
|
+
const bCmp = typeof bv === "boolean" ? bv ? 1 : 0 : bv;
|
|
138
|
+
if (aCmp < bCmp) return -1 * sign;
|
|
139
|
+
if (aCmp > bCmp) return 1 * sign;
|
|
140
|
+
return 0;
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
const totalCount = computed(
|
|
144
|
+
() => props.serverMode ? props.total ?? props.data.length : sorted.value.length
|
|
145
|
+
);
|
|
146
|
+
const paginated = computed(() => {
|
|
147
|
+
if (props.serverMode || !props.pagination) return sorted.value;
|
|
148
|
+
const start = page.value * pageSize.value;
|
|
149
|
+
return sorted.value.slice(start, start + pageSize.value);
|
|
150
|
+
});
|
|
151
|
+
const displayedRows = computed(() => props.groupBy ? sorted.value : paginated.value);
|
|
152
|
+
const keyedRows = computed(() => {
|
|
153
|
+
const seen = /* @__PURE__ */ new Map();
|
|
154
|
+
return displayedRows.value.map((row) => {
|
|
155
|
+
const base = rowKeyFor(row) ?? "\u2205";
|
|
156
|
+
const n = seen.get(base) ?? 0;
|
|
157
|
+
seen.set(base, n + 1);
|
|
158
|
+
return { row, key: n === 0 ? base : `${base}#${n}` };
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
const renderRows = computed(() => {
|
|
162
|
+
if (!props.groupBy) return keyedRows.value.map(({ row, key }) => ({ kind: "row", row, key }));
|
|
163
|
+
const emptyLabel = props.groupEmptyLabel ?? t("shared.datatable.group.ungrouped.default");
|
|
164
|
+
const groups = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const entry of keyedRows.value) {
|
|
166
|
+
const labels2 = props.groupBy(entry.row);
|
|
167
|
+
for (const label of labels2.length ? labels2 : [emptyLabel]) {
|
|
168
|
+
if (!groups.has(label)) groups.set(label, []);
|
|
169
|
+
groups.get(label).push(entry);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const labels = [...groups.keys()].sort((a, b) => a.localeCompare(b));
|
|
173
|
+
if (labels.includes(emptyLabel)) {
|
|
174
|
+
labels.splice(labels.indexOf(emptyLabel), 1);
|
|
175
|
+
labels.push(emptyLabel);
|
|
176
|
+
}
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const label of labels) {
|
|
179
|
+
const rows = groups.get(label);
|
|
180
|
+
out.push({ kind: "header", label, count: rows.length, key: `__group__${label}` });
|
|
181
|
+
for (const e of rows) out.push({ kind: "row", row: e.row, key: `${label}::${e.key}` });
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
});
|
|
185
|
+
function emitState() {
|
|
186
|
+
emit("stateChange", {
|
|
187
|
+
search: query.value,
|
|
188
|
+
sort: { ...sort.value },
|
|
189
|
+
page: page.value,
|
|
190
|
+
pageSize: pageSize.value
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
watch([query, sort, page, pageSize], () => {
|
|
194
|
+
if (props.serverMode) emitState();
|
|
195
|
+
});
|
|
196
|
+
watch([query, sort, pageSize, activeFilters], () => {
|
|
197
|
+
page.value = 0;
|
|
198
|
+
}, { deep: true });
|
|
199
|
+
function toggleSort(column) {
|
|
200
|
+
if (sort.value.column !== column) {
|
|
201
|
+
sort.value = { column, direction: "asc" };
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (sort.value.direction === "asc") {
|
|
205
|
+
sort.value = { column, direction: "desc" };
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
sort.value = { column: null, direction: null };
|
|
209
|
+
}
|
|
210
|
+
function rowKeyFor(row) {
|
|
211
|
+
if (typeof props.rowKey === "function") return props.rowKey(row);
|
|
212
|
+
return row[props.rowKey];
|
|
213
|
+
}
|
|
214
|
+
const gridTemplate = computed(() => {
|
|
215
|
+
const tracks = visibleColumns.value.map((c) => c.width ?? "minmax(0,1fr)");
|
|
216
|
+
return `grid-template-columns: ${tracks.join(" ")};`;
|
|
217
|
+
});
|
|
218
|
+
const placeholder = computed(
|
|
219
|
+
() => typeof props.search === "object" ? props.search.placeholder ?? t("shared.datatable.toolbar.search.placeholder.default") : t("shared.datatable.toolbar.search.placeholder.default")
|
|
220
|
+
);
|
|
221
|
+
const slots = useSlots();
|
|
222
|
+
const showToolbar = computed(
|
|
223
|
+
() => props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!props.savedViews || !!props.viewModes?.length || !!props.rail || !!slots.actions || !!slots.scope || !!slots["toolbar-left"] || !!slots["toolbar-right"]
|
|
224
|
+
);
|
|
225
|
+
const railLabel = computed(
|
|
226
|
+
() => (typeof props.rail === "object" ? props.rail.label : void 0) ?? t("shared.datatable.toolbar.rail.label.default")
|
|
227
|
+
);
|
|
228
|
+
const showTable = computed(() => !props.viewModes?.length || viewMode.value === "list");
|
|
229
|
+
const sortableColumns = computed(() => props.columns.filter((c) => c.sortable));
|
|
230
|
+
const showSortMenu = computed(() => !showTable.value && sortableColumns.value.length > 0);
|
|
231
|
+
function cellIconFor(col) {
|
|
232
|
+
return col.cellIcon ?? null;
|
|
233
|
+
}
|
|
234
|
+
const showFooter = computed(() => props.pagination !== false && !props.groupBy);
|
|
235
|
+
const lastPage = computed(
|
|
236
|
+
() => pageSize.value > 0 ? Math.max(0, Math.ceil(totalCount.value / pageSize.value) - 1) : 0
|
|
237
|
+
);
|
|
238
|
+
const rangeStart = computed(() => totalCount.value === 0 ? 0 : page.value * pageSize.value + 1);
|
|
239
|
+
const rangeEnd = computed(
|
|
240
|
+
() => Math.min((page.value + 1) * pageSize.value, totalCount.value)
|
|
241
|
+
);
|
|
242
|
+
function prevPage() {
|
|
243
|
+
if (page.value > 0) page.value -= 1;
|
|
244
|
+
}
|
|
245
|
+
function nextPage() {
|
|
246
|
+
if (page.value < lastPage.value) page.value += 1;
|
|
247
|
+
}
|
|
248
|
+
function defaultCell(col, row) {
|
|
249
|
+
const v = col.sortGetter ? col.sortGetter(row) : row[col.key];
|
|
250
|
+
if (v == null) return "\u2014";
|
|
251
|
+
return String(v);
|
|
252
|
+
}
|
|
253
|
+
</script>
|
|
254
|
+
|
|
255
|
+
<template>
|
|
256
|
+
<div class="rounded-lg border border-black/[0.06] overflow-hidden flex flex-col">
|
|
257
|
+
<!-- Toolbar ------------------------------------------------------ -->
|
|
258
|
+
<!-- The single control row of a list surface:
|
|
259
|
+
LEFT = rail · search · filter · columns · #toolbar-left
|
|
260
|
+
RIGHT = #toolbar-right · saved views · #scope · view mode · #actions
|
|
261
|
+
(Pagination always lives in the footer.) -->
|
|
262
|
+
<div
|
|
263
|
+
v-if="showToolbar"
|
|
264
|
+
class="flex flex-wrap items-center justify-between gap-2 px-4 py-3 bg-white/55 backdrop-blur-md"
|
|
265
|
+
>
|
|
266
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
267
|
+
<Button
|
|
268
|
+
v-if="rail"
|
|
269
|
+
variant="outline"
|
|
270
|
+
size="sm"
|
|
271
|
+
class="h-7 shrink-0 gap-1.5 text-sm"
|
|
272
|
+
:class="railOpen ? 'border-primary/30 text-primary' : ''"
|
|
273
|
+
:aria-pressed="railOpen"
|
|
274
|
+
:aria-label="railOpen ? t('shared.datatable.toolbar.rail.aria.hide', { label: railLabel }) : t('shared.datatable.toolbar.rail.aria.show', { label: railLabel })"
|
|
275
|
+
@click="railOpen = !railOpen"
|
|
276
|
+
>
|
|
277
|
+
<SidebarMinimalistic :size="14" />
|
|
278
|
+
{{ railLabel }}
|
|
279
|
+
</Button>
|
|
280
|
+
<!-- Shrinks (never wraps the row) once the toolbar carries scope /
|
|
281
|
+
view-mode / saved-view controls next to it. -->
|
|
282
|
+
<div v-if="search" class="relative w-[21rem] min-w-40 max-w-full shrink">
|
|
283
|
+
<Search :size="13" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
284
|
+
<Input
|
|
285
|
+
v-model="query"
|
|
286
|
+
:placeholder="placeholder"
|
|
287
|
+
class="h-7 pl-8 text-sm bg-white"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
<!-- Generic "Filter" dropdown — each dimension is a labelled sub-menu. -->
|
|
291
|
+
<DropdownMenu v-if="filterDims.length">
|
|
292
|
+
<DropdownMenuTrigger as-child>
|
|
293
|
+
<Button
|
|
294
|
+
variant="outline"
|
|
295
|
+
size="sm"
|
|
296
|
+
class="h-7 gap-1.5 text-sm"
|
|
297
|
+
:class="anyFilterActive ? 'border-primary/30 text-primary' : ''"
|
|
298
|
+
>
|
|
299
|
+
<Filter :size="14" />
|
|
300
|
+
{{ t("shared.datatable.toolbar.filter.action") }}
|
|
301
|
+
<AltArrowDown :size="12" class="opacity-60" />
|
|
302
|
+
</Button>
|
|
303
|
+
</DropdownMenuTrigger>
|
|
304
|
+
<DropdownMenuContent align="start" class="w-48">
|
|
305
|
+
<DropdownMenuSub v-for="d in filterDims" :key="d.key">
|
|
306
|
+
<DropdownMenuSubTrigger class="gap-2">
|
|
307
|
+
<span class="flex-1">{{ d.label }}</span>
|
|
308
|
+
<span v-if="activeFilters[d.key]" class="text-xs text-primary capitalize">{{ activeFilters[d.key] }}</span>
|
|
309
|
+
</DropdownMenuSubTrigger>
|
|
310
|
+
<DropdownMenuSubContent class="w-48">
|
|
311
|
+
<DropdownMenuItem class="gap-2" @select="setFilter(d.key, null)">
|
|
312
|
+
<span class="flex-1">{{ d.allLabel ?? t("shared.datatable.toolbar.filter.all.default") }}</span>
|
|
313
|
+
<span class="tabular-nums text-muted-foreground">{{ data.length }}</span>
|
|
314
|
+
</DropdownMenuItem>
|
|
315
|
+
<DropdownMenuSeparator />
|
|
316
|
+
<DropdownMenuItem
|
|
317
|
+
v-for="o in d.options"
|
|
318
|
+
:key="o.value"
|
|
319
|
+
class="gap-2"
|
|
320
|
+
@select="setFilter(d.key, o.value)"
|
|
321
|
+
>
|
|
322
|
+
<span class="flex-1">{{ o.label }}</span>
|
|
323
|
+
<span class="tabular-nums text-muted-foreground">{{ dimCounts(d)[o.value] ?? 0 }}</span>
|
|
324
|
+
</DropdownMenuItem>
|
|
325
|
+
</DropdownMenuSubContent>
|
|
326
|
+
</DropdownMenuSub>
|
|
327
|
+
</DropdownMenuContent>
|
|
328
|
+
</DropdownMenu>
|
|
329
|
+
<FilterMenu
|
|
330
|
+
v-if="filter"
|
|
331
|
+
:width="typeof filter === 'object' ? filter.width : void 0"
|
|
332
|
+
:active="typeof filter === 'object' ? filter.active : void 0"
|
|
333
|
+
>
|
|
334
|
+
<slot name="filter-menu" />
|
|
335
|
+
</FilterMenu>
|
|
336
|
+
<SortMenu v-if="showSortMenu" v-model="sort" :columns="sortableColumns" />
|
|
337
|
+
<ColumnsMenu
|
|
338
|
+
v-if="columnsMenu && showTable"
|
|
339
|
+
:columns="menuColumns"
|
|
340
|
+
:visible="visibleKeys"
|
|
341
|
+
@toggle="toggleColumn"
|
|
342
|
+
/>
|
|
343
|
+
<slot name="toolbar-left" />
|
|
344
|
+
</div>
|
|
345
|
+
<div class="flex items-center gap-1.5 ml-auto shrink-0">
|
|
346
|
+
<slot name="toolbar-right" />
|
|
347
|
+
<SavedViewsMenu v-if="savedViews" :config="savedViews" />
|
|
348
|
+
<!-- Market scope ("Showing: … / All markets") — the surface passes the
|
|
349
|
+
shared MarketScopeToggle in; the row position is fixed here. -->
|
|
350
|
+
<slot name="scope" />
|
|
351
|
+
<ViewModeSwitch v-if="viewModes?.length" v-model="viewMode" :modes="viewModes" />
|
|
352
|
+
<slot name="actions" />
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<!-- Alternate view mode (gallery, board, …) — same toolbar, same footer. -->
|
|
357
|
+
<div v-if="!showTable" class="border-t border-black/[0.04] bg-white/55">
|
|
358
|
+
<slot name="view" :rows="displayedRows" :loading="loading" />
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<!-- Header row -------------------------------------------------- -->
|
|
362
|
+
<div
|
|
363
|
+
v-if="showTable"
|
|
364
|
+
class="grid items-center gap-2 border-t rounded-t-lg border-gray-200 bg-gray-100 px-4 py-2.5 text-sm font-medium uppercase tracking-normal text-muted-foreground"
|
|
365
|
+
:style="gridTemplate"
|
|
366
|
+
>
|
|
367
|
+
<div
|
|
368
|
+
v-for="col in visibleColumns"
|
|
369
|
+
:key="col.key"
|
|
370
|
+
:class="col.alignRight ? 'text-right' : ''"
|
|
371
|
+
>
|
|
372
|
+
<!-- `#header-<key>` lets a page take over a header cell (e.g. a
|
|
373
|
+
select-all checkbox in a selection column). Default: sortable
|
|
374
|
+
SortHeader or the plain label. -->
|
|
375
|
+
<slot :name="`header-${col.key}`">
|
|
376
|
+
<SortHeader
|
|
377
|
+
v-if="col.sortable"
|
|
378
|
+
:column="col.key"
|
|
379
|
+
:active="sort.column"
|
|
380
|
+
:direction="sort.direction"
|
|
381
|
+
:align-right="col.alignRight"
|
|
382
|
+
@toggle="toggleSort"
|
|
383
|
+
>
|
|
384
|
+
{{ col.label }}
|
|
385
|
+
</SortHeader>
|
|
386
|
+
<span v-else>{{ col.label }}</span>
|
|
387
|
+
</slot>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<!-- Body --------------------------------------------------------- -->
|
|
392
|
+
<template v-for="entry in showTable ? renderRows : []" :key="entry.key">
|
|
393
|
+
<!-- Group header (groupBy mode) -->
|
|
394
|
+
<div
|
|
395
|
+
v-if="entry.kind === 'header'"
|
|
396
|
+
class="flex items-center gap-2 border-t border-black/[0.04] bg-gray-100/70 px-4 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
|
397
|
+
>
|
|
398
|
+
{{ entry.label }}
|
|
399
|
+
<span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
|
|
400
|
+
</div>
|
|
401
|
+
<div
|
|
402
|
+
v-else
|
|
403
|
+
class="group grid items-center gap-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
|
|
404
|
+
:style="gridTemplate"
|
|
405
|
+
@click="emit('rowClick', entry.row)"
|
|
406
|
+
>
|
|
407
|
+
<div
|
|
408
|
+
v-for="col in visibleColumns"
|
|
409
|
+
:key="col.key"
|
|
410
|
+
class="min-w-0"
|
|
411
|
+
:class="col.alignRight ? 'text-right' : ''"
|
|
412
|
+
>
|
|
413
|
+
<slot :name="`cell-${col.key}`" :row="entry.row">
|
|
414
|
+
<!-- Primary column: bold record anchor + optional tinted icon tile. -->
|
|
415
|
+
<div v-if="col.primary" class="flex items-center gap-2.5 min-w-0">
|
|
416
|
+
<span
|
|
417
|
+
v-if="cellIconFor(col)"
|
|
418
|
+
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"
|
|
419
|
+
>
|
|
420
|
+
<component :is="cellIconFor(col)" :size="13" />
|
|
421
|
+
</span>
|
|
422
|
+
<span class="text-sm font-semibold text-[#0B0522] truncate transition-colors group-hover:text-primary">{{ defaultCell(col, entry.row) }}</span>
|
|
423
|
+
</div>
|
|
424
|
+
<!-- `block`, or `truncate` clips nothing: an inline span establishes
|
|
425
|
+
no box, so a long unbroken value overruns the next column
|
|
426
|
+
instead of ending in an ellipsis. -->
|
|
427
|
+
<span v-else class="block truncate text-sm" :title="String(defaultCell(col, entry.row) ?? '')">{{ defaultCell(col, entry.row) }}</span>
|
|
428
|
+
</slot>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</template>
|
|
432
|
+
|
|
433
|
+
<!-- Loading skeleton (only when there are no rows yet) ----------- -->
|
|
434
|
+
<template v-if="showTable && loading && !displayedRows.length">
|
|
435
|
+
<div
|
|
436
|
+
v-for="n in 6"
|
|
437
|
+
:key="`sk-${n}`"
|
|
438
|
+
class="grid items-center gap-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
|
|
439
|
+
:style="gridTemplate"
|
|
440
|
+
>
|
|
441
|
+
<div
|
|
442
|
+
v-for="col in visibleColumns"
|
|
443
|
+
:key="col.key"
|
|
444
|
+
:class="col.alignRight ? 'flex justify-end' : ''"
|
|
445
|
+
>
|
|
446
|
+
<div
|
|
447
|
+
class="h-3 rounded bg-black/[0.07] animate-pulse"
|
|
448
|
+
:class="col.alignRight ? 'w-10' : 'w-2/3'"
|
|
449
|
+
/>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
</template>
|
|
453
|
+
|
|
454
|
+
<!-- Empty -------------------------------------------------------- -->
|
|
455
|
+
<div
|
|
456
|
+
v-if="showTable && !displayedRows.length && !loading"
|
|
457
|
+
class="px-3 py-8 text-center text-sm text-muted-foreground"
|
|
458
|
+
>
|
|
459
|
+
<slot name="empty">
|
|
460
|
+
{{ emptyText ?? t("shared.datatable.empty.default") }}
|
|
461
|
+
</slot>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<!-- Footer ------------------------------------------------------- -->
|
|
465
|
+
<div
|
|
466
|
+
v-if="showFooter"
|
|
467
|
+
class="flex items-center justify-between gap-2 border-t border-gray-200 bg-white/55 backdrop-blur-md px-4 py-3 text-sm text-muted-foreground/60"
|
|
468
|
+
>
|
|
469
|
+
<div class="flex items-center gap-2.5">
|
|
470
|
+
<span class="tabular-nums">
|
|
471
|
+
<span v-if="totalCount === 0">{{ t("shared.datatable.footer.noresults") }}</span>
|
|
472
|
+
<span v-else>{{ t("shared.datatable.footer.showing", { start: rangeStart, end: rangeEnd, total: totalCount }) }}</span>
|
|
473
|
+
</span>
|
|
474
|
+
<span class="text-muted-foreground/40">-</span>
|
|
475
|
+
<label class="flex items-center gap-1.5">
|
|
476
|
+
<span class="hidden sm:inline">{{ t("shared.datatable.footer.pagesize.label") }}</span>
|
|
477
|
+
<select
|
|
478
|
+
v-model.number="pageSize"
|
|
479
|
+
class="h-7 rounded-md border border-input bg-white px-1.5 text-sm tabular-nums focus:outline-none focus:ring-1 focus:ring-ring"
|
|
480
|
+
>
|
|
481
|
+
<option v-for="s in pageSizeOptions" :key="s" :value="s">{{ s }}</option>
|
|
482
|
+
</select>
|
|
483
|
+
</label>
|
|
484
|
+
</div>
|
|
485
|
+
<div v-if="lastPage > 0" class="flex items-center gap-2">
|
|
486
|
+
<Button
|
|
487
|
+
variant="outline"
|
|
488
|
+
size="sm"
|
|
489
|
+
class="h-7 text-sm"
|
|
490
|
+
:disabled="page === 0 || loading"
|
|
491
|
+
@click="prevPage"
|
|
492
|
+
>
|
|
493
|
+
{{ t("shared.datatable.footer.action.previous") }}
|
|
494
|
+
</Button>
|
|
495
|
+
<span class="tabular-nums">
|
|
496
|
+
{{ page + 1 }} / {{ lastPage + 1 }}
|
|
497
|
+
</span>
|
|
498
|
+
<Button
|
|
499
|
+
variant="outline"
|
|
500
|
+
size="sm"
|
|
501
|
+
class="h-7 text-sm"
|
|
502
|
+
:disabled="page >= lastPage || loading"
|
|
503
|
+
@click="nextPage"
|
|
504
|
+
>
|
|
505
|
+
{{ t("shared.datatable.footer.action.next") }}
|
|
506
|
+
</Button>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
</template>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { DataTableColumn, DataTableFilter, DataTableRail, DataTableSavedViews, DataTableState, DataTableViewMode, RowKey, SortState } from './types.js';
|
|
2
|
+
declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
3
|
+
props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{
|
|
4
|
+
readonly "onUpdate:sort"?: ((value: SortState) => any) | undefined;
|
|
5
|
+
readonly "onUpdate:page"?: ((value: number) => any) | undefined;
|
|
6
|
+
readonly "onUpdate:viewMode"?: ((value: string) => any) | undefined;
|
|
7
|
+
readonly "onUpdate:railOpen"?: ((value: boolean) => any) | undefined;
|
|
8
|
+
readonly onRowClick?: ((row: T) => any) | undefined;
|
|
9
|
+
readonly onStateChange?: ((state: DataTableState) => any) | undefined;
|
|
10
|
+
} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:sort" | "onUpdate:page" | "onUpdate:viewMode" | "onUpdate:railOpen" | "onRowClick" | "onStateChange"> & ({
|
|
11
|
+
data: T[];
|
|
12
|
+
columns: DataTableColumn<T>[];
|
|
13
|
+
rowKey: RowKey<T>;
|
|
14
|
+
search?: boolean | {
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
};
|
|
17
|
+
columnsMenu?: boolean;
|
|
18
|
+
/** Mount the surface's own filter panel in the toolbar's Filter popover
|
|
19
|
+
* (`#filter-menu`). Pass an object to widen it / flag it as active. */
|
|
20
|
+
filter?: boolean | {
|
|
21
|
+
width?: string;
|
|
22
|
+
active?: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** Built-in status/dimension filter pills (static mode). One dimension, or
|
|
25
|
+
* several (each renders as its own sub-menu). */
|
|
26
|
+
filterTabs?: DataTableFilter<T> | DataTableFilter<T>[];
|
|
27
|
+
/** Saved views control — the surface supplies snapshot/apply, the table
|
|
28
|
+
* owns the control, the storage and the dialogs. */
|
|
29
|
+
savedViews?: DataTableSavedViews | null;
|
|
30
|
+
/** View-mode switch (e.g. List | Gallery). Any mode other than `list`
|
|
31
|
+
* renders the `#view` slot in place of the table body. */
|
|
32
|
+
viewModes?: DataTableViewMode[] | null;
|
|
33
|
+
/** Collapse handle for the surface's left rail (see `ListSurface`). */
|
|
34
|
+
rail?: boolean | DataTableRail;
|
|
35
|
+
pagination?: boolean | {
|
|
36
|
+
pageSize?: number;
|
|
37
|
+
};
|
|
38
|
+
defaultSort?: {
|
|
39
|
+
column: string;
|
|
40
|
+
direction: "asc" | "desc";
|
|
41
|
+
};
|
|
42
|
+
serverMode?: boolean;
|
|
43
|
+
total?: number;
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
emptyText?: string;
|
|
46
|
+
groupBy?: ((row: T) => string[]) | null;
|
|
47
|
+
groupEmptyLabel?: string;
|
|
48
|
+
} & {
|
|
49
|
+
sort?: SortState;
|
|
50
|
+
page?: number;
|
|
51
|
+
viewMode?: string;
|
|
52
|
+
railOpen?: boolean;
|
|
53
|
+
}) & Partial<{}>> & import("vue").PublicProps;
|
|
54
|
+
expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
|
|
55
|
+
attrs: any;
|
|
56
|
+
slots: {
|
|
57
|
+
[x: string]: ((props: {}) => any) | undefined;
|
|
58
|
+
} & {
|
|
59
|
+
[x: string]: ((props: {
|
|
60
|
+
row: any;
|
|
61
|
+
}) => any) | undefined;
|
|
62
|
+
} & {
|
|
63
|
+
'filter-menu'?: (props: {}) => any;
|
|
64
|
+
} & {
|
|
65
|
+
'toolbar-left'?: (props: {}) => any;
|
|
66
|
+
} & {
|
|
67
|
+
'toolbar-right'?: (props: {}) => any;
|
|
68
|
+
} & {
|
|
69
|
+
scope?: (props: {}) => any;
|
|
70
|
+
} & {
|
|
71
|
+
actions?: (props: {}) => any;
|
|
72
|
+
} & {
|
|
73
|
+
view?: (props: {
|
|
74
|
+
rows: any;
|
|
75
|
+
loading: any;
|
|
76
|
+
}) => any;
|
|
77
|
+
} & {
|
|
78
|
+
empty?: (props: {}) => any;
|
|
79
|
+
};
|
|
80
|
+
emit: (((evt: "rowClick", row: T) => void) & ((evt: "stateChange", state: DataTableState) => void)) & (((evt: "update:sort", value: SortState) => void) & ((evt: "update:page", value: number) => void) & ((evt: "update:viewMode", value: string) => void) & ((evt: "update:railOpen", value: boolean) => void));
|
|
81
|
+
}>) => import("vue").VNode & {
|
|
82
|
+
__ctx?: Awaited<typeof __VLS_setup>;
|
|
83
|
+
};
|
|
84
|
+
export default _default;
|
|
85
|
+
type __VLS_PrettifyLocal<T> = {
|
|
86
|
+
[K in keyof T]: T[K];
|
|
87
|
+
} & {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** Popover width class (a wide panel of inputs needs more than the default). */
|
|
3
|
+
width?: string;
|
|
4
|
+
/** Tint the trigger while at least one filter is set. */
|
|
5
|
+
active?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare var __VLS_34: {};
|
|
8
|
+
type __VLS_Slots = {} & {
|
|
9
|
+
default?: (props: typeof __VLS_34) => any;
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
12
|
+
active: boolean;
|
|
13
|
+
width: string;
|
|
14
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
16
|
+
export default _default;
|
|
17
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
18
|
+
new (): {
|
|
19
|
+
$slots: S;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { AltArrowDown, Filter } from "#solar-icons";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuLabel,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuTrigger
|
|
11
|
+
} from "@revenexx/studio";
|
|
12
|
+
defineProps({
|
|
13
|
+
width: { type: String, required: false, default: "w-56" },
|
|
14
|
+
active: { type: Boolean, required: false, default: false }
|
|
15
|
+
});
|
|
16
|
+
const t = useT();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<DropdownMenu>
|
|
21
|
+
<DropdownMenuTrigger as-child>
|
|
22
|
+
<Button
|
|
23
|
+
variant="outline"
|
|
24
|
+
size="sm"
|
|
25
|
+
class="h-7 gap-1.5 text-sm"
|
|
26
|
+
:class="active ? 'border-primary/30 text-primary' : ''"
|
|
27
|
+
:aria-label="t('shared.datatable.filtermenu.aria')"
|
|
28
|
+
>
|
|
29
|
+
<Filter :size="14" />
|
|
30
|
+
{{ t("shared.datatable.filtermenu.action") }}
|
|
31
|
+
<AltArrowDown :size="12" class="opacity-60" />
|
|
32
|
+
</Button>
|
|
33
|
+
</DropdownMenuTrigger>
|
|
34
|
+
<DropdownMenuContent align="start" :class="width">
|
|
35
|
+
<DropdownMenuLabel class="text-sm text-muted-foreground">
|
|
36
|
+
{{ t("shared.datatable.filtermenu.heading") }}
|
|
37
|
+
</DropdownMenuLabel>
|
|
38
|
+
<DropdownMenuSeparator />
|
|
39
|
+
<slot>
|
|
40
|
+
<DropdownMenuItem disabled>
|
|
41
|
+
{{ t("shared.datatable.filtermenu.empty") }}
|
|
42
|
+
</DropdownMenuItem>
|
|
43
|
+
</slot>
|
|
44
|
+
</DropdownMenuContent>
|
|
45
|
+
</DropdownMenu>
|
|
46
|
+
</template>
|