@revenexx/studio-shared 0.1.2 → 0.2.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 +9 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +27 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +12 -0
- package/{components → dist/runtime/components}/DataTable/ColumnsMenu.vue +10 -17
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +12 -0
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +59 -0
- package/dist/runtime/components/DataTable/DataTable.vue +453 -0
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +59 -0
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +12 -0
- package/{components → dist/runtime/components}/DataTable/FilterMenu.vue +4 -8
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +12 -0
- package/dist/runtime/components/DataTable/types.d.ts +70 -0
- package/dist/runtime/components/DataTable/types.js +0 -0
- package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +30 -0
- package/{components → dist/runtime/components}/Shared/BuildLogsFlyout.vue +27 -45
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +30 -0
- package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +17 -0
- package/{components → dist/runtime/components}/Shared/GitDeploymentSettings.vue +22 -35
- 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/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/SidebarNav.d.vue.ts +36 -0
- package/{components → dist/runtime/components}/Shared/SidebarNav.vue +15 -47
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +36 -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/{components → dist/runtime/components}/Shared/Wizard.vue +24 -41
- package/dist/runtime/components/Shared/Wizard.vue.d.ts +47 -0
- package/{composables/usePlatformAuth.ts → dist/runtime/composables/usePlatformAuth.d.ts} +3 -21
- package/dist/runtime/composables/usePlatformAuth.js +17 -0
- package/dist/runtime/composables/usePlatformTenant.d.ts +14 -0
- package/dist/runtime/composables/usePlatformTenant.js +18 -0
- package/dist/runtime/types/nav.d.ts +23 -0
- package/dist/runtime/types/nav.js +0 -0
- package/dist/types.d.mts +15 -0
- package/package.json +25 -13
- package/components/DataTable/DataTable.vue +0 -518
- package/components/DataTable/types.ts +0 -82
- package/components/Shared/SortHeader.vue +0 -50
- package/components/Shared/StudioCard.vue +0 -24
- package/composables/usePlatformTenant.ts +0 -35
- package/index.ts +0 -16
- package/nuxt.config.ts +0 -23
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { AltArrowDown, Filter, Magnifier as Search } 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
|
+
const props = defineProps({
|
|
19
|
+
data: { type: Array, required: true },
|
|
20
|
+
columns: { type: Array, required: true },
|
|
21
|
+
rowKey: { type: Function, required: true, skipCheck: true },
|
|
22
|
+
search: { type: [Boolean, Object], required: false, default: true },
|
|
23
|
+
columnsMenu: { type: Boolean, required: false, default: true },
|
|
24
|
+
filter: { type: Boolean, required: false, default: false },
|
|
25
|
+
filterTabs: { type: [Object, Array], required: false },
|
|
26
|
+
pagination: { type: [Boolean, Object], required: false, default: true },
|
|
27
|
+
defaultSort: { type: Object, required: false },
|
|
28
|
+
serverMode: { type: Boolean, required: false, default: false },
|
|
29
|
+
total: { type: Number, required: false },
|
|
30
|
+
loading: { type: Boolean, required: false, default: false },
|
|
31
|
+
emptyText: { type: String, required: false },
|
|
32
|
+
groupBy: { type: [Function, null], required: false, default: null },
|
|
33
|
+
groupEmptyLabel: { type: String, required: false }
|
|
34
|
+
});
|
|
35
|
+
const emit = defineEmits(["rowClick", "stateChange"]);
|
|
36
|
+
const query = ref("");
|
|
37
|
+
const sort = defineModel("sort", { type: Object, ...{
|
|
38
|
+
default: () => ({
|
|
39
|
+
column: null,
|
|
40
|
+
direction: null
|
|
41
|
+
})
|
|
42
|
+
} });
|
|
43
|
+
const page = defineModel("page", { type: Number, ...{ default: 0 } });
|
|
44
|
+
if (props.defaultSort && sort.value.column == null) {
|
|
45
|
+
sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction };
|
|
46
|
+
}
|
|
47
|
+
const pageSize = ref(
|
|
48
|
+
typeof props.pagination === "object" && props.pagination?.pageSize ? props.pagination.pageSize : 20
|
|
49
|
+
);
|
|
50
|
+
const pageSizeOptions = computed(() => Array.from(/* @__PURE__ */ new Set([10, 20, 50, 100, pageSize.value])).sort((a, b) => a - b));
|
|
51
|
+
const visibleKeys = ref(
|
|
52
|
+
new Set(props.columns.filter((c) => c.visible !== false).map((c) => c.key))
|
|
53
|
+
);
|
|
54
|
+
function toggleColumn(key) {
|
|
55
|
+
const next = new Set(visibleKeys.value);
|
|
56
|
+
if (next.has(key)) next.delete(key);
|
|
57
|
+
else next.add(key);
|
|
58
|
+
visibleKeys.value = next;
|
|
59
|
+
}
|
|
60
|
+
const visibleColumns = computed(
|
|
61
|
+
() => props.columns.filter((c) => visibleKeys.value.has(c.key))
|
|
62
|
+
);
|
|
63
|
+
const menuColumns = computed(() => props.columns);
|
|
64
|
+
function searchOf(col, row) {
|
|
65
|
+
if (col.searchGetter) return col.searchGetter(row).toLowerCase();
|
|
66
|
+
if (col.sortGetter) {
|
|
67
|
+
const v = col.sortGetter(row);
|
|
68
|
+
return v == null ? "" : String(v).toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
const raw = row[col.key];
|
|
71
|
+
return raw == null ? "" : String(raw).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
const filterDims = computed(() => {
|
|
74
|
+
const raw = props.filterTabs;
|
|
75
|
+
if (!raw) return [];
|
|
76
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
77
|
+
return arr.map((f, i) => ({ key: f.key ?? f.label ?? `dim${i}`, label: f.label ?? "Status", getter: f.getter, options: f.options, allLabel: f.allLabel }));
|
|
78
|
+
});
|
|
79
|
+
const activeFilters = ref({});
|
|
80
|
+
const anyFilterActive = computed(() => Object.values(activeFilters.value).some(Boolean));
|
|
81
|
+
function setFilter(key, value) {
|
|
82
|
+
activeFilters.value = { ...activeFilters.value, [key]: value };
|
|
83
|
+
}
|
|
84
|
+
function dimCounts(d) {
|
|
85
|
+
const m = {};
|
|
86
|
+
for (const row of props.data) {
|
|
87
|
+
const k = String(d.getter(row) ?? "");
|
|
88
|
+
m[k] = (m[k] ?? 0) + 1;
|
|
89
|
+
}
|
|
90
|
+
return m;
|
|
91
|
+
}
|
|
92
|
+
const filtered = computed(() => {
|
|
93
|
+
if (props.serverMode || !filterDims.value.length) return props.data;
|
|
94
|
+
const active = filterDims.value.filter((d) => activeFilters.value[d.key]);
|
|
95
|
+
if (!active.length) return props.data;
|
|
96
|
+
return props.data.filter((row) => active.every((d) => String(d.getter(row) ?? "") === activeFilters.value[d.key]));
|
|
97
|
+
});
|
|
98
|
+
const searched = computed(() => {
|
|
99
|
+
if (props.serverMode) return props.data;
|
|
100
|
+
const q = query.value.trim().toLowerCase();
|
|
101
|
+
if (!q) return filtered.value;
|
|
102
|
+
return filtered.value.filter(
|
|
103
|
+
(row) => props.columns.some((col) => searchOf(col, row).includes(q))
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
function sortKey(col, row) {
|
|
107
|
+
if (col.sortGetter) return col.sortGetter(row);
|
|
108
|
+
const raw = row[col.key];
|
|
109
|
+
return raw;
|
|
110
|
+
}
|
|
111
|
+
const sorted = computed(() => {
|
|
112
|
+
if (props.serverMode) return searched.value;
|
|
113
|
+
const col = sort.value.column;
|
|
114
|
+
const dir = sort.value.direction;
|
|
115
|
+
if (!col || !dir) return searched.value;
|
|
116
|
+
const c = props.columns.find((x) => x.key === col);
|
|
117
|
+
if (!c) return searched.value;
|
|
118
|
+
const sign = dir === "asc" ? 1 : -1;
|
|
119
|
+
return [...searched.value].sort((a, b) => {
|
|
120
|
+
const av = sortKey(c, a);
|
|
121
|
+
const bv = sortKey(c, b);
|
|
122
|
+
const aNull = av == null || av === "";
|
|
123
|
+
const bNull = bv == null || bv === "";
|
|
124
|
+
if (aNull && bNull) return 0;
|
|
125
|
+
if (aNull) return 1;
|
|
126
|
+
if (bNull) return -1;
|
|
127
|
+
const aCmp = typeof av === "boolean" ? av ? 1 : 0 : av;
|
|
128
|
+
const bCmp = typeof bv === "boolean" ? bv ? 1 : 0 : bv;
|
|
129
|
+
if (aCmp < bCmp) return -1 * sign;
|
|
130
|
+
if (aCmp > bCmp) return 1 * sign;
|
|
131
|
+
return 0;
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
const totalCount = computed(
|
|
135
|
+
() => props.serverMode ? props.total ?? props.data.length : sorted.value.length
|
|
136
|
+
);
|
|
137
|
+
const paginated = computed(() => {
|
|
138
|
+
if (props.serverMode || !props.pagination) return sorted.value;
|
|
139
|
+
const start = page.value * pageSize.value;
|
|
140
|
+
return sorted.value.slice(start, start + pageSize.value);
|
|
141
|
+
});
|
|
142
|
+
const displayedRows = computed(() => props.groupBy ? sorted.value : paginated.value);
|
|
143
|
+
const keyedRows = computed(() => {
|
|
144
|
+
const seen = /* @__PURE__ */ new Map();
|
|
145
|
+
return displayedRows.value.map((row) => {
|
|
146
|
+
const base = rowKeyFor(row) ?? "\u2205";
|
|
147
|
+
const n = seen.get(base) ?? 0;
|
|
148
|
+
seen.set(base, n + 1);
|
|
149
|
+
return { row, key: n === 0 ? base : `${base}#${n}` };
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
const renderRows = computed(() => {
|
|
153
|
+
if (!props.groupBy) return keyedRows.value.map(({ row, key }) => ({ kind: "row", row, key }));
|
|
154
|
+
const emptyLabel = props.groupEmptyLabel ?? "Ungrouped";
|
|
155
|
+
const groups = /* @__PURE__ */ new Map();
|
|
156
|
+
for (const entry of keyedRows.value) {
|
|
157
|
+
const labels2 = props.groupBy(entry.row);
|
|
158
|
+
for (const label of labels2.length ? labels2 : [emptyLabel]) {
|
|
159
|
+
if (!groups.has(label)) groups.set(label, []);
|
|
160
|
+
groups.get(label).push(entry);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const labels = [...groups.keys()].sort((a, b) => a.localeCompare(b));
|
|
164
|
+
if (labels.includes(emptyLabel)) {
|
|
165
|
+
labels.splice(labels.indexOf(emptyLabel), 1);
|
|
166
|
+
labels.push(emptyLabel);
|
|
167
|
+
}
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const label of labels) {
|
|
170
|
+
const rows = groups.get(label);
|
|
171
|
+
out.push({ kind: "header", label, count: rows.length, key: `__group__${label}` });
|
|
172
|
+
for (const e of rows) out.push({ kind: "row", row: e.row, key: `${label}::${e.key}` });
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
});
|
|
176
|
+
function emitState() {
|
|
177
|
+
emit("stateChange", {
|
|
178
|
+
search: query.value,
|
|
179
|
+
sort: { ...sort.value },
|
|
180
|
+
page: page.value,
|
|
181
|
+
pageSize: pageSize.value
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
watch([query, sort, page, pageSize], () => {
|
|
185
|
+
if (props.serverMode) emitState();
|
|
186
|
+
});
|
|
187
|
+
watch([query, sort, pageSize, activeFilters], () => {
|
|
188
|
+
page.value = 0;
|
|
189
|
+
}, { deep: true });
|
|
190
|
+
function toggleSort(column) {
|
|
191
|
+
if (sort.value.column !== column) {
|
|
192
|
+
sort.value = { column, direction: "asc" };
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (sort.value.direction === "asc") {
|
|
196
|
+
sort.value = { column, direction: "desc" };
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
sort.value = { column: null, direction: null };
|
|
200
|
+
}
|
|
201
|
+
function rowKeyFor(row) {
|
|
202
|
+
if (typeof props.rowKey === "function") return props.rowKey(row);
|
|
203
|
+
return row[props.rowKey];
|
|
204
|
+
}
|
|
205
|
+
const gridTemplate = computed(() => {
|
|
206
|
+
const tracks = visibleColumns.value.map((c) => c.width ?? "minmax(0,1fr)");
|
|
207
|
+
return `grid-template-columns: ${tracks.join(" ")};`;
|
|
208
|
+
});
|
|
209
|
+
const placeholder = computed(
|
|
210
|
+
() => typeof props.search === "object" ? props.search.placeholder ?? "Search\u2026" : "Search\u2026"
|
|
211
|
+
);
|
|
212
|
+
const slots = useSlots();
|
|
213
|
+
const showToolbar = computed(
|
|
214
|
+
() => props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!slots.actions || !!slots["toolbar-left"]
|
|
215
|
+
);
|
|
216
|
+
function cellIconFor(col) {
|
|
217
|
+
return col.cellIcon ?? null;
|
|
218
|
+
}
|
|
219
|
+
const showFooter = computed(() => props.pagination !== false && !props.groupBy);
|
|
220
|
+
const lastPage = computed(
|
|
221
|
+
() => pageSize.value > 0 ? Math.max(0, Math.ceil(totalCount.value / pageSize.value) - 1) : 0
|
|
222
|
+
);
|
|
223
|
+
const rangeStart = computed(() => totalCount.value === 0 ? 0 : page.value * pageSize.value + 1);
|
|
224
|
+
const rangeEnd = computed(
|
|
225
|
+
() => Math.min((page.value + 1) * pageSize.value, totalCount.value)
|
|
226
|
+
);
|
|
227
|
+
function prevPage() {
|
|
228
|
+
if (page.value > 0) page.value -= 1;
|
|
229
|
+
}
|
|
230
|
+
function nextPage() {
|
|
231
|
+
if (page.value < lastPage.value) page.value += 1;
|
|
232
|
+
}
|
|
233
|
+
function defaultCell(col, row) {
|
|
234
|
+
const v = col.sortGetter ? col.sortGetter(row) : row[col.key];
|
|
235
|
+
if (v == null) return "\u2014";
|
|
236
|
+
return String(v);
|
|
237
|
+
}
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<template>
|
|
241
|
+
<div class="rounded-lg border border-black/[0.06] overflow-hidden flex flex-col">
|
|
242
|
+
<!-- Toolbar ------------------------------------------------------ -->
|
|
243
|
+
<!-- Toolbar: LEFT = search · filter · columns · RIGHT = actions.
|
|
244
|
+
(Pagination always lives in the footer.) -->
|
|
245
|
+
<div
|
|
246
|
+
v-if="showToolbar"
|
|
247
|
+
class="flex items-center justify-between gap-2 px-4 py-3 bg-white/55 backdrop-blur-md"
|
|
248
|
+
>
|
|
249
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
250
|
+
<div v-if="search" class="relative w-[21rem] max-w-full">
|
|
251
|
+
<Search :size="13" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
252
|
+
<Input
|
|
253
|
+
v-model="query"
|
|
254
|
+
:placeholder="placeholder"
|
|
255
|
+
class="h-7 pl-8 text-sm bg-white"
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
<!-- Generic "Filter" dropdown — each dimension is a labelled sub-menu. -->
|
|
259
|
+
<DropdownMenu v-if="filterDims.length">
|
|
260
|
+
<DropdownMenuTrigger as-child>
|
|
261
|
+
<Button
|
|
262
|
+
variant="outline"
|
|
263
|
+
size="sm"
|
|
264
|
+
class="h-7 gap-1.5 text-sm"
|
|
265
|
+
:class="anyFilterActive ? 'border-primary/30 text-primary' : ''"
|
|
266
|
+
>
|
|
267
|
+
<Filter :size="14" />
|
|
268
|
+
Filter
|
|
269
|
+
<AltArrowDown :size="12" class="opacity-60" />
|
|
270
|
+
</Button>
|
|
271
|
+
</DropdownMenuTrigger>
|
|
272
|
+
<DropdownMenuContent align="start" class="w-48">
|
|
273
|
+
<DropdownMenuSub v-for="d in filterDims" :key="d.key">
|
|
274
|
+
<DropdownMenuSubTrigger class="gap-2">
|
|
275
|
+
<span class="flex-1">{{ d.label }}</span>
|
|
276
|
+
<span v-if="activeFilters[d.key]" class="text-xs text-primary capitalize">{{ activeFilters[d.key] }}</span>
|
|
277
|
+
</DropdownMenuSubTrigger>
|
|
278
|
+
<DropdownMenuSubContent class="w-48">
|
|
279
|
+
<DropdownMenuItem class="gap-2" @select="setFilter(d.key, null)">
|
|
280
|
+
<span class="flex-1">{{ d.allLabel ?? "All" }}</span>
|
|
281
|
+
<span class="tabular-nums text-muted-foreground">{{ data.length }}</span>
|
|
282
|
+
</DropdownMenuItem>
|
|
283
|
+
<DropdownMenuSeparator />
|
|
284
|
+
<DropdownMenuItem
|
|
285
|
+
v-for="o in d.options"
|
|
286
|
+
:key="o.value"
|
|
287
|
+
class="gap-2"
|
|
288
|
+
@select="setFilter(d.key, o.value)"
|
|
289
|
+
>
|
|
290
|
+
<span class="flex-1">{{ o.label }}</span>
|
|
291
|
+
<span class="tabular-nums text-muted-foreground">{{ dimCounts(d)[o.value] ?? 0 }}</span>
|
|
292
|
+
</DropdownMenuItem>
|
|
293
|
+
</DropdownMenuSubContent>
|
|
294
|
+
</DropdownMenuSub>
|
|
295
|
+
</DropdownMenuContent>
|
|
296
|
+
</DropdownMenu>
|
|
297
|
+
<FilterMenu v-if="filter">
|
|
298
|
+
<slot name="filter-menu" />
|
|
299
|
+
</FilterMenu>
|
|
300
|
+
<ColumnsMenu
|
|
301
|
+
v-if="columnsMenu"
|
|
302
|
+
:columns="menuColumns"
|
|
303
|
+
:visible="visibleKeys"
|
|
304
|
+
@toggle="toggleColumn"
|
|
305
|
+
/>
|
|
306
|
+
<slot name="toolbar-left" />
|
|
307
|
+
</div>
|
|
308
|
+
<div class="flex items-center gap-1.5 ml-auto shrink-0">
|
|
309
|
+
<slot name="actions" />
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<!-- Header row -------------------------------------------------- -->
|
|
314
|
+
<div
|
|
315
|
+
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"
|
|
316
|
+
:style="gridTemplate"
|
|
317
|
+
>
|
|
318
|
+
<div
|
|
319
|
+
v-for="col in visibleColumns"
|
|
320
|
+
:key="col.key"
|
|
321
|
+
:class="col.alignRight ? 'text-right' : ''"
|
|
322
|
+
>
|
|
323
|
+
<SortHeader
|
|
324
|
+
v-if="col.sortable"
|
|
325
|
+
:column="col.key"
|
|
326
|
+
:active="sort.column"
|
|
327
|
+
:direction="sort.direction"
|
|
328
|
+
:align-right="col.alignRight"
|
|
329
|
+
@toggle="toggleSort"
|
|
330
|
+
>
|
|
331
|
+
{{ col.label }}
|
|
332
|
+
</SortHeader>
|
|
333
|
+
<span v-else>{{ col.label }}</span>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<!-- Body --------------------------------------------------------- -->
|
|
338
|
+
<template v-for="entry in renderRows" :key="entry.key">
|
|
339
|
+
<!-- Group header (groupBy mode) -->
|
|
340
|
+
<div
|
|
341
|
+
v-if="entry.kind === 'header'"
|
|
342
|
+
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"
|
|
343
|
+
>
|
|
344
|
+
{{ entry.label }}
|
|
345
|
+
<span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
|
|
346
|
+
</div>
|
|
347
|
+
<div
|
|
348
|
+
v-else
|
|
349
|
+
class="group grid items-center gap-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
|
|
350
|
+
:style="gridTemplate"
|
|
351
|
+
@click="emit('rowClick', entry.row)"
|
|
352
|
+
>
|
|
353
|
+
<div
|
|
354
|
+
v-for="col in visibleColumns"
|
|
355
|
+
:key="col.key"
|
|
356
|
+
class="min-w-0"
|
|
357
|
+
:class="col.alignRight ? 'text-right' : ''"
|
|
358
|
+
>
|
|
359
|
+
<slot :name="`cell-${col.key}`" :row="entry.row">
|
|
360
|
+
<!-- Primary column: bold record anchor + optional tinted icon tile. -->
|
|
361
|
+
<div v-if="col.primary" class="flex items-center gap-2.5 min-w-0">
|
|
362
|
+
<span
|
|
363
|
+
v-if="cellIconFor(col)"
|
|
364
|
+
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"
|
|
365
|
+
>
|
|
366
|
+
<component :is="cellIconFor(col)" :size="13" />
|
|
367
|
+
</span>
|
|
368
|
+
<span class="text-sm font-semibold text-[#0B0522] truncate transition-colors group-hover:text-primary">{{ defaultCell(col, entry.row) }}</span>
|
|
369
|
+
</div>
|
|
370
|
+
<span v-else class="text-sm truncate">{{ defaultCell(col, entry.row) }}</span>
|
|
371
|
+
</slot>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</template>
|
|
375
|
+
|
|
376
|
+
<!-- Loading skeleton (only when there are no rows yet) ----------- -->
|
|
377
|
+
<template v-if="loading && !displayedRows.length">
|
|
378
|
+
<div
|
|
379
|
+
v-for="n in 6"
|
|
380
|
+
:key="`sk-${n}`"
|
|
381
|
+
class="grid items-center gap-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
|
|
382
|
+
:style="gridTemplate"
|
|
383
|
+
>
|
|
384
|
+
<div
|
|
385
|
+
v-for="col in visibleColumns"
|
|
386
|
+
:key="col.key"
|
|
387
|
+
:class="col.alignRight ? 'flex justify-end' : ''"
|
|
388
|
+
>
|
|
389
|
+
<div
|
|
390
|
+
class="h-3 rounded bg-black/[0.07] animate-pulse"
|
|
391
|
+
:class="col.alignRight ? 'w-10' : 'w-2/3'"
|
|
392
|
+
/>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</template>
|
|
396
|
+
|
|
397
|
+
<!-- Empty -------------------------------------------------------- -->
|
|
398
|
+
<div
|
|
399
|
+
v-if="!displayedRows.length && !loading"
|
|
400
|
+
class="px-3 py-8 text-center text-sm text-muted-foreground"
|
|
401
|
+
>
|
|
402
|
+
<slot name="empty">
|
|
403
|
+
{{ emptyText ?? "No entries." }}
|
|
404
|
+
</slot>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<!-- Footer ------------------------------------------------------- -->
|
|
408
|
+
<div
|
|
409
|
+
v-if="showFooter"
|
|
410
|
+
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"
|
|
411
|
+
>
|
|
412
|
+
<div class="flex items-center gap-2.5">
|
|
413
|
+
<span class="tabular-nums">
|
|
414
|
+
<span v-if="totalCount === 0">No results</span>
|
|
415
|
+
<span v-else>Showing {{ rangeStart }}–{{ rangeEnd }} of {{ totalCount }}</span>
|
|
416
|
+
</span>
|
|
417
|
+
<span class="text-muted-foreground/40">-</span>
|
|
418
|
+
<label class="flex items-center gap-1.5">
|
|
419
|
+
<span class="hidden sm:inline">Rows per page</span>
|
|
420
|
+
<select
|
|
421
|
+
v-model.number="pageSize"
|
|
422
|
+
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"
|
|
423
|
+
>
|
|
424
|
+
<option v-for="s in pageSizeOptions" :key="s" :value="s">{{ s }}</option>
|
|
425
|
+
</select>
|
|
426
|
+
</label>
|
|
427
|
+
</div>
|
|
428
|
+
<div v-if="lastPage > 0" class="flex items-center gap-2">
|
|
429
|
+
<Button
|
|
430
|
+
variant="outline"
|
|
431
|
+
size="sm"
|
|
432
|
+
class="h-7 text-sm"
|
|
433
|
+
:disabled="page === 0 || loading"
|
|
434
|
+
@click="prevPage"
|
|
435
|
+
>
|
|
436
|
+
Previous
|
|
437
|
+
</Button>
|
|
438
|
+
<span class="tabular-nums">
|
|
439
|
+
{{ page + 1 }} / {{ lastPage + 1 }}
|
|
440
|
+
</span>
|
|
441
|
+
<Button
|
|
442
|
+
variant="outline"
|
|
443
|
+
size="sm"
|
|
444
|
+
class="h-7 text-sm"
|
|
445
|
+
:disabled="page >= lastPage || loading"
|
|
446
|
+
@click="nextPage"
|
|
447
|
+
>
|
|
448
|
+
Next
|
|
449
|
+
</Button>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DataTableColumn, DataTableFilter, DataTableState, 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 onRowClick?: ((row: T) => any) | undefined;
|
|
7
|
+
readonly onStateChange?: ((state: DataTableState) => any) | undefined;
|
|
8
|
+
} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:sort" | "onUpdate:page" | "onRowClick" | "onStateChange"> & ({
|
|
9
|
+
data: T[];
|
|
10
|
+
columns: DataTableColumn<T>[];
|
|
11
|
+
rowKey: RowKey<T>;
|
|
12
|
+
search?: boolean | {
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
};
|
|
15
|
+
columnsMenu?: boolean;
|
|
16
|
+
filter?: boolean;
|
|
17
|
+
/** Built-in status/dimension filter pills (static mode). One dimension, or
|
|
18
|
+
* several (each renders as its own sub-menu). */
|
|
19
|
+
filterTabs?: DataTableFilter<T> | DataTableFilter<T>[];
|
|
20
|
+
pagination?: boolean | {
|
|
21
|
+
pageSize?: number;
|
|
22
|
+
};
|
|
23
|
+
defaultSort?: {
|
|
24
|
+
column: string;
|
|
25
|
+
direction: "asc" | "desc";
|
|
26
|
+
};
|
|
27
|
+
serverMode?: boolean;
|
|
28
|
+
total?: number;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
emptyText?: string;
|
|
31
|
+
groupBy?: ((row: T) => string[]) | null;
|
|
32
|
+
groupEmptyLabel?: string;
|
|
33
|
+
} & {
|
|
34
|
+
sort?: SortState;
|
|
35
|
+
page?: number;
|
|
36
|
+
}) & Partial<{}>> & import("vue").PublicProps;
|
|
37
|
+
expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
|
|
38
|
+
attrs: any;
|
|
39
|
+
slots: {
|
|
40
|
+
[x: string]: ((props: {
|
|
41
|
+
row: any;
|
|
42
|
+
}) => any) | undefined;
|
|
43
|
+
} & {
|
|
44
|
+
'filter-menu'?: (props: {}) => any;
|
|
45
|
+
} & {
|
|
46
|
+
'toolbar-left'?: (props: {}) => any;
|
|
47
|
+
} & {
|
|
48
|
+
actions?: (props: {}) => any;
|
|
49
|
+
} & {
|
|
50
|
+
empty?: (props: {}) => any;
|
|
51
|
+
};
|
|
52
|
+
emit: (((evt: "rowClick", row: T) => void) & ((evt: "stateChange", state: DataTableState) => void)) & (((evt: "update:sort", value: SortState) => void) & ((evt: "update:page", value: number) => void));
|
|
53
|
+
}>) => import("vue").VNode & {
|
|
54
|
+
__ctx?: Awaited<typeof __VLS_setup>;
|
|
55
|
+
};
|
|
56
|
+
export default _default;
|
|
57
|
+
type __VLS_PrettifyLocal<T> = {
|
|
58
|
+
[K in keyof T]: T[K];
|
|
59
|
+
} & {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare var __VLS_26: {};
|
|
2
|
+
type __VLS_Slots = {} & {
|
|
3
|
+
default?: (props: typeof __VLS_26) => any;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
7
|
+
export default _default;
|
|
8
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
9
|
+
new (): {
|
|
10
|
+
$slots: S;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
<script setup
|
|
2
|
-
import { Filter } from
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Filter } from "#solar-icons";
|
|
3
3
|
import {
|
|
4
4
|
DropdownMenu,
|
|
5
5
|
DropdownMenuContent,
|
|
6
6
|
DropdownMenuItem,
|
|
7
7
|
DropdownMenuLabel,
|
|
8
8
|
DropdownMenuSeparator,
|
|
9
|
-
DropdownMenuTrigger
|
|
10
|
-
} from
|
|
11
|
-
|
|
12
|
-
// Placeholder filter affordance — exposes the icon + dropdown shell so
|
|
13
|
-
// pages can mount custom filter chips inside via the default slot. Wire
|
|
14
|
-
// up actual filter state at the call site.
|
|
9
|
+
DropdownMenuTrigger
|
|
10
|
+
} from "@revenexx/studio";
|
|
15
11
|
</script>
|
|
16
12
|
|
|
17
13
|
<template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare var __VLS_26: {};
|
|
2
|
+
type __VLS_Slots = {} & {
|
|
3
|
+
default?: (props: typeof __VLS_26) => any;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
7
|
+
export default _default;
|
|
8
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
9
|
+
new (): {
|
|
10
|
+
$slots: S;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Component } from 'vue';
|
|
2
|
+
export type Comparable = string | number | boolean | Date | null | undefined;
|
|
3
|
+
export interface DataTableColumn<T = unknown> {
|
|
4
|
+
/** Stable key — matches the slot name `cell-<key>` and is used in the
|
|
5
|
+
* sort state. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Header label. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** CSS grid track. Defaults to `minmax(0,1fr)` for text columns; pass
|
|
10
|
+
* a fixed value like `120px` or `minmax(0,140px)` for compact ones. */
|
|
11
|
+
width?: string;
|
|
12
|
+
/** Enable sort affordance on this column. */
|
|
13
|
+
sortable?: boolean;
|
|
14
|
+
/** Returns the sort key for the row. Falls back to `row[key]` if it
|
|
15
|
+
* looks like a primitive. ISO date strings sort correctly as-is. */
|
|
16
|
+
sortGetter?: (row: T) => Comparable;
|
|
17
|
+
/** Returns the string a text-search query is matched against. Falls
|
|
18
|
+
* back to the stringified sortGetter result. */
|
|
19
|
+
searchGetter?: (row: T) => string;
|
|
20
|
+
/** Initial visibility (default `true`). */
|
|
21
|
+
visible?: boolean;
|
|
22
|
+
/** Hide-able via the column menu (default `true`). */
|
|
23
|
+
toggleable?: boolean;
|
|
24
|
+
/** Right-align cell content (numeric columns). */
|
|
25
|
+
alignRight?: boolean;
|
|
26
|
+
/** Optional icon component for the header. */
|
|
27
|
+
icon?: Component;
|
|
28
|
+
/** Marks the identifying column. When set (and no `#cell-<key>` slot is
|
|
29
|
+
* given) the DataTable renders the value as the bold record anchor —
|
|
30
|
+
* `font-semibold text-[#0B0522]`, truncated — optionally led by a tinted
|
|
31
|
+
* icon tile (see `cellIcon`). One per table. (datatable-ux) */
|
|
32
|
+
primary?: boolean;
|
|
33
|
+
/** Leading tinted icon tile for the cell (typically the `primary` column).
|
|
34
|
+
* A static icon component, rendered as the `bg-primary/10` rounded tile the
|
|
35
|
+
* scanning rules call for. (For a per-row icon use a `#cell-<key>` slot —
|
|
36
|
+
* `cellIcon` must NOT be a getter: Solar icons are functional components, so
|
|
37
|
+
* the table can't tell a getter from an icon and would crash invoking it.) */
|
|
38
|
+
cellIcon?: Component;
|
|
39
|
+
}
|
|
40
|
+
/** A built-in filter dimension rendered under the generic "Filter" dropdown as
|
|
41
|
+
* a labelled sub-menu (datatable-ux). Static mode only: the DataTable filters
|
|
42
|
+
* `data` in-memory and shows a count per option. Pass one, or an array of
|
|
43
|
+
* several dimensions — each becomes its own sub-menu. */
|
|
44
|
+
export interface DataTableFilter<T = unknown> {
|
|
45
|
+
/** Stable id for this dimension (defaults to `label`). */
|
|
46
|
+
key?: string;
|
|
47
|
+
/** Sub-menu title, e.g. "Status" (default `Status`). */
|
|
48
|
+
label?: string;
|
|
49
|
+
/** The row's value for this dimension (e.g. `r => r.status`). */
|
|
50
|
+
getter: (row: T) => string | null | undefined;
|
|
51
|
+
/** The selectable values + their labels. */
|
|
52
|
+
options: {
|
|
53
|
+
value: string;
|
|
54
|
+
label: string;
|
|
55
|
+
}[];
|
|
56
|
+
/** Label for the reset/"all" entry (default `All`). */
|
|
57
|
+
allLabel?: string;
|
|
58
|
+
}
|
|
59
|
+
export type SortDirection = 'asc' | 'desc' | null;
|
|
60
|
+
export interface SortState {
|
|
61
|
+
column: string | null;
|
|
62
|
+
direction: SortDirection;
|
|
63
|
+
}
|
|
64
|
+
export interface DataTableState {
|
|
65
|
+
search: string;
|
|
66
|
+
sort: SortState;
|
|
67
|
+
page: number;
|
|
68
|
+
pageSize: number;
|
|
69
|
+
}
|
|
70
|
+
export type RowKey<T> = keyof T | ((row: T) => string | number);
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal shape of a site deployment — just the fields this flyout renders.
|
|
3
|
+
* Kept local so studio-shared has no dependency on the private SDK; a full
|
|
4
|
+
* SDK `Models.Deployment` is structurally assignable to it.
|
|
5
|
+
*/
|
|
6
|
+
interface BuildDeployment {
|
|
7
|
+
$id?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
buildLogs?: string;
|
|
10
|
+
buildDuration?: number;
|
|
11
|
+
providerBranch?: string;
|
|
12
|
+
providerCommitHash?: string;
|
|
13
|
+
providerCommitUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
type __VLS_Props = {
|
|
16
|
+
open: boolean;
|
|
17
|
+
deployment: BuildDeployment | null;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
isBuilding?: boolean;
|
|
20
|
+
userPinned?: boolean;
|
|
21
|
+
error?: string | null;
|
|
22
|
+
};
|
|
23
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
24
|
+
"update:open": (args_0: boolean) => any;
|
|
25
|
+
refresh: () => any;
|
|
26
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
27
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
28
|
+
onRefresh?: (() => any) | undefined;
|
|
29
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
30
|
+
export default _default;
|