@stacksjs/defaults 0.75.263 → 0.75.267
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/ai/skills/stacks-browse/SKILL.md +3 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +73 -14
- package/ai/skills/stacks-chat/SKILL.md +32 -0
- package/ai/skills/stacks-native/CATALOG.md +8 -8
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/App/RequestsOverview.stx +75 -58
- package/resources/components/Dashboard/App/SourceInventory.stx +68 -59
- package/resources/components/Dashboard/Auth/AccessTokens.stx +57 -37
- package/resources/components/Dashboard/Billing/BillingTransactionHistory.stx +27 -33
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +85 -49
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +63 -66
- package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +67 -23
- package/resources/components/Dashboard/Content/BlogDashboard.stx +73 -67
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +39 -12
- package/resources/components/Dashboard/Data/DataRecords.stx +85 -88
- package/resources/components/Dashboard/Deployments/DeploymentTable.stx +49 -64
- package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +46 -28
- package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +72 -61
- package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +74 -70
- package/resources/components/Dashboard/Queue/QueueDashboard.stx +27 -61
- package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +35 -61
- package/resources/components/Dashboard/Teams/TeamPeopleDashboard.stx +127 -143
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { TaxRateRecord } from '../../../../app/Actions/Dashboard/Commerce/tax-rate-records'
|
|
3
5
|
|
|
@@ -5,72 +7,67 @@ const records = useReactiveProp('records', [] as TaxRateRecord[])
|
|
|
5
7
|
const loading = useReactiveProp('loading', false)
|
|
6
8
|
const emit = defineEmits()
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The name cell is slotted: the default, code and exemptible badges sit
|
|
12
|
+
* beside the name because each changes what a customer is charged, and the
|
|
13
|
+
* code is what application code matches on, so renaming the rate must not
|
|
14
|
+
* change the bill. Everything else is a column option.
|
|
15
|
+
*/
|
|
16
|
+
const COLUMNS = [
|
|
17
|
+
{ key: 'name', label: 'Tax rate', sortable: true, slotted: true },
|
|
18
|
+
{ key: 'rateLabel', label: 'Rate', sortable: true, searchable: false, class: 'font-medium text-gray-900 whitespace-nowrap dark:text-white' },
|
|
19
|
+
{ key: 'country', label: 'Location', sortable: true, subtitleKey: 'region' },
|
|
20
|
+
{
|
|
21
|
+
key: 'status',
|
|
22
|
+
label: 'Status',
|
|
23
|
+
type: 'badge' as const,
|
|
24
|
+
sortable: true,
|
|
25
|
+
badgeClasses: { active: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40' },
|
|
26
|
+
badgeLabels: { active: 'Active', inactive: 'Inactive' },
|
|
27
|
+
},
|
|
28
|
+
{ key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, class: 'whitespace-nowrap' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const ROW_ACTIONS = [
|
|
32
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
33
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const rows = derived(() => records().map(record => ({
|
|
37
|
+
...record,
|
|
38
|
+
rateLabel: `${record.rate}%`,
|
|
39
|
+
// Anything but active reads as inactive, as the hand-rolled pill did.
|
|
40
|
+
status: record.status === 'active' ? 'active' : 'inactive',
|
|
41
|
+
})))
|
|
42
|
+
|
|
43
|
+
function handleRowAction(payload: { action: string, row: TaxRateRecord }): void {
|
|
44
|
+
emit(payload.action, payload.row)
|
|
13
45
|
}
|
|
14
46
|
</script>
|
|
15
47
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<div class="flex flex-wrap gap-2 items-center">
|
|
43
|
-
<p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.name }}</p>
|
|
44
|
-
<span :if="record.isDefault" class="px-2 py-0.5 font-medium text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-full">Default</span>
|
|
45
|
-
{{--
|
|
46
|
-
Both badges change what a customer is charged, so they sit
|
|
47
|
-
next to the name rather than in a column someone has to
|
|
48
|
-
scroll to. The code is what application code matches on;
|
|
49
|
-
renaming the rate must not change the bill.
|
|
50
|
-
--}}
|
|
51
|
-
<span :if="record.code" class="px-2 py-0.5 font-mono text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record.code }}</span>
|
|
52
|
-
<span :if="record.exemptible" class="px-2 py-0.5 font-medium text-amber-700 text-xs dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40 rounded-full" x-tooltip="Not charged to a customer who qualifies for an exemption" title="Not charged to a customer who qualifies for an exemption">Exemptible</span>
|
|
53
|
-
</div>
|
|
54
|
-
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ record.type }}</p>
|
|
55
|
-
</td>
|
|
56
|
-
<td class="px-4 py-4 font-medium text-gray-900 text-sm whitespace-nowrap dark:text-white">{{ record.rate }}%</td>
|
|
57
|
-
<td class="px-4 py-4">
|
|
58
|
-
<p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.country }}</p>
|
|
59
|
-
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ record.region }}</p>
|
|
60
|
-
</td>
|
|
61
|
-
<td class="px-4 py-4">
|
|
62
|
-
<span :class="'inline-flex px-2 py-1 font-medium text-xs rounded-full ' + (record.status === 'active' ? 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40' : 'text-gray-600 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700')">{{ record.status === 'active' ? 'Active' : 'Inactive' }}</span>
|
|
63
|
-
</td>
|
|
64
|
-
<td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatDate(record.createdAt) }}</td>
|
|
65
|
-
<td class="px-4 py-4">
|
|
66
|
-
<div class="flex gap-1 justify-end">
|
|
67
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.name" @click="emit('edit', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span></Button>
|
|
68
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.name" @click="emit('delete', record)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button>
|
|
69
|
-
</div>
|
|
70
|
-
</td>
|
|
71
|
-
</tr>
|
|
72
|
-
</template>
|
|
73
|
-
</tbody>
|
|
74
|
-
</table>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
48
|
+
{{--
|
|
49
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box
|
|
50
|
+
and the filter selects. Paging is what was missing, and Table brings it.
|
|
51
|
+
--}}
|
|
52
|
+
<Table
|
|
53
|
+
:columns="COLUMNS"
|
|
54
|
+
:data="rows()"
|
|
55
|
+
:loading="loading()"
|
|
56
|
+
:row-actions="ROW_ACTIONS"
|
|
57
|
+
action-label-key="name"
|
|
58
|
+
paginated
|
|
59
|
+
:page-size="25"
|
|
60
|
+
empty-title="No tax rates found"
|
|
61
|
+
empty-description="Adjust the filters or add a TaxRate record."
|
|
62
|
+
@row-action="handleRowAction"
|
|
63
|
+
>
|
|
64
|
+
<template #cell="{ row }">
|
|
65
|
+
<div class="flex flex-wrap gap-2 items-center">
|
|
66
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.name"></p>
|
|
67
|
+
<span :if="row.isDefault" class="px-2 py-0.5 font-medium text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-full">Default</span>
|
|
68
|
+
<span :if="row.code" class="px-2 py-0.5 font-mono text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded-full" x-text="row.code"></span>
|
|
69
|
+
<span :if="row.exemptible" class="px-2 py-0.5 font-medium text-amber-700 text-xs dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40 rounded-full" x-tooltip="Not charged to a customer who qualifies for an exemption" title="Not charged to a customer who qualifies for an exemption">Exemptible</span>
|
|
70
|
+
</div>
|
|
71
|
+
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400" x-text="row.type"></p>
|
|
72
|
+
</template>
|
|
73
|
+
</Table>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { ProductVariantRecord } from '../../../../app/Actions/Dashboard/Commerce/product-variant-records'
|
|
3
5
|
|
|
@@ -5,30 +7,72 @@ const records = useReactiveProp('records', [] as ProductVariantRecord[])
|
|
|
5
7
|
const loading = useReactiveProp('loading', false)
|
|
6
8
|
const emit = defineEmits()
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Two slotted columns, dispatched on `column.key`: the variant with its type
|
|
12
|
+
* and description beneath it, and the option pills, of which the first four
|
|
13
|
+
* show and the rest are counted.
|
|
14
|
+
*/
|
|
15
|
+
const COLUMNS = [
|
|
16
|
+
{ key: 'variant', label: 'Variant', sortable: true, slotted: true },
|
|
17
|
+
{ key: 'productName', label: 'Product', sortable: true, emptyText: 'Unassigned' },
|
|
18
|
+
{ key: 'optionsLabel', label: 'Options', slotted: true, class: 'max-w-sm' },
|
|
19
|
+
{
|
|
20
|
+
key: 'status',
|
|
21
|
+
label: 'Status',
|
|
22
|
+
type: 'badge' as const,
|
|
23
|
+
sortable: true,
|
|
24
|
+
badgeClasses: {
|
|
25
|
+
active: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40',
|
|
26
|
+
draft: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, class: 'whitespace-nowrap' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const ROW_ACTIONS = [
|
|
33
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
34
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const rows = derived(() => records().map(record => ({
|
|
38
|
+
...record,
|
|
39
|
+
// Searchable text for the options, and the parts the cell draws.
|
|
40
|
+
optionsLabel: record.options.join(', '),
|
|
41
|
+
shownOptions: record.options.slice(0, 4),
|
|
42
|
+
moreOptions: record.options.length > 4 ? `+${record.options.length - 4}` : '',
|
|
43
|
+
})))
|
|
44
|
+
|
|
45
|
+
function handleRowAction(payload: { action: string, row: ProductVariantRecord }): void {
|
|
46
|
+
emit(payload.action, payload.row)
|
|
11
47
|
}
|
|
12
48
|
</script>
|
|
13
49
|
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
<Table
|
|
51
|
+
:columns="COLUMNS"
|
|
52
|
+
:data="rows()"
|
|
53
|
+
:loading="loading()"
|
|
54
|
+
:row-actions="ROW_ACTIONS"
|
|
55
|
+
action-label-key="variant"
|
|
56
|
+
paginated
|
|
57
|
+
:page-size="25"
|
|
58
|
+
empty-title="No product variants found"
|
|
59
|
+
empty-description="Adjust the filters or add a ProductVariant record."
|
|
60
|
+
@row-action="handleRowAction"
|
|
61
|
+
>
|
|
62
|
+
<template #cell="{ row, column }">
|
|
63
|
+
<template :if="column.key === 'variant'">
|
|
64
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.variant"></p>
|
|
65
|
+
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400" x-text="row.type"></p>
|
|
66
|
+
<p class="mt-2 max-w-sm text-gray-500 text-xs dark:text-neutral-400" x-text="row.description"></p>
|
|
67
|
+
</template>
|
|
68
|
+
<template :else>
|
|
69
|
+
<div class="flex flex-wrap gap-1">
|
|
70
|
+
<template :for="option in row.shownOptions" :key="option">
|
|
71
|
+
<span class="px-2 py-0.5 text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-full" x-text="option"></span>
|
|
30
72
|
</template>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</
|
|
73
|
+
<span :if="row.moreOptions" class="px-2 py-0.5 text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded-full" x-text="row.moreOptions"></span>
|
|
74
|
+
<span :if="row.shownOptions.length === 0" class="text-gray-400 text-xs">No options</span>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
</template>
|
|
78
|
+
</Table>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script server>
|
|
2
4
|
import { publicApplicationUrl } from '../../../../functions/public-application-url'
|
|
3
5
|
|
|
@@ -102,57 +104,47 @@ defineClientPayload({ blogUrl })
|
|
|
102
104
|
</div>
|
|
103
105
|
|
|
104
106
|
<div x-show="!isLoading() && !loadError() && filteredPosts().length > 0" class="overflow-x-auto max-w-full w-full">
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</Button>
|
|
147
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + post.title" @click="askDelete(post)">
|
|
148
|
-
<span class="block h-4 w-4 text-red-600 dark:text-red-400 i-hugeicons-waste"></span>
|
|
149
|
-
</Button>
|
|
150
|
-
</div>
|
|
151
|
-
</td>
|
|
152
|
-
</tr>
|
|
153
|
-
</template>
|
|
154
|
-
</tbody>
|
|
155
|
-
</table>
|
|
107
|
+
{{--
|
|
108
|
+
The page owns the search, the status filter and the loading, error
|
|
109
|
+
and empty states, so the table brings only sorting and paging. The
|
|
110
|
+
post is slotted for its featured mark; the actions are a slot for the
|
|
111
|
+
view link, which only a published post has.
|
|
112
|
+
--}}
|
|
113
|
+
<Table
|
|
114
|
+
:columns="POST_COLUMNS"
|
|
115
|
+
:data="postRows()"
|
|
116
|
+
row-key="slug"
|
|
117
|
+
paginated
|
|
118
|
+
:page-size="25"
|
|
119
|
+
>
|
|
120
|
+
<template #cell="{ row }">
|
|
121
|
+
<div class="flex gap-2 items-center">
|
|
122
|
+
<span class="font-medium" x-text="row.title"></span>
|
|
123
|
+
<span :if="row.featured" class="inline-flex px-1.5 py-0.5 font-medium text-[11px] text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/40 rounded">Featured</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="mt-1 max-w-xl text-neutral-500 text-xs truncate dark:text-neutral-400" x-text="row.summary"></div>
|
|
126
|
+
</template>
|
|
127
|
+
<template #actions="{ row }">
|
|
128
|
+
<div class="flex gap-1 justify-end">
|
|
129
|
+
<a
|
|
130
|
+
:if="!row.draft"
|
|
131
|
+
:href="row.publicUrl"
|
|
132
|
+
target="_blank"
|
|
133
|
+
rel="noopener"
|
|
134
|
+
:title="'View ' + row.title"
|
|
135
|
+
class="p-2 text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded"
|
|
136
|
+
>
|
|
137
|
+
<span class="block h-4 w-4 i-hugeicons-view"></span>
|
|
138
|
+
</a>
|
|
139
|
+
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + row.title" @click="openEditor(row.post)">
|
|
140
|
+
<span class="block h-4 w-4 i-hugeicons-edit-01"></span>
|
|
141
|
+
</Button>
|
|
142
|
+
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + row.title" @click="askDelete(row.post)">
|
|
143
|
+
<span class="block h-4 w-4 text-red-600 dark:text-red-400 i-hugeicons-waste"></span>
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|
|
147
|
+
</Table>
|
|
156
148
|
</div>
|
|
157
149
|
|
|
158
150
|
<!-- Empty: distinguishes "no posts yet" from "nothing matched the filter". -->
|
|
@@ -272,22 +264,36 @@ const filteredPosts = derived(() => {
|
|
|
272
264
|
})
|
|
273
265
|
})
|
|
274
266
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
: '
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
267
|
+
const POST_COLUMNS = [
|
|
268
|
+
{ key: 'title', label: 'Post', sortable: true, slotted: true },
|
|
269
|
+
{
|
|
270
|
+
key: 'status',
|
|
271
|
+
label: 'Status',
|
|
272
|
+
type: 'badge' as const,
|
|
273
|
+
sortable: true,
|
|
274
|
+
badgeClasses: {
|
|
275
|
+
draft: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200',
|
|
276
|
+
published: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-200',
|
|
277
|
+
},
|
|
278
|
+
badgeLabels: { draft: 'Draft', published: 'Published' },
|
|
279
|
+
},
|
|
280
|
+
{ key: 'date', label: 'Date', type: 'date' as const, sortable: true, emptyText: 'No date', class: 'text-neutral-600 dark:text-neutral-300' },
|
|
281
|
+
{ key: 'lengthLabel', label: 'Length', searchable: false, class: 'text-neutral-600 dark:text-neutral-300' },
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
/** What the table reads; `post` is the record the edit and delete actions take. */
|
|
285
|
+
const postRows = derived(() => filteredPosts().map(post => ({
|
|
286
|
+
slug: post.slug,
|
|
287
|
+
title: post.title,
|
|
288
|
+
featured: post.featured,
|
|
289
|
+
draft: post.draft,
|
|
290
|
+
summary: post.description || post.slug,
|
|
291
|
+
status: post.draft ? 'draft' : 'published',
|
|
292
|
+
date: post.date,
|
|
293
|
+
lengthLabel: `${post.readingTime} min read`,
|
|
294
|
+
publicUrl: publicPostUrl(post.slug),
|
|
295
|
+
post,
|
|
296
|
+
})))
|
|
291
297
|
|
|
292
298
|
function clearFilters() {
|
|
293
299
|
searchQuery.set('')
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
5
|
import { pushToast } from '../../../../functions/toasts'
|
|
@@ -214,14 +216,22 @@ function formatFileSize(bytes: number | null): string {
|
|
|
214
216
|
return `${Number.parseFloat((bytes / 1024 ** index).toFixed(2))} ${units[index]}`
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const date
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
const FILE_COLUMNS = [
|
|
220
|
+
{ key: 'name', label: 'Name', sortable: true, slotted: true },
|
|
221
|
+
{ key: 'size', label: 'Size', sortable: true, slotted: true, searchable: false },
|
|
222
|
+
{ key: 'lastModified', label: 'Modified', type: 'date' as const, sortable: true, emptyText: 'No date', class: 'text-sm text-stone-500 dark:text-neutral-400' },
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
/** What the list view's table reads; `item` is what the click and delete handlers take. */
|
|
226
|
+
const fileRows = derived(() => filteredItems().map(item => ({
|
|
227
|
+
id: item.id,
|
|
228
|
+
name: item.name,
|
|
229
|
+
size: item.type === 'folder' ? -1 : item.size,
|
|
230
|
+
sizeLabel: item.type === 'folder' ? '-' : formatFileSize(item.size),
|
|
231
|
+
lastModified: item.lastModified,
|
|
232
|
+
iconClass: itemIconClass(item),
|
|
233
|
+
item,
|
|
234
|
+
})))
|
|
225
235
|
|
|
226
236
|
async function loadFiles(options: { resetPath?: boolean } = {}): Promise<void> {
|
|
227
237
|
loading.set(true)
|
|
@@ -505,10 +515,27 @@ onMount(() => { void loadFiles() })
|
|
|
505
515
|
</template>
|
|
506
516
|
</div>
|
|
507
517
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
518
|
+
{{--
|
|
519
|
+
The list view. The page owns search, the folder path and the loading
|
|
520
|
+
and empty states; the table brings sorting and paging, which a large
|
|
521
|
+
folder had none of. Name is slotted for its icon and the click that
|
|
522
|
+
opens a folder, and size is slotted so it reads as a size while
|
|
523
|
+
sorting on the byte count underneath.
|
|
524
|
+
--}}
|
|
525
|
+
<div :else>
|
|
526
|
+
<Table :columns="FILE_COLUMNS" :data="fileRows()" paginated :page-size="50" empty-title="No items" empty-description="Nothing to show in this folder.">
|
|
527
|
+
<template #cell="{ row, column }">
|
|
528
|
+
<template :if="column.key === 'name'">
|
|
529
|
+
<button type="button" class="flex items-center max-w-md text-left" @click="onItemClick(row.item)"><span :class="row.iconClass"></span><span class="font-medium text-sm text-stone-900 truncate dark:text-white" x-text="row.name"></span></button>
|
|
530
|
+
</template>
|
|
531
|
+
<template :else>
|
|
532
|
+
<span class="text-sm text-stone-500 dark:text-neutral-400" x-text="row.sizeLabel"></span>
|
|
533
|
+
</template>
|
|
534
|
+
</template>
|
|
535
|
+
<template #actions="{ row }">
|
|
536
|
+
<div class="text-right"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + row.name" @click="openDelete(row.item, $event)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div>
|
|
537
|
+
</template>
|
|
538
|
+
</Table>
|
|
512
539
|
</div>
|
|
513
540
|
</div>
|
|
514
541
|
</div>
|