@stacksjs/defaults 0.70.362 → 0.70.365
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 +2 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +20 -2
- package/ai/skills/stacks-dashboard/SKILL.md +4 -0
- package/app/Actions/Auth/LogoutAction.ts +11 -4
- package/app/Actions/Auth/LogoutAllAction.ts +8 -4
- package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
- package/app/Actions/Dashboard/Models/model-write.ts +5 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Buttons/Counter.stx +1 -1
- package/resources/components/Buttons/ToggleDark.stx +1 -1
- package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
- package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
- package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
- package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
- package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
- package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
- package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
- package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
- package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
- package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
- package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
- package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
- package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
- package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
- package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
- package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
- package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
- package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
- package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
- package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
- package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
- package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
- package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
- package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
- package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
- package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
- package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
- package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
- package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
- package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
- package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
- package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
- package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
- package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
- package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
- package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
- package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
- package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
- package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
- package/resources/components/Dashboard/GlobalSearch.stx +3 -9
- package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
- package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
- package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
- package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
- package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
- package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
- package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
- package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
- package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
- package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
- package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
- package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
- package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
- package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
- package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
- package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
- package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
- package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
- package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
- package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
- package/resources/components/Dashboard/Navbar.stx +5 -5
- package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
- package/resources/components/Dashboard/UI/Button.stx +4 -0
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +4 -0
- package/resources/components/Dashboard/UI/Drawer.stx +114 -0
- package/resources/components/Dashboard/UI/Modal.stx +19 -12
- package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
- package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
- package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
- package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
- package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
- package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
- package/resources/components/Docs/TeamMember.stx +16 -9
- package/resources/components/Forum/ForumReplyForm.stx +7 -7
- package/resources/components/Forum/ForumSidebar.stx +10 -6
- package/resources/components/Marketing/Faqs.stx +5 -5
- package/resources/components/Marketing/Feature.stx +11 -6
- package/resources/components/Marketing/Layout.stx +12 -8
- package/resources/components/Marketing/Pricing.stx +11 -5
- package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
- package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
- package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
- package/resources/components/Marketing/Testimonials.stx +8 -5
- package/resources/views/index.stx +4 -4
- package/views/system-tray/index.stx +1 -1
|
@@ -116,6 +116,10 @@ function closeDelete(): void {
|
|
|
116
116
|
deletingComment.set(null)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function deleteDescription(): string {
|
|
120
|
+
return `The comment from ${deletingComment()?.authorName || 'Anonymous'} cannot be restored.`
|
|
121
|
+
}
|
|
122
|
+
|
|
119
123
|
async function deleteComment(): Promise<void> {
|
|
120
124
|
const comment = deletingComment()
|
|
121
125
|
if (!comment)
|
|
@@ -166,7 +170,13 @@ onMount(() => {
|
|
|
166
170
|
</section>
|
|
167
171
|
</div>
|
|
168
172
|
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
<ConfirmDialog
|
|
174
|
+
:isOpen="Boolean(deletingComment())"
|
|
175
|
+
title="Permanently delete comment?"
|
|
176
|
+
:description="deleteDescription()"
|
|
177
|
+
confirmLabel="Delete permanently"
|
|
178
|
+
:busy="deleting()"
|
|
179
|
+
:error="deleteError()"
|
|
180
|
+
@close="closeDelete"
|
|
181
|
+
@confirm="deleteComment"
|
|
182
|
+
/>
|
|
@@ -11,6 +11,12 @@ interface TaxonomyRecord {
|
|
|
11
11
|
postCount: number
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
interface TaxonomyDraft {
|
|
15
|
+
name: string
|
|
16
|
+
slug: string
|
|
17
|
+
description: string
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
const resource = useReactiveProp('resource', 'categories')
|
|
15
21
|
const title = useReactiveProp('title', 'Categories')
|
|
16
22
|
const description = useReactiveProp('description', '')
|
|
@@ -19,10 +25,6 @@ const loading = state(true)
|
|
|
19
25
|
const loadError = state('')
|
|
20
26
|
const search = state('')
|
|
21
27
|
const dialogOpen = state(false)
|
|
22
|
-
const name = state('')
|
|
23
|
-
const slug = state('')
|
|
24
|
-
const taxonomyDescription = state('')
|
|
25
|
-
const slugTouched = state(false)
|
|
26
28
|
const saving = state(false)
|
|
27
29
|
const saveError = state('')
|
|
28
30
|
const deletingRecord = state<TaxonomyRecord | null>(null)
|
|
@@ -63,33 +65,6 @@ function secondaryMetric(): number {
|
|
|
63
65
|
: records().filter(record => record.active).length
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function slugify(value: string): string {
|
|
67
|
-
return value
|
|
68
|
-
.normalize('NFKD')
|
|
69
|
-
.replace(/[\u0300-\u036F]/g, '')
|
|
70
|
-
.trim()
|
|
71
|
-
.toLowerCase()
|
|
72
|
-
.replace(/['"]/g, '')
|
|
73
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
74
|
-
.replace(/^-+|-+$/g, '')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
effect(() => {
|
|
78
|
-
if (!dialogOpen() || slugTouched())
|
|
79
|
-
return
|
|
80
|
-
slug.set(slugify(name()))
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
function markSlugTouched(): void {
|
|
84
|
-
slugTouched.set(true)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function normalizeSlug(): void {
|
|
88
|
-
const normalized = slugify(slug())
|
|
89
|
-
if (normalized !== slug())
|
|
90
|
-
slug.set(normalized)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
68
|
async function loadRecords(): Promise<void> {
|
|
94
69
|
loading.set(true)
|
|
95
70
|
loadError.set('')
|
|
@@ -110,10 +85,6 @@ async function loadRecords(): Promise<void> {
|
|
|
110
85
|
}
|
|
111
86
|
|
|
112
87
|
function openCreate(): void {
|
|
113
|
-
name.set('')
|
|
114
|
-
slug.set('')
|
|
115
|
-
taxonomyDescription.set('')
|
|
116
|
-
slugTouched.set(false)
|
|
117
88
|
saveError.set('')
|
|
118
89
|
dialogOpen.set(true)
|
|
119
90
|
}
|
|
@@ -123,17 +94,13 @@ function closeCreate(): void {
|
|
|
123
94
|
dialogOpen.set(false)
|
|
124
95
|
}
|
|
125
96
|
|
|
126
|
-
async function createRecord(): Promise<void> {
|
|
97
|
+
async function createRecord(draft: TaxonomyDraft): Promise<void> {
|
|
127
98
|
saving.set(true)
|
|
128
99
|
saveError.set('')
|
|
129
100
|
try {
|
|
130
101
|
await dashboardApi(`/api/dashboard/${resource()}`, {
|
|
131
102
|
method: 'POST',
|
|
132
|
-
body:
|
|
133
|
-
name: name().trim(),
|
|
134
|
-
slug: slugify(slug()),
|
|
135
|
-
description: taxonomyDescription().trim(),
|
|
136
|
-
},
|
|
103
|
+
body: draft,
|
|
137
104
|
})
|
|
138
105
|
dialogOpen.set(false)
|
|
139
106
|
pushToast('success', `${singular().replace(/^\w/, letter => letter.toUpperCase())} created`)
|
|
@@ -157,6 +124,19 @@ function closeDelete(): void {
|
|
|
157
124
|
deletingRecord.set(null)
|
|
158
125
|
}
|
|
159
126
|
|
|
127
|
+
function deleteTitle(): string {
|
|
128
|
+
return `Delete ${singular()}?`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function deleteDescription(): string {
|
|
132
|
+
const record = deletingRecord()
|
|
133
|
+
return record ? `${record.name} will be permanently removed.` : ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function deleteLabel(): string {
|
|
137
|
+
return `Delete ${singular()}`
|
|
138
|
+
}
|
|
139
|
+
|
|
160
140
|
async function deleteRecord(): Promise<void> {
|
|
161
141
|
const record = deletingRecord()
|
|
162
142
|
if (!record)
|
|
@@ -218,17 +198,22 @@ onMount(() => {
|
|
|
218
198
|
</section>
|
|
219
199
|
</div>
|
|
220
200
|
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
201
|
+
<TaxonomyEditorDialog
|
|
202
|
+
:open="dialogOpen()"
|
|
203
|
+
:kind="singular()"
|
|
204
|
+
:busy="saving()"
|
|
205
|
+
:error="saveError()"
|
|
206
|
+
@close="closeCreate"
|
|
207
|
+
@submit="createRecord"
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<ConfirmDialog
|
|
211
|
+
:isOpen="Boolean(deletingRecord())"
|
|
212
|
+
:title="deleteTitle()"
|
|
213
|
+
:description="deleteDescription()"
|
|
214
|
+
:confirmLabel="deleteLabel()"
|
|
215
|
+
:busy="deleting()"
|
|
216
|
+
:error="deleteError()"
|
|
217
|
+
@close="closeDelete"
|
|
218
|
+
@confirm="deleteRecord"
|
|
219
|
+
/>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
const open = useReactiveProp('open', false)
|
|
3
|
+
const location = useReactiveProp('location', 'Storage root')
|
|
4
|
+
const busy = useReactiveProp('busy', false)
|
|
5
|
+
const error = useReactiveProp('error', '')
|
|
6
|
+
const folderName = state('')
|
|
7
|
+
const emit = defineEmits()
|
|
8
|
+
let wasOpen = false
|
|
9
|
+
|
|
10
|
+
effect(() => {
|
|
11
|
+
const isOpen = open()
|
|
12
|
+
if (isOpen && !wasOpen)
|
|
13
|
+
folderName.set('')
|
|
14
|
+
wasOpen = isOpen
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function close(): void {
|
|
18
|
+
if (!busy())
|
|
19
|
+
emit('close')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function submit(): void {
|
|
23
|
+
const name = folderName().trim()
|
|
24
|
+
if (name)
|
|
25
|
+
emit('submit', name)
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<Modal :isOpen="open()" title="Create folder" :description="'Add a folder inside ' + location() + '.'" size="sm" :closable="!busy()" @close="close">
|
|
30
|
+
<form id="create-folder-form" @submit.prevent="submit">
|
|
31
|
+
<label class="block">
|
|
32
|
+
<span class="font-medium text-stone-700 text-sm dark:text-neutral-200">Folder name</span>
|
|
33
|
+
<input x-model.trim="folderName" name="name" required maxlength="255" autocomplete="off" class="mt-1 px-3 py-2 w-full text-stone-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-stone-300 rounded-lg dark:border-neutral-700" />
|
|
34
|
+
</label>
|
|
35
|
+
</form>
|
|
36
|
+
|
|
37
|
+
<div :if="error()" role="alert" class="mt-4 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ error() }}</div>
|
|
38
|
+
|
|
39
|
+
<template #footer>
|
|
40
|
+
<div class="flex gap-2 justify-end w-full">
|
|
41
|
+
<Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
|
|
42
|
+
<Button type="submit" form="create-folder-form" :disabled="!folderName().trim()" :loading="busy()">Create folder</Button>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
</Modal>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
interface FileItem {
|
|
3
|
+
id: string
|
|
4
|
+
name: string
|
|
5
|
+
type: string
|
|
6
|
+
size: number
|
|
7
|
+
path: string
|
|
8
|
+
lastModified: string | null
|
|
9
|
+
mime_type?: string
|
|
10
|
+
url?: string
|
|
11
|
+
thumbnail?: string
|
|
12
|
+
starred: false
|
|
13
|
+
shared: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const item = useReactiveProp('item', null as FileItem | null)
|
|
17
|
+
const disk = useReactiveProp('disk', '')
|
|
18
|
+
const title = derived(() => item()?.name || 'File details')
|
|
19
|
+
const description = derived(() => `File details · ${disk()} disk`)
|
|
20
|
+
const emit = defineEmits()
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<Drawer :isOpen="Boolean(item())" :title="title()" :description="description()" size="sm" @close="emit('close')">
|
|
24
|
+
<div class="p-5">
|
|
25
|
+
<MediaInfoPanel :item="item()" />
|
|
26
|
+
<Button :if="item()?.url" tag="a" :href="item()?.url" target="_blank" rel="noopener" fullWidth class="mt-5">
|
|
27
|
+
<span class="h-4 w-4 i-hugeicons-link-square-02"></span>
|
|
28
|
+
Open file
|
|
29
|
+
</Button>
|
|
30
|
+
</div>
|
|
31
|
+
</Drawer>
|
|
@@ -82,20 +82,15 @@ const scanLimit = state(1000)
|
|
|
82
82
|
|
|
83
83
|
const selectedItem = state<FileItem | null>(null)
|
|
84
84
|
const showCreateFolderModal = state(false)
|
|
85
|
-
const newFolderName = state('')
|
|
86
85
|
const creatingFolder = state(false)
|
|
87
86
|
const createError = state('')
|
|
88
87
|
const showUploadModal = state(false)
|
|
89
|
-
const selectedFiles = state<File[]>([])
|
|
90
88
|
const uploading = state(false)
|
|
91
89
|
const uploadError = state('')
|
|
92
90
|
const itemToDelete = state<FileItem | null>(null)
|
|
93
91
|
const deleting = state(false)
|
|
94
92
|
const deleteError = state('')
|
|
95
93
|
|
|
96
|
-
const folderNameInput = useRef<HTMLInputElement>('fileManagerFolderName')
|
|
97
|
-
const uploadInput = useRef<HTMLInputElement>('fileManagerUploadInput')
|
|
98
|
-
|
|
99
94
|
const fileTypeIcons: Record<string, string> = {
|
|
100
95
|
pdf: 'i-hugeicons-pdf-01',
|
|
101
96
|
doc: 'i-hugeicons-doc-01',
|
|
@@ -176,14 +171,6 @@ const volumePercent = derived(() => {
|
|
|
176
171
|
const capacityKnown = derived(() => stats().disk.totalBytes !== null && stats().disk.availableBytes !== null)
|
|
177
172
|
const currentFolderLabel = derived(() => currentFolderPath() || 'Storage root')
|
|
178
173
|
const emptyHint = derived(() => searchQuery() ? 'No items match your search.' : 'This folder is empty.')
|
|
179
|
-
const selectedFilesLabel = derived(() => {
|
|
180
|
-
if (!selectedFiles().length)
|
|
181
|
-
return 'No files selected'
|
|
182
|
-
if (selectedFiles().length === 1)
|
|
183
|
-
return selectedFiles()[0]?.name || '1 file selected'
|
|
184
|
-
return `${selectedFiles().length} files selected`
|
|
185
|
-
})
|
|
186
|
-
|
|
187
174
|
const sidePanelClass = derived(() => `border-r border-black/[0.06] dark:border-white/[0.08] bg-[#F5F3EE]/70 dark:bg-neutral-900/40 backdrop-blur-2xl transition-all duration-300 ${showSidePanel() ? 'w-64' : 'w-0 overflow-hidden'}`)
|
|
188
175
|
const segmentBase = 'inline-flex items-center px-2.5 py-1.5 rounded-md text-sm font-medium transition-colors'
|
|
189
176
|
const segmentActive = 'bg-white dark:bg-neutral-700 text-stone-800 dark:text-white shadow-sm'
|
|
@@ -295,10 +282,8 @@ function toggleSortOrder(): void {
|
|
|
295
282
|
}
|
|
296
283
|
|
|
297
284
|
function openCreateFolder(): void {
|
|
298
|
-
newFolderName.set('')
|
|
299
285
|
createError.set('')
|
|
300
286
|
showCreateFolderModal.set(true)
|
|
301
|
-
void nextTick(() => folderNameInput.current?.focus())
|
|
302
287
|
}
|
|
303
288
|
|
|
304
289
|
function closeCreateFolder(): void {
|
|
@@ -306,10 +291,7 @@ function closeCreateFolder(): void {
|
|
|
306
291
|
showCreateFolderModal.set(false)
|
|
307
292
|
}
|
|
308
293
|
|
|
309
|
-
async function createFolder(): Promise<void> {
|
|
310
|
-
const name = newFolderName().trim()
|
|
311
|
-
if (!name)
|
|
312
|
-
return
|
|
294
|
+
async function createFolder(name: string): Promise<void> {
|
|
313
295
|
creatingFolder.set(true)
|
|
314
296
|
createError.set('')
|
|
315
297
|
try {
|
|
@@ -329,47 +311,18 @@ async function createFolder(): Promise<void> {
|
|
|
329
311
|
}
|
|
330
312
|
}
|
|
331
313
|
|
|
332
|
-
function resetUploadSelection(): void {
|
|
333
|
-
selectedFiles.set([])
|
|
334
|
-
if (uploadInput.current)
|
|
335
|
-
uploadInput.current.value = ''
|
|
336
|
-
}
|
|
337
|
-
|
|
338
314
|
function openUpload(): void {
|
|
339
315
|
uploadError.set('')
|
|
340
|
-
resetUploadSelection()
|
|
341
316
|
showUploadModal.set(true)
|
|
342
317
|
}
|
|
343
318
|
|
|
344
319
|
function closeUpload(): void {
|
|
345
|
-
if (!uploading())
|
|
320
|
+
if (!uploading())
|
|
346
321
|
showUploadModal.set(false)
|
|
347
|
-
resetUploadSelection()
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function acceptFiles(files: FileList | File[]): void {
|
|
352
|
-
selectedFiles.set(Array.from(files))
|
|
353
|
-
uploadError.set('')
|
|
354
322
|
}
|
|
355
323
|
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
if (input.files)
|
|
359
|
-
acceptFiles(input.files)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function handleDrop(event: DragEvent): void {
|
|
363
|
-
if (event.dataTransfer?.files)
|
|
364
|
-
acceptFiles(event.dataTransfer.files)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function handleDragOver(): void {
|
|
368
|
-
uploadError.set('')
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function uploadFiles(): Promise<void> {
|
|
372
|
-
if (!selectedFiles().length)
|
|
324
|
+
async function uploadFiles(files: File[]): Promise<void> {
|
|
325
|
+
if (!files.length)
|
|
373
326
|
return
|
|
374
327
|
uploading.set(true)
|
|
375
328
|
uploadError.set('')
|
|
@@ -377,12 +330,11 @@ async function uploadFiles(): Promise<void> {
|
|
|
377
330
|
const formData = new FormData()
|
|
378
331
|
formData.append('disk', selectedDisk())
|
|
379
332
|
formData.append('path', currentFolderPath())
|
|
380
|
-
for (const file of
|
|
333
|
+
for (const file of files)
|
|
381
334
|
formData.append('files', file)
|
|
382
335
|
await dashboardApi('/api/dashboard/files/uploads', { method: 'POST', formData })
|
|
383
|
-
const count =
|
|
336
|
+
const count = files.length
|
|
384
337
|
showUploadModal.set(false)
|
|
385
|
-
resetUploadSelection()
|
|
386
338
|
pushToast('success', `${count} ${count === 1 ? 'file' : 'files'} uploaded`)
|
|
387
339
|
await loadFiles()
|
|
388
340
|
}
|
|
@@ -405,6 +357,17 @@ function closeDelete(): void {
|
|
|
405
357
|
itemToDelete.set(null)
|
|
406
358
|
}
|
|
407
359
|
|
|
360
|
+
function deleteTitle(): string {
|
|
361
|
+
const item = itemToDelete()
|
|
362
|
+
return item ? `Delete ${item.name}` : 'Delete item'
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function deleteDescription(): string {
|
|
366
|
+
return itemToDelete()?.type === 'folder'
|
|
367
|
+
? 'The folder and everything inside it will be permanently deleted.'
|
|
368
|
+
: 'This file will be permanently deleted.'
|
|
369
|
+
}
|
|
370
|
+
|
|
408
371
|
async function deleteItem(): Promise<void> {
|
|
409
372
|
const item = itemToDelete()
|
|
410
373
|
if (!item)
|
|
@@ -430,25 +393,6 @@ async function deleteItem(): Promise<void> {
|
|
|
430
393
|
}
|
|
431
394
|
}
|
|
432
395
|
|
|
433
|
-
function handleKeydown(event: KeyboardEvent): void {
|
|
434
|
-
if (event.key !== 'Escape')
|
|
435
|
-
return
|
|
436
|
-
if (itemToDelete()) {
|
|
437
|
-
closeDelete()
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
if (showUploadModal()) {
|
|
441
|
-
closeUpload()
|
|
442
|
-
return
|
|
443
|
-
}
|
|
444
|
-
if (showCreateFolderModal()) {
|
|
445
|
-
closeCreateFolder()
|
|
446
|
-
return
|
|
447
|
-
}
|
|
448
|
-
selectedItem.set(null)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
useEventListener('keydown', handleKeydown)
|
|
452
396
|
onMount(() => { void loadFiles() })
|
|
453
397
|
</script>
|
|
454
398
|
|
|
@@ -568,37 +512,33 @@ onMount(() => { void loadFiles() })
|
|
|
568
512
|
</div>
|
|
569
513
|
</div>
|
|
570
514
|
|
|
571
|
-
<
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
<div :if="deleteError()" role="alert" class="mt-4 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ deleteError() }}</div>
|
|
602
|
-
<div class="flex gap-2 justify-end mt-6"><Button :disabled="deleting()" variant="secondary" @click="closeDelete()">Cancel</Button><Button :loading="deleting()" variant="danger" @click="deleteItem()">Delete permanently</Button></div>
|
|
603
|
-
</section>
|
|
604
|
-
</div>
|
|
515
|
+
<FileDetailsDrawer :item="selectedItem()" :disk="selectedDisk()" @close="selectedItem.set(null)" />
|
|
516
|
+
|
|
517
|
+
<CreateFolderDialog
|
|
518
|
+
:open="showCreateFolderModal()"
|
|
519
|
+
:location="currentFolderLabel()"
|
|
520
|
+
:busy="creatingFolder()"
|
|
521
|
+
:error="createError()"
|
|
522
|
+
@close="closeCreateFolder"
|
|
523
|
+
@submit="createFolder"
|
|
524
|
+
/>
|
|
525
|
+
|
|
526
|
+
<FileUploadDialog
|
|
527
|
+
:open="showUploadModal()"
|
|
528
|
+
:location="currentFolderLabel()"
|
|
529
|
+
:busy="uploading()"
|
|
530
|
+
:error="uploadError()"
|
|
531
|
+
@close="closeUpload"
|
|
532
|
+
@submit="uploadFiles"
|
|
533
|
+
/>
|
|
534
|
+
|
|
535
|
+
<ConfirmDialog
|
|
536
|
+
:isOpen="Boolean(itemToDelete())"
|
|
537
|
+
:title="deleteTitle()"
|
|
538
|
+
:description="deleteDescription()"
|
|
539
|
+
confirmLabel="Delete permanently"
|
|
540
|
+
:busy="deleting()"
|
|
541
|
+
:error="deleteError()"
|
|
542
|
+
@close="closeDelete"
|
|
543
|
+
@confirm="deleteItem"
|
|
544
|
+
/>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
const open = useReactiveProp('open', false)
|
|
3
|
+
const location = useReactiveProp('location', 'Storage root')
|
|
4
|
+
const busy = useReactiveProp('busy', false)
|
|
5
|
+
const error = useReactiveProp('error', '')
|
|
6
|
+
const files = state<File[]>([])
|
|
7
|
+
const fileInput = useRef<HTMLInputElement>('dashboardFileUploadInput')
|
|
8
|
+
const emit = defineEmits()
|
|
9
|
+
let wasOpen = false
|
|
10
|
+
|
|
11
|
+
const selectedFilesLabel = derived(() => {
|
|
12
|
+
if (!files().length)
|
|
13
|
+
return 'No files selected'
|
|
14
|
+
if (files().length === 1)
|
|
15
|
+
return files()[0]?.name || '1 file selected'
|
|
16
|
+
return `${files().length} files selected`
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
effect(() => {
|
|
20
|
+
const isOpen = open()
|
|
21
|
+
if (isOpen && !wasOpen)
|
|
22
|
+
reset()
|
|
23
|
+
wasOpen = isOpen
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function formatFileSize(bytes: number): string {
|
|
27
|
+
if (bytes === 0)
|
|
28
|
+
return '0 Bytes'
|
|
29
|
+
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
|
30
|
+
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)))
|
|
31
|
+
return `${Number.parseFloat((bytes / 1024 ** index).toFixed(2))} ${units[index]}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function reset(): void {
|
|
35
|
+
files.set([])
|
|
36
|
+
if (fileInput.current)
|
|
37
|
+
fileInput.current.value = ''
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function acceptFiles(value: FileList | File[]): void {
|
|
41
|
+
files.set(Array.from(value))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleFileSelection(event: Event): void {
|
|
45
|
+
const input = event.target as HTMLInputElement
|
|
46
|
+
if (input.files)
|
|
47
|
+
acceptFiles(input.files)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleDrop(event: DragEvent): void {
|
|
51
|
+
if (event.dataTransfer?.files)
|
|
52
|
+
acceptFiles(event.dataTransfer.files)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function close(): void {
|
|
56
|
+
if (!busy())
|
|
57
|
+
emit('close')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function submit(): void {
|
|
61
|
+
if (files().length)
|
|
62
|
+
emit('submit', [...files()])
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<Modal :isOpen="open()" title="Upload files" :description="'Upload to ' + location() + '. Each file can be up to 10 MB.'" :closable="!busy()" @close="close">
|
|
67
|
+
<form id="file-upload-form" @submit.prevent="submit">
|
|
68
|
+
<label class="flex flex-col items-center justify-center p-8 text-center border-2 border-dashed border-stone-300 rounded-xl dark:border-neutral-600 hover:border-blue-400 dark:hover:border-blue-600 cursor-pointer" @dragover.prevent @drop.prevent="handleDrop($event)">
|
|
69
|
+
<span class="h-10 w-10 text-stone-400 i-hugeicons-upload-01"></span>
|
|
70
|
+
<span class="mt-3 font-semibold text-stone-800 text-sm dark:text-white">Choose files or drop them here</span>
|
|
71
|
+
<span class="mt-1 text-stone-500 text-xs dark:text-neutral-400">{{ selectedFilesLabel() }}</span>
|
|
72
|
+
<input ref="dashboardFileUploadInput" name="files" type="file" class="sr-only" multiple @change="handleFileSelection($event)" />
|
|
73
|
+
</label>
|
|
74
|
+
<ul :if="files().length" class="overflow-y-auto mt-4 space-y-1 max-h-32 text-sm">
|
|
75
|
+
<template :for="file in files">
|
|
76
|
+
<li class="flex gap-4 items-center justify-between px-3 py-2 bg-stone-50 dark:bg-neutral-900 rounded-lg"><span class="text-stone-700 truncate dark:text-neutral-200">{{ file.name }}</span><span class="flex-shrink-0 font-mono text-stone-400 text-xs dark:text-neutral-500">{{ formatFileSize(file.size) }}</span></li>
|
|
77
|
+
</template>
|
|
78
|
+
</ul>
|
|
79
|
+
</form>
|
|
80
|
+
|
|
81
|
+
<div :if="error()" role="alert" class="mt-4 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ error() }}</div>
|
|
82
|
+
|
|
83
|
+
<template #footer>
|
|
84
|
+
<div class="flex gap-2 justify-end w-full">
|
|
85
|
+
<Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
|
|
86
|
+
<Button type="submit" form="file-upload-form" :disabled="!files().length" :loading="busy()">Upload</Button>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
</Modal>
|