@stacksjs/defaults 0.70.354 → 0.70.355
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 +17 -2
- package/ai/skills/stacks-browse/scripts/browse.ts +207 -6
- package/ai/skills/stacks-email/SKILL.md +35 -15
- package/app/Actions/Dashboard/Auth/MeAction.ts +8 -7
- package/app/Actions/Dashboard/Ci/RepoRunJobsAction.ts +10 -15
- package/app/Actions/Dashboard/Ci/RepoRunsAction.ts +12 -14
- package/app/Actions/Dashboard/Ci/RunnerHistoryAction.ts +11 -10
- package/app/Actions/Dashboard/Ci/StatusAction.ts +2 -11
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +1 -0
- package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +3 -4
- package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +1 -0
- package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +1 -0
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -0
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -0
- package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +3 -2
- package/app/Actions/Dashboard/Deployments/GetDeployment.ts +1 -0
- package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +3 -2
- package/app/Actions/Dashboard/Email/InboxActivityAction.ts +28 -16
- package/app/Actions/Dashboard/Email/InboxAttachmentDownloadAction.ts +45 -0
- package/app/Actions/Dashboard/Email/InboxDeleteAction.ts +11 -6
- package/app/Actions/Dashboard/Email/InboxIndexAction.ts +5 -7
- package/app/Actions/Dashboard/Email/InboxMarkReadAction.ts +11 -6
- package/app/Actions/Dashboard/Email/InboxMarkUnreadAction.ts +11 -6
- package/app/Actions/Dashboard/Email/InboxPreferenceShowAction.ts +10 -4
- package/app/Actions/Dashboard/Email/InboxPreferenceUpdateAction.ts +8 -9
- package/app/Actions/Dashboard/Email/InboxShowAction.ts +7 -7
- package/app/Actions/Dashboard/Email/InboxStatsAction.ts +5 -7
- package/app/Actions/Dashboard/Email/inbox-request.ts +33 -0
- package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +3 -4
- package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +3 -6
- package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +8 -15
- package/app/Actions/Dashboard/Jobs/JobRetryAction.ts +4 -3
- package/app/Actions/Dashboard/Jobs/JobShowAction.ts +3 -3
- package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +5 -5
- package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +9 -16
- package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +4 -3
- package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +5 -5
- package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +1 -2
- package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +4 -3
- package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +11 -9
- package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +10 -12
- package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +19 -43
- package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +44 -0
- package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +5 -5
- package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +6 -6
- package/app/Actions/Dashboard/Kanban/CardShowAction.ts +4 -4
- package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +7 -6
- package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +5 -5
- package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +4 -3
- package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +5 -5
- package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +4 -3
- package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +5 -5
- package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +4 -3
- package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +4 -4
- package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +4 -3
- package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +5 -5
- package/app/Actions/Dashboard/Kanban/UsersListAction.ts +1 -1
- package/app/Actions/Dashboard/Kanban/kanban-comment.ts +14 -0
- package/app/Actions/Dashboard/Kanban/kanban-model.test.ts +36 -0
- package/app/Actions/Dashboard/Kanban/kanban-model.ts +14 -7
- package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +4 -3
- package/app/Actions/Dashboard/Models/ModelShowAction.ts +15 -18
- package/app/Actions/Dashboard/Models/ModelStoreAction.ts +8 -8
- package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +9 -9
- package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +4 -3
- package/app/Actions/Dashboard/Models/model-api.ts +27 -0
- package/app/Actions/Dashboard/Models/model-write.ts +2 -10
- package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +3 -4
- package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +6 -4
- package/app/Actions/Dashboard/Queries/QueryShowAction.ts +1 -0
- package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +4 -4
- package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +3 -2
- package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +4 -4
- package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +4 -4
- package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +4 -3
- package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +3 -2
- package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +4 -5
- package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +4 -4
- package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -0
- package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +3 -3
- package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +2 -2
- package/app/Actions/Dashboard/dashboard-request.ts +13 -0
- package/app/Models/CardComment.ts +3 -4
- package/functions/dashboard-api.ts +51 -10
- package/functions/inbox.test.ts +2 -0
- package/functions/inbox.ts +27 -2
- package/functions/public-application-url.test.ts +13 -0
- package/functions/public-application-url.ts +27 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/Auth/AccessTokens.stx +15 -0
- package/resources/components/Dashboard/Ci/CiDashboard.stx +24 -6
- package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +8 -7
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +8 -7
- package/resources/components/Dashboard/Content/BlogDashboard.stx +14 -2
- package/resources/components/Dashboard/Content/PostsDashboard.stx +7 -1
- package/resources/components/Dashboard/Deployments/DeploymentDetail.stx +16 -2
- package/resources/components/Dashboard/Email/EmailNavigation.stx +5 -5
- package/resources/components/Dashboard/Email/EmailSidebar.stx +1 -1
- package/resources/components/Dashboard/Email/InboxAttachmentList.stx +53 -0
- package/resources/components/Dashboard/Email/InboxDashboard.stx +5 -4
- package/resources/components/Dashboard/Email/InboxMessageDetail.stx +6 -4
- package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +63 -6
- package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +3 -6
- package/resources/components/Dashboard/Management/PermissionsDashboard.stx +4 -4
- package/resources/components/Dashboard/NotFoundDashboard.stx +2 -5
- package/resources/components/Dashboard/Queries/QueryDashboard.stx +6 -2
- package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +4 -4
- package/resources/components/Dashboard/UI/EmptyState.stx +2 -2
- package/resources/components/Dashboard/UI/PageLayout.stx +3 -3
- package/resources/functions/dashboard/sidebar.ts +4 -4
- package/routes/dashboard-api.ts +8 -20
- package/views/dashboard/.discovered-models.json +24 -0
- package/views/dashboard/layouts/default.stx +19 -16
- package/views/dashboard/stores/ci.ts +13 -7
- package/views/dashboard/stores/kanban.ts +48 -11
|
@@ -26,6 +26,13 @@ const isSaving = state(false)
|
|
|
26
26
|
const isDeleting = state(false)
|
|
27
27
|
const busyId = state('')
|
|
28
28
|
const validationErrors = state<Record<string, string>>({})
|
|
29
|
+
const metrics = derived(() => [
|
|
30
|
+
{ label: 'Waiting', value: summary().waiting },
|
|
31
|
+
{ label: 'Quoted wait', value: `${summary().averageQuotedWait} min` },
|
|
32
|
+
{ label: 'Seated', value: summary().seated },
|
|
33
|
+
{ label: 'No show', value: summary().noShow },
|
|
34
|
+
{ label: 'Seating rate', value: `${summary().seatingRate}%` },
|
|
35
|
+
])
|
|
29
36
|
|
|
30
37
|
function rows(): RestaurantWaitlistRecord[] {
|
|
31
38
|
const query = search().trim().toLowerCase()
|
|
@@ -220,13 +227,7 @@ onMount(() => {
|
|
|
220
227
|
<div :if="loadError()" class="p-4 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">{{ loadError() }}</div>
|
|
221
228
|
|
|
222
229
|
<section class="grid gap-3 grid-cols-2 lg:grid-cols-5">
|
|
223
|
-
<template :for="item in
|
|
224
|
-
{ label: 'Waiting', value: summary().waiting },
|
|
225
|
-
{ label: 'Quoted wait', value: summary().averageQuotedWait + ' min' },
|
|
226
|
-
{ label: 'Seated', value: summary().seated },
|
|
227
|
-
{ label: 'No show', value: summary().noShow },
|
|
228
|
-
{ label: 'Seating rate', value: summary().seatingRate + '%' }
|
|
229
|
-
]">
|
|
230
|
+
<template :for="item in metrics()">
|
|
230
231
|
<div class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
231
232
|
<p class="text-gray-500 text-xs uppercase dark:text-neutral-400">{{ item.label }}</p>
|
|
232
233
|
<p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
import { publicApplicationUrl } from '../../../../functions/public-application-url'
|
|
3
|
+
|
|
4
|
+
export const blogUrl = publicApplicationUrl('/blog')
|
|
5
|
+
</script>
|
|
6
|
+
|
|
1
7
|
<template>
|
|
2
8
|
<div class="p-6 min-h-screen max-w-full text-neutral-950 dark:text-neutral-100 bg-neutral-50 dark:bg-neutral-950">
|
|
3
9
|
<div class="flex flex-col gap-6 mx-auto min-w-0 max-w-7xl w-full">
|
|
@@ -10,7 +16,7 @@
|
|
|
10
16
|
</p>
|
|
11
17
|
</div>
|
|
12
18
|
<div class="flex flex-wrap gap-2">
|
|
13
|
-
<Button tag="a" href="
|
|
19
|
+
<Button tag="a" href="{{ blogUrl }}" target="_blank" rel="noopener" variant="secondary">
|
|
14
20
|
<span class="h-4 w-4 i-hugeicons-view"></span>
|
|
15
21
|
View blog
|
|
16
22
|
</Button>
|
|
@@ -123,7 +129,7 @@
|
|
|
123
129
|
<div class="flex gap-1 justify-end">
|
|
124
130
|
<a
|
|
125
131
|
x-show="!post.draft"
|
|
126
|
-
:href="
|
|
132
|
+
:href="publicPostUrl(post.slug)"
|
|
127
133
|
target="_blank"
|
|
128
134
|
rel="noopener"
|
|
129
135
|
:title="'View ' + post.title"
|
|
@@ -264,6 +270,8 @@
|
|
|
264
270
|
// markdown files in content/blog. It owns no state; the signals below do.
|
|
265
271
|
import { deleteBlogPost, fetchBlogPost, fetchBlogPosts, saveBlogPost } from '~/storage/framework/defaults/functions/blog'
|
|
266
272
|
|
|
273
|
+
const publicBlogBaseUrl = {{ blogUrl }}
|
|
274
|
+
|
|
267
275
|
// Fixed-length list purely to render the loading skeleton rows.
|
|
268
276
|
const skeletonRows = [1, 2, 3, 4]
|
|
269
277
|
|
|
@@ -291,6 +299,10 @@ const formFeatured = state(false)
|
|
|
291
299
|
|
|
292
300
|
const postToDelete = state(null)
|
|
293
301
|
|
|
302
|
+
function publicPostUrl(slug) {
|
|
303
|
+
return `${publicBlogBaseUrl}/${encodeURIComponent(String(slug))}`
|
|
304
|
+
}
|
|
305
|
+
|
|
294
306
|
// Read by the delete dialog's copy; kept as a derived signal so the template
|
|
295
307
|
// never has to reach through a possibly-null post.
|
|
296
308
|
const deleteTargetSlug = derived(() => {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
import { publicApplicationUrl } from '../../../../functions/public-application-url'
|
|
3
|
+
|
|
4
|
+
export const blogUrl = publicApplicationUrl('/blog')
|
|
5
|
+
</script>
|
|
6
|
+
|
|
1
7
|
<script client>
|
|
2
8
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
9
|
import { pushToast } from '../../../../functions/toasts'
|
|
@@ -229,7 +235,7 @@ onMount(() => { void loadPosts() })
|
|
|
229
235
|
<div class="space-y-6">
|
|
230
236
|
<header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
231
237
|
<div><p class="font-semibold text-blue-600 text-xs tracking-wide uppercase dark:text-blue-400">CMS</p><h1 class="mt-1 font-bold text-2xl text-gray-900 dark:text-white">Posts</h1><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Manage persisted articles, publishing, authors, and native taxonomies.</p></div>
|
|
232
|
-
<div class="flex flex-wrap gap-2"><Button href="
|
|
238
|
+
<div class="flex flex-wrap gap-2"><Button href="{{ blogUrl }}" target="_blank" rel="noreferrer" variant="secondary"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-view"></span>View blog</Button><Button :loading="loading()" variant="secondary" @click="loadPosts()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button><Button @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>New post</Button></div>
|
|
233
239
|
</header>
|
|
234
240
|
|
|
235
241
|
<div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between p-4 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"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadPosts()">Retry</Button></div>
|
|
@@ -44,6 +44,20 @@ function statusLabel(status: string): string {
|
|
|
44
44
|
return status.charAt(0).toUpperCase() + status.slice(1)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function deploymentUrl(): string {
|
|
48
|
+
const value = record()?.url?.trim()
|
|
49
|
+
if (!value)
|
|
50
|
+
return ''
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const url = new URL(value)
|
|
54
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : ''
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return ''
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
async function loadDeployment(): Promise<void> {
|
|
48
62
|
const route = useRoute()
|
|
49
63
|
const id = String(route.params.id || '')
|
|
@@ -131,9 +145,9 @@ onMount(() => {
|
|
|
131
145
|
<dt class="text-gray-500 dark:text-neutral-400">Created</dt>
|
|
132
146
|
<dd class="mt-1 text-gray-900 dark:text-white">{{ formatDate(record()?.createdAt || '') }}</dd>
|
|
133
147
|
</div>
|
|
134
|
-
<div :if="
|
|
148
|
+
<div :if="deploymentUrl()">
|
|
135
149
|
<dt class="text-gray-500 dark:text-neutral-400">Deployment URL</dt>
|
|
136
|
-
<dd class="mt-1"><a :href="
|
|
150
|
+
<dd class="mt-1"><a :href="deploymentUrl()" target="_blank" rel="noopener noreferrer" class="font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500">{{ deploymentUrl() }}</a></dd>
|
|
137
151
|
</div>
|
|
138
152
|
</dl>
|
|
139
153
|
</div>
|
|
@@ -8,17 +8,17 @@ function linkClass(name: string): string {
|
|
|
8
8
|
}
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
|
-
<nav aria-label="Mail sections" class="
|
|
12
|
-
<StxLink to="/inbox" :class="'px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('inbox')">
|
|
11
|
+
<nav aria-label="Mail sections" class="flex overflow-x-auto gap-0.5 sm:gap-1 p-1 max-w-full bg-gray-100 dark:bg-neutral-900 rounded-lg">
|
|
12
|
+
<StxLink to="/inbox" :class="'px-2 sm:px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('inbox')">
|
|
13
13
|
Inbox
|
|
14
14
|
</StxLink>
|
|
15
|
-
<StxLink to="/inbox/activity" :class="'px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('activity')">
|
|
15
|
+
<StxLink to="/inbox/activity" :class="'px-2 sm:px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('activity')">
|
|
16
16
|
Activity
|
|
17
17
|
</StxLink>
|
|
18
|
-
<StxLink to="/inbox/captured" :class="'px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('captured')">
|
|
18
|
+
<StxLink to="/inbox/captured" :class="'px-2 sm:px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('captured')">
|
|
19
19
|
Captured
|
|
20
20
|
</StxLink>
|
|
21
|
-
<StxLink to="/inbox/settings" :class="'px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('settings')">
|
|
21
|
+
<StxLink to="/inbox/settings" :class="'px-2 sm:px-3 py-1.5 font-medium text-sm rounded-md transition-colors ' + linkClass('settings')">
|
|
22
22
|
Settings
|
|
23
23
|
</StxLink>
|
|
24
24
|
</nav>
|
|
@@ -47,7 +47,7 @@ function mailboxLabel(): string {
|
|
|
47
47
|
}
|
|
48
48
|
</script>
|
|
49
49
|
|
|
50
|
-
<aside class="flex flex-col flex-shrink-0 w-64 bg-white dark:bg-neutral-800 border-gray-200 border-r dark:border-neutral-600">
|
|
50
|
+
<aside class="flex flex-col flex-shrink-0 w-full md:w-64 bg-white dark:bg-neutral-800 border-b border-gray-200 md:border-b-0 md:border-r dark:border-neutral-600">
|
|
51
51
|
<div class="p-3 border-b border-gray-200 dark:border-neutral-600">
|
|
52
52
|
<div class="flex items-center space-x-2">
|
|
53
53
|
<div class="flex flex-shrink-0 items-center justify-center h-8 w-8 text-gray-700 dark:text-white bg-gray-200 dark:bg-neutral-700 rounded">
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
import type { DashboardInboxAttachment } from '../../../../functions/inbox'
|
|
3
|
+
import { downloadDashboardInboxAttachment } from '../../../../functions/inbox'
|
|
4
|
+
import { pushToast } from '../../../../functions/toasts'
|
|
5
|
+
|
|
6
|
+
const attachments = useReactiveProp<DashboardInboxAttachment[]>('attachments', [])
|
|
7
|
+
const mailbox = useReactiveProp('mailbox', '')
|
|
8
|
+
const messageId = useReactiveProp('messageId', '')
|
|
9
|
+
const downloadingId = state('')
|
|
10
|
+
|
|
11
|
+
function formatBytes(bytes: number): string {
|
|
12
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
13
|
+
return 'Unknown size'
|
|
14
|
+
|
|
15
|
+
const units = ['B', 'KB', 'MB', 'GB']
|
|
16
|
+
const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
|
17
|
+
const value = bytes / 1024 ** unitIndex
|
|
18
|
+
return `${value >= 10 || unitIndex === 0 ? Math.round(value) : value.toFixed(1)} ${units[unitIndex]}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function download(attachment: DashboardInboxAttachment): Promise<void> {
|
|
22
|
+
downloadingId.set(attachment.id)
|
|
23
|
+
try {
|
|
24
|
+
await downloadDashboardInboxAttachment(messageId(), attachment, mailbox())
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
pushToast('error', 'Could not download attachment', { detail: String(error) })
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
downloadingId.set('')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<section :if="attachments().length" aria-label="Message attachments" class="mb-4 mx-4 p-3 bg-gray-50 dark:bg-neutral-800 border border-gray-200 rounded-md dark:border-neutral-600">
|
|
36
|
+
<div class="flex gap-2 items-center text-gray-800 dark:text-neutral-200">
|
|
37
|
+
<span aria-hidden="true" class="h-5 w-5 i-hugeicons-attachment-01"></span>
|
|
38
|
+
<h3 class="font-medium text-sm">Attachments</h3>
|
|
39
|
+
<span class="text-gray-500 text-xs dark:text-neutral-400">{{ attachments().length }}</span>
|
|
40
|
+
</div>
|
|
41
|
+
<ul class="mt-3 space-y-2">
|
|
42
|
+
<li :for="attachment in attachments()" class="flex gap-3 items-center justify-between p-2 bg-white dark:bg-neutral-700 border border-gray-200 rounded-md dark:border-neutral-600">
|
|
43
|
+
<div class="min-w-0">
|
|
44
|
+
<p class="font-medium text-gray-900 text-sm truncate dark:text-white">{{ attachment.name }}</p>
|
|
45
|
+
<p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ formatBytes(attachment.size) }}</p>
|
|
46
|
+
</div>
|
|
47
|
+
<Button size="sm" :loading="downloadingId() === attachment.id" :disabled="Boolean(downloadingId())" @click="download(attachment)">
|
|
48
|
+
<span :if="downloadingId() !== attachment.id" aria-hidden="true" class="h-4 w-4 i-hugeicons-download-04"></span>
|
|
49
|
+
Download
|
|
50
|
+
</Button>
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
</section>
|
|
@@ -121,7 +121,7 @@ async function openEmail(value: DashboardInboxEmail | CustomEvent<DashboardInbox
|
|
|
121
121
|
deletePending.set(false)
|
|
122
122
|
replyText.set('')
|
|
123
123
|
|
|
124
|
-
if (!email.
|
|
124
|
+
if (!email.detailsLoaded) {
|
|
125
125
|
try {
|
|
126
126
|
const body = await fetchDashboardInboxBody(email.id, mailbox())
|
|
127
127
|
emails.set(emails().map(item => item.id === email.id ? { ...item, ...body } : item))
|
|
@@ -228,7 +228,7 @@ onMount(() => {
|
|
|
228
228
|
</script>
|
|
229
229
|
|
|
230
230
|
<div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
231
|
-
<div class="flex min-h-[calc(100vh-8rem)]">
|
|
231
|
+
<div class="flex flex-col md:flex-row min-h-[calc(100vh-8rem)]">
|
|
232
232
|
<EmailSidebar
|
|
233
233
|
activeFolder="inbox"
|
|
234
234
|
:folders="folders"
|
|
@@ -237,10 +237,10 @@ onMount(() => {
|
|
|
237
237
|
@compose="compose"
|
|
238
238
|
/>
|
|
239
239
|
|
|
240
|
-
<div class="flex overflow-hidden flex-1 flex-col">
|
|
240
|
+
<div class="flex overflow-hidden flex-1 flex-col min-w-0">
|
|
241
241
|
<header class="px-4 py-3 bg-white dark:bg-neutral-700 border-b border-gray-200 dark:border-neutral-600">
|
|
242
242
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
243
|
-
<div class="flex gap-4 items-center">
|
|
243
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:gap-4 sm:items-center min-w-0">
|
|
244
244
|
<h1 class="font-bold text-gray-900 text-xl dark:text-white">Inbox</h1>
|
|
245
245
|
<EmailNavigation active="inbox" />
|
|
246
246
|
</div>
|
|
@@ -277,6 +277,7 @@ onMount(() => {
|
|
|
277
277
|
:replying="isReplying"
|
|
278
278
|
:replyText="replyText"
|
|
279
279
|
:canSendReply="canSendReply"
|
|
280
|
+
:mailbox="mailbox"
|
|
280
281
|
@close="closeEmail"
|
|
281
282
|
@toggle-read="toggleSelectedRead"
|
|
282
283
|
@request-delete="requestDelete"
|
|
@@ -7,6 +7,7 @@ const deleting = useReactiveProp('deleting', false)
|
|
|
7
7
|
const replying = useReactiveProp('replying', false)
|
|
8
8
|
const replyText = useReactiveProp('replyText', '')
|
|
9
9
|
const canSendReply = useReactiveProp('canSendReply', false)
|
|
10
|
+
const mailbox = useReactiveProp('mailbox', '')
|
|
10
11
|
const emit = defineEmits()
|
|
11
12
|
|
|
12
13
|
function message(): DashboardInboxEmail | null {
|
|
@@ -69,10 +70,11 @@ function formatDate(value: string): string {
|
|
|
69
70
|
:text="message()?.bodyText || ''"
|
|
70
71
|
:title="message()?.subject || 'Inbound email preview'"
|
|
71
72
|
/>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
<InboxAttachmentList
|
|
74
|
+
:attachments="message()?.attachments || []"
|
|
75
|
+
:mailbox="mailbox"
|
|
76
|
+
:messageId="message()?.id || ''"
|
|
77
|
+
/>
|
|
76
78
|
</div>
|
|
77
79
|
|
|
78
80
|
<div class="p-4 bg-white dark:bg-neutral-700 border-gray-200 border-t dark:border-neutral-600">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
-
// Kanban board view
|
|
2
|
+
// Kanban board view.
|
|
3
3
|
//
|
|
4
4
|
// Optimistic-update pattern:
|
|
5
5
|
// 1. The DnD handler mutates the store immediately (so the card
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
const columns = derived(() => kanban.currentColumns())
|
|
30
30
|
const allLabels = derived(() => kanban.currentLabels())
|
|
31
31
|
|
|
32
|
-
// Card detail modal state
|
|
32
|
+
// Card detail modal state - read straight from the store. The
|
|
33
33
|
// modal itself lives at the bottom of this file.
|
|
34
34
|
const openCard = derived(() => kanban.openCard())
|
|
35
35
|
const loadingCard = derived(() => kanban.loadingCard())
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
const titleDraft = state('')
|
|
49
49
|
const descDraft = state('')
|
|
50
50
|
const commentDraft = state('')
|
|
51
|
+
const editingCommentId = state(null)
|
|
52
|
+
const commentEditDraft = state('')
|
|
53
|
+
const savingCommentId = state(null)
|
|
51
54
|
const showLabelPicker = state(false)
|
|
52
55
|
const showAssigneePicker = state(false)
|
|
53
56
|
const showNewLabelComposer = state(false)
|
|
@@ -83,6 +86,9 @@
|
|
|
83
86
|
descDraft.set(c ? (c.description || '') : '')
|
|
84
87
|
dueDateDraft.set(c?.dueDate ? c.dueDate.slice(0, 10) : '')
|
|
85
88
|
commentDraft.set('')
|
|
89
|
+
editingCommentId.set(null)
|
|
90
|
+
commentEditDraft.set('')
|
|
91
|
+
savingCommentId.set(null)
|
|
86
92
|
}
|
|
87
93
|
// Sync drafts when the open card changes (open / close / refetch).
|
|
88
94
|
effect(() => { openCard(); Promise.resolve().then(syncDraftsFromCard) })
|
|
@@ -134,6 +140,40 @@
|
|
|
134
140
|
if (ok) commentDraft.set('')
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
function startCommentEdit(id, body) {
|
|
144
|
+
editingCommentId.set(id)
|
|
145
|
+
commentEditDraft.set(body)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cancelCommentEdit() {
|
|
149
|
+
editingCommentId.set(null)
|
|
150
|
+
commentEditDraft.set('')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function saveCommentEdit(id) {
|
|
154
|
+
const body = commentEditDraft().trim()
|
|
155
|
+
if (!body || savingCommentId()) return
|
|
156
|
+
savingCommentId.set(id)
|
|
157
|
+
try {
|
|
158
|
+
if (await kanban.updateComment(id, body))
|
|
159
|
+
cancelCommentEdit()
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
savingCommentId.set(null)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleCommentEditKeydown(event, id) {
|
|
167
|
+
if (event.key === 'Escape') {
|
|
168
|
+
event.preventDefault()
|
|
169
|
+
cancelCommentEdit()
|
|
170
|
+
}
|
|
171
|
+
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
172
|
+
event.preventDefault()
|
|
173
|
+
void saveCommentEdit(id)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
137
177
|
function requestDelete(kind: DeleteKind, id: number, hasCards = false): void {
|
|
138
178
|
pendingDelete.set({ kind, id, hasCards })
|
|
139
179
|
}
|
|
@@ -219,7 +259,7 @@
|
|
|
219
259
|
}
|
|
220
260
|
|
|
221
261
|
// Pull the id from the URL through the navigation composable.
|
|
222
|
-
// (stacksjs/stacks#1838
|
|
262
|
+
// (stacksjs/stacks#1838 - no vanilla DOM in dashboard pages).
|
|
223
263
|
const id = useNavigation().urlIdParam()
|
|
224
264
|
if (id !== null)
|
|
225
265
|
kanban.loadBoard(id)
|
|
@@ -647,7 +687,7 @@
|
|
|
647
687
|
</div>
|
|
648
688
|
</div>
|
|
649
689
|
|
|
650
|
-
<!--
|
|
690
|
+
<!-- Card detail modal -->
|
|
651
691
|
<!-- Opens on card click. Fixed positioned with a backdrop; closes
|
|
652
692
|
on backdrop click + Escape. Sections (top → bottom):
|
|
653
693
|
· Title (inline-editable, blurs to commit)
|
|
@@ -739,9 +779,26 @@
|
|
|
739
779
|
<div class="flex gap-2 items-baseline">
|
|
740
780
|
<span class="font-medium text-xs text-zinc-700 dark:text-zinc-200" :text="cm.authorName || cm.authorEmail || 'Unknown'"></span>
|
|
741
781
|
<span class="text-[10px] text-zinc-400" :text="formatTimestamp(cm.createdAt)"></span>
|
|
742
|
-
<
|
|
782
|
+
<template :if="editingCommentId() !== cm.id">
|
|
783
|
+
<Button variant="ghost" size="xs" @click="startCommentEdit(cm.id, cm.body)">Edit</Button>
|
|
784
|
+
<Button variant="ghost" size="xs" @click="requestDelete('comment', cm.id)"><span class="text-red-500">Delete</span></Button>
|
|
785
|
+
</template>
|
|
786
|
+
</div>
|
|
787
|
+
<p :if="editingCommentId() !== cm.id" class="mt-0.5 text-xs text-zinc-700 whitespace-pre-wrap dark:text-zinc-300" :text="cm.body"></p>
|
|
788
|
+
<div :if="editingCommentId() === cm.id" class="mt-2">
|
|
789
|
+
<textarea
|
|
790
|
+
x-model="commentEditDraft"
|
|
791
|
+
@keydown="handleCommentEditKeydown($event, cm.id)"
|
|
792
|
+
rows="3"
|
|
793
|
+
maxlength="10000"
|
|
794
|
+
:aria-label="'Edit comment by ' + (cm.authorName || cm.authorEmail || 'Unknown')"
|
|
795
|
+
class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
|
|
796
|
+
></textarea>
|
|
797
|
+
<div class="flex gap-2 justify-end mt-2">
|
|
798
|
+
<Button variant="secondary" size="xs" :disabled="savingCommentId() === cm.id" @click="cancelCommentEdit()">Cancel</Button>
|
|
799
|
+
<Button size="xs" :loading="savingCommentId() === cm.id" :disabled="!commentEditDraft().trim()" @click="saveCommentEdit(cm.id)">Save comment</Button>
|
|
800
|
+
</div>
|
|
743
801
|
</div>
|
|
744
|
-
<p class="mt-0.5 text-xs text-zinc-700 whitespace-pre-wrap dark:text-zinc-300" :text="cm.body"></p>
|
|
745
802
|
</div>
|
|
746
803
|
</li>
|
|
747
804
|
</ul>
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
<script client>
|
|
2
|
-
// Kanban — Boards index (stacksjs/stacks#1846, Phase 1).
|
|
3
|
-
//
|
|
4
2
|
// Lists every non-archived board with a card-count badge. Clicking a
|
|
5
|
-
// board navigates to
|
|
6
|
-
// drag-and-drop board view at that URL.
|
|
3
|
+
// board navigates to its complete drag-and-drop view.
|
|
7
4
|
//
|
|
8
5
|
// Follows #1838 conventions: no <script server>, no vanilla DOM
|
|
9
|
-
// reads, all state via the store. Role gate via useRole()
|
|
6
|
+
// reads, all state via the store. Role gate via useRole() - the
|
|
10
7
|
// surface is dev-mode-only (admins + devs see it, clients don't).
|
|
11
8
|
import { useRole } from '~/storage/framework/defaults/views/dashboard/composables/useRole'
|
|
12
9
|
import { kanbanStore } from '~/storage/framework/defaults/views/dashboard/stores/kanban'
|
|
@@ -175,7 +172,7 @@
|
|
|
175
172
|
<p class="mt-2 text-xs text-zinc-500">Most common: migrations haven't run yet. Try <code>./buddy migrate</code>.</p>
|
|
176
173
|
</div>
|
|
177
174
|
|
|
178
|
-
<!-- Empty state
|
|
175
|
+
<!-- Empty state - DB reachable but no boards yet -->
|
|
179
176
|
<div :if="!loading && !error && !hasBoards" class="px-6 py-12 text-center border border-dashed border-zinc-300 rounded-lg dark:border-zinc-700">
|
|
180
177
|
<span class="block mb-3 mx-auto h-10 w-10 text-zinc-400 i-hugeicons-layout-3-column"></span>
|
|
181
178
|
<p class="font-medium text-sm text-zinc-700 dark:text-zinc-300">No boards yet</p>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// and the role/permission matrix against the live `user_roles` /
|
|
6
6
|
// `role_permissions` / `user_permissions` pivots. All persistence
|
|
7
7
|
// flows through the dashboard RBAC endpoints, which delegate
|
|
8
|
-
// to `@stacksjs/auth`'s `Rbac` facade
|
|
8
|
+
// to `@stacksjs/auth`'s `Rbac` facade - no direct DB writes from the
|
|
9
9
|
// page.
|
|
10
10
|
//
|
|
11
11
|
// Follows #1838 conventions:
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
// - All state in the rbacStore; the page is pure presentation.
|
|
14
14
|
// - Mutations are optimistic with rollback on failure (store handles
|
|
15
15
|
// the snapshot/rollback dance).
|
|
16
|
-
import { useStore } from '@stacksjs/stx'
|
|
17
16
|
import { useRole } from '~/storage/framework/defaults/views/dashboard/composables/useRole'
|
|
17
|
+
import { rbacStore } from '~/storage/framework/defaults/views/dashboard/stores/rbac'
|
|
18
18
|
|
|
19
|
-
const rbac =
|
|
19
|
+
const rbac = rbacStore
|
|
20
20
|
// Admin-only surface. Per `useRole.ts`, `isAdmin()` returns true for
|
|
21
21
|
// any user with the `admin` role *or* an unauthenticated viewer on
|
|
22
|
-
// the localhost dev dashboard
|
|
22
|
+
// the localhost dev dashboard - same permissive-dev defaults the rest
|
|
23
23
|
// of the management surface uses.
|
|
24
24
|
const { isAdmin } = useRole()
|
|
25
25
|
const canManage = derived(() => isAdmin())
|
|
@@ -11,19 +11,16 @@
|
|
|
11
11
|
Sorry, we couldn't find the page you're looking for. The page might have been moved or deleted.
|
|
12
12
|
</p>
|
|
13
13
|
<div class="flex gap-x-6 items-center justify-center mt-10">
|
|
14
|
-
<Button tag="a" href="/
|
|
14
|
+
<Button tag="a" href="/">
|
|
15
15
|
Return to dashboard
|
|
16
16
|
</Button>
|
|
17
|
-
<Button tag="a" href="/" variant="ghost">
|
|
18
|
-
Go to home <span aria-hidden="true">→</span>
|
|
19
|
-
</Button>
|
|
20
17
|
</div>
|
|
21
18
|
</div>
|
|
22
19
|
|
|
23
20
|
<div>
|
|
24
21
|
<h2 class="font-semibold leading-6 text-base text-center text-gray-900 dark:text-white">You might be looking for</h2>
|
|
25
22
|
<div class="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 mt-6">
|
|
26
|
-
<StxLink to="/
|
|
23
|
+
<StxLink to="/" class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow hover:shadow-md duration-200 transition-shadow">
|
|
27
24
|
<div class="p-5">
|
|
28
25
|
<div class="flex items-center">
|
|
29
26
|
<div class="flex-shrink-0 p-3 bg-blue-500 rounded-md">
|
|
@@ -97,6 +97,10 @@ function formatDate(value: string): string {
|
|
|
97
97
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString()
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function queryDetailsPath(id: DashboardQueryLog['id']): string {
|
|
101
|
+
return `/queries/${encodeURIComponent(String(id))}`
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
function typeClass(value: DashboardQueryLog['type']): string {
|
|
101
105
|
const classes: Record<DashboardQueryLog['type'], string> = {
|
|
102
106
|
SELECT: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
@@ -248,9 +252,9 @@ onMount(() => {
|
|
|
248
252
|
</td>
|
|
249
253
|
<td class="px-4 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(query.executedAt) }}</td>
|
|
250
254
|
<td class="pr-5 py-3.5 text-right">
|
|
251
|
-
<
|
|
255
|
+
<a :href="queryDetailsPath(query.id)" :aria-label="'View query ' + query.id" class="inline-flex p-1.5 text-neutral-500 dark:text-neutral-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-md">
|
|
252
256
|
<span class="h-4 w-4 i-hugeicons-arrow-right-01"></span>
|
|
253
|
-
</
|
|
257
|
+
</a>
|
|
254
258
|
</td>
|
|
255
259
|
</tr>
|
|
256
260
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script server>
|
|
2
2
|
// The navigation the toggles below control. Read from the same builder the
|
|
3
|
-
// layout uses, so the list can never drift from what the sidebar renders
|
|
3
|
+
// layout uses, so the list can never drift from what the sidebar renders -
|
|
4
4
|
// including sections a project has switched off in config/dashboard.ts, which
|
|
5
5
|
// simply are not here to toggle.
|
|
6
6
|
import { buildWebSidebarSections } from '~/storage/framework/defaults/resources/functions/dashboard/sidebar'
|
|
@@ -29,7 +29,7 @@ const appearance = useAppearance()
|
|
|
29
29
|
// see until you navigate away is the thing this is avoiding.
|
|
30
30
|
const styleOptions = SIDEBAR_STYLE_OPTIONS
|
|
31
31
|
const colorModes = COLOR_MODE_OPTIONS
|
|
32
|
-
const sections = navSections
|
|
32
|
+
const sections = {{ navSections }}
|
|
33
33
|
|
|
34
34
|
const sidebarStyle = appearance.sidebarStyle
|
|
35
35
|
const colorMode = appearance.colorMode
|
|
@@ -58,9 +58,9 @@ function resetAll() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Selected state travels as a data attribute rather than as a swapped class
|
|
61
|
-
// string. It began as a workaround
|
|
61
|
+
// string. It began as a workaround - Crosswind could not see class names that
|
|
62
62
|
// existed only inside a client-script expression, so those utilities were never
|
|
63
|
-
// emitted and the control rendered unstyled
|
|
63
|
+
// emitted and the control rendered unstyled - and that gap is now closed
|
|
64
64
|
// upstream. It stays because it is the better shape regardless: one selector
|
|
65
65
|
// per state in the block below, rather than two class strings kept in sync by
|
|
66
66
|
// hand inside a ternary.
|
|
@@ -19,9 +19,9 @@ const actionHref = useReactiveProp('actionHref', '')
|
|
|
19
19
|
<p :if="description()" class="mb-6 max-w-sm text-neutral-500 text-sm dark:text-neutral-400" :text="description()"></p>
|
|
20
20
|
|
|
21
21
|
<Button
|
|
22
|
-
:if="actionLabel()"
|
|
22
|
+
:if="actionLabel() && actionHref()"
|
|
23
23
|
tag="a"
|
|
24
|
-
:href="actionHref()
|
|
24
|
+
:href="actionHref()"
|
|
25
25
|
>
|
|
26
26
|
<span :if="actionIcon()" :class="actionIcon() + ' h-4 w-4'"></span>
|
|
27
27
|
<span :text="actionLabel()"></span>
|
|
@@ -3,11 +3,11 @@ interface PageLayoutProps {
|
|
|
3
3
|
/**
|
|
4
4
|
* Visual variant.
|
|
5
5
|
*
|
|
6
|
-
* - `default`
|
|
6
|
+
* - `default` - standard centered column with `mx-auto` + max-width
|
|
7
7
|
* gutter. Use for almost every page.
|
|
8
|
-
* - `centered`
|
|
8
|
+
* - `centered` - vertically centered hero block on a min-height
|
|
9
9
|
* container. Use for splash / 404 / empty-route pages.
|
|
10
|
-
* - `bare`
|
|
10
|
+
* - `bare` - no container at all. Use when the page wants to own
|
|
11
11
|
* its own gutter (e.g. a settings page with side-by-side panels
|
|
12
12
|
* that already pad themselves).
|
|
13
13
|
*/
|
|
@@ -428,11 +428,11 @@ export function buildNavSections(
|
|
|
428
428
|
// The layout's role map hides the row from client-role viewers.
|
|
429
429
|
{ to: '/kanban', icon: 'table', text: 'Kanban', roles: ['admin', 'dev'] },
|
|
430
430
|
]
|
|
431
|
-
// CI tracking (stacksjs/stacks#1844)
|
|
432
|
-
// config/dashboard.ts.
|
|
433
|
-
// (
|
|
431
|
+
// CI tracking (stacksjs/stacks#1844) - opt-in via `ci.enabled` in
|
|
432
|
+
// config/dashboard.ts. Keep the row gate aligned with the page's
|
|
433
|
+
// `useRole().isDev()` check so client-only viewers never get a dead link.
|
|
434
434
|
if (toggles.ci)
|
|
435
|
-
managementItems.push({ to: '/ci', icon: 'check-circle', text: 'CI' })
|
|
435
|
+
managementItems.push({ to: '/ci', icon: 'check-circle', text: 'CI', roles: ['dev'] })
|
|
436
436
|
sections.push(['management', 'management', managementItems])
|
|
437
437
|
}
|
|
438
438
|
|