@stacksjs/defaults 0.70.353 → 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-dashboard/scripts/audit.ts +0 -1
- 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/Commerce/tax-rate-records.ts +9 -0
- package/app/Actions/Dashboard/Content/author-input.ts +1 -1
- package/app/Actions/Dashboard/Content/page-input.ts +1 -1
- package/app/Actions/Dashboard/DashboardStatsAction.ts +0 -1
- 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/app/Models/commerce/TaxRate.ts +48 -0
- 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 +23 -0
- package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +4 -0
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +9 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { Card } from '@stacksjs/orm'
|
|
3
4
|
import { modelBoolean, modelNullableString, modelNumber, modelString, modelValue, refreshModel } from './kanban-model'
|
|
@@ -11,7 +12,7 @@ interface CardInput {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* `PATCH /api/dashboard/kanban/cards/:id
|
|
15
|
+
* `PATCH /api/dashboard/kanban/cards/:id`.
|
|
15
16
|
*
|
|
16
17
|
* Partial update for the card body: title, description, due date,
|
|
17
18
|
* archive flag. `position` and `columnId` are NOT updatable here —
|
|
@@ -25,14 +26,13 @@ export default new Action({
|
|
|
25
26
|
description: 'Partial update of a card body (title, description, due date, archive).',
|
|
26
27
|
method: 'PATCH',
|
|
27
28
|
apiResponse: true,
|
|
28
|
-
async handle(request) {
|
|
29
|
-
const
|
|
30
|
-
const id = Number(rawId)
|
|
29
|
+
async handle(request: RequestInstance<CardInput>) {
|
|
30
|
+
const id = Number(request.getParam('id'))
|
|
31
31
|
if (!Number.isFinite(id) || id <= 0) {
|
|
32
32
|
return kanbanError('Invalid card id', 400)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const body = (
|
|
35
|
+
const body = request.all()
|
|
36
36
|
const set: Record<string, unknown> = {}
|
|
37
37
|
|
|
38
38
|
if (typeof body.title === 'string') {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { db } from '@stacksjs/database'
|
|
3
4
|
import { kanbanError } from './kanban-response'
|
|
@@ -31,7 +32,7 @@ interface ReorderInput {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
* `POST /api/dashboard/kanban/cards/reorder
|
|
35
|
+
* `POST /api/dashboard/kanban/cards/reorder`.
|
|
35
36
|
*
|
|
36
37
|
* The drag-and-drop hot path. Rewrites card positions and column
|
|
37
38
|
* assignments across one or more columns of a single board in a single
|
|
@@ -47,8 +48,8 @@ export default new Action({
|
|
|
47
48
|
description: 'Bulk-rewrite cards.{column_id, position} across one or more columns of the same board.',
|
|
48
49
|
method: 'POST',
|
|
49
50
|
apiResponse: true,
|
|
50
|
-
async handle(request) {
|
|
51
|
-
const body = (
|
|
51
|
+
async handle(request: RequestInstance<ReorderInput>) {
|
|
52
|
+
const body = request.all()
|
|
52
53
|
|
|
53
54
|
if (!Array.isArray(body.columns) || body.columns.length === 0) {
|
|
54
55
|
return kanbanError('`columns` must be a non-empty array.', 400)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { db } from '@stacksjs/database'
|
|
3
4
|
import { kanbanError } from './kanban-response'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* `DELETE /api/dashboard/kanban/columns/:id
|
|
7
|
+
* `DELETE /api/dashboard/kanban/columns/:id`.
|
|
7
8
|
*
|
|
8
9
|
* Hard-deletes the column and cascade-cleans every card inside it
|
|
9
10
|
* (plus the pivot rows referencing those cards). Same convention as
|
|
@@ -19,9 +20,8 @@ export default new Action({
|
|
|
19
20
|
description: 'Hard-deletes a column and its cards.',
|
|
20
21
|
method: 'DELETE',
|
|
21
22
|
apiResponse: true,
|
|
22
|
-
async handle(request) {
|
|
23
|
-
const
|
|
24
|
-
const id = Number(rawId)
|
|
23
|
+
async handle(request: RequestInstance) {
|
|
24
|
+
const id = Number(request.getParam('id'))
|
|
25
25
|
if (!Number.isFinite(id) || id <= 0) {
|
|
26
26
|
return kanbanError('Invalid column id', 400)
|
|
27
27
|
}
|
|
@@ -37,7 +37,7 @@ export default new Action({
|
|
|
37
37
|
'DELETE FROM card_assignees WHERE card_id IN (SELECT id FROM cards WHERE column_id = ?)',
|
|
38
38
|
[id],
|
|
39
39
|
).execute()
|
|
40
|
-
// Card comments
|
|
40
|
+
// Card comments are card-scoped children.
|
|
41
41
|
await qb.unsafe(
|
|
42
42
|
'DELETE FROM card_comments WHERE card_id IN (SELECT id FROM cards WHERE column_id = ?)',
|
|
43
43
|
[id],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { db } from '@stacksjs/database'
|
|
3
4
|
import { Board, BoardColumn } from '@stacksjs/orm'
|
|
@@ -11,7 +12,7 @@ interface ColumnInput {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* `POST /api/dashboard/kanban/columns
|
|
15
|
+
* `POST /api/dashboard/kanban/columns`.
|
|
15
16
|
*
|
|
16
17
|
* Appends a new column to the end of the target board. Validates that
|
|
17
18
|
* `boardId` points at a real board so a typo doesn't silently create
|
|
@@ -22,8 +23,8 @@ export default new Action({
|
|
|
22
23
|
description: 'Creates a new column on a board.',
|
|
23
24
|
method: 'POST',
|
|
24
25
|
apiResponse: true,
|
|
25
|
-
async handle(request) {
|
|
26
|
-
const body = (
|
|
26
|
+
async handle(request: RequestInstance<ColumnInput>) {
|
|
27
|
+
const body = request.all()
|
|
27
28
|
|
|
28
29
|
const boardId = Number(body.boardId)
|
|
29
30
|
if (!Number.isFinite(boardId) || boardId <= 0) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { BoardColumn } from '@stacksjs/orm'
|
|
3
4
|
import { modelNullableString, modelNumber, modelString, modelValue, refreshModel } from './kanban-model'
|
|
@@ -10,7 +11,7 @@ interface ColumnInput {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* `PATCH /api/dashboard/kanban/columns/:id
|
|
14
|
+
* `PATCH /api/dashboard/kanban/columns/:id`.
|
|
14
15
|
*
|
|
15
16
|
* Partial update for name / color / card limit. `position` and
|
|
16
17
|
* `boardId` are NOT updatable here — moving a column between boards
|
|
@@ -23,14 +24,13 @@ export default new Action({
|
|
|
23
24
|
description: 'Partial update of a board column.',
|
|
24
25
|
method: 'PATCH',
|
|
25
26
|
apiResponse: true,
|
|
26
|
-
async handle(request) {
|
|
27
|
-
const
|
|
28
|
-
const id = Number(rawId)
|
|
27
|
+
async handle(request: RequestInstance<ColumnInput>) {
|
|
28
|
+
const id = Number(request.getParam('id'))
|
|
29
29
|
if (!Number.isFinite(id) || id <= 0) {
|
|
30
30
|
return kanbanError('Invalid column id', 400)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const body = (
|
|
33
|
+
const body = request.all()
|
|
34
34
|
const set: Record<string, unknown> = {}
|
|
35
35
|
|
|
36
36
|
if (typeof body.name === 'string') {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { db } from '@stacksjs/database'
|
|
3
4
|
import { kanbanError } from './kanban-response'
|
|
@@ -8,7 +9,7 @@ interface ReorderInput {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* `POST /api/dashboard/kanban/columns/reorder
|
|
12
|
+
* `POST /api/dashboard/kanban/columns/reorder`.
|
|
12
13
|
*
|
|
13
14
|
* Bulk rewrite column positions for a board. The submitted `order`
|
|
14
15
|
* is an array of column ids in their new display order; index becomes
|
|
@@ -27,8 +28,8 @@ export default new Action({
|
|
|
27
28
|
description: 'Bulk-rewrite `position` on a board\'s columns.',
|
|
28
29
|
method: 'POST',
|
|
29
30
|
apiResponse: true,
|
|
30
|
-
async handle(request) {
|
|
31
|
-
const body = (
|
|
31
|
+
async handle(request: RequestInstance<ReorderInput>) {
|
|
32
|
+
const body = request.all()
|
|
32
33
|
|
|
33
34
|
const boardId = Number(body.boardId)
|
|
34
35
|
if (!Number.isFinite(boardId) || boardId <= 0) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { db } from '@stacksjs/database'
|
|
3
4
|
import { kanbanError } from './kanban-response'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* `DELETE /api/dashboard/kanban/labels/:id
|
|
7
|
+
* `DELETE /api/dashboard/kanban/labels/:id`.
|
|
7
8
|
*
|
|
8
9
|
* Removes a label and detaches it from every card that carries it.
|
|
9
10
|
* The label row goes from the `labels` table; every pivot row in
|
|
@@ -15,9 +16,8 @@ export default new Action({
|
|
|
15
16
|
description: 'Hard-deletes a label and its card_labels pivot rows.',
|
|
16
17
|
method: 'DELETE',
|
|
17
18
|
apiResponse: true,
|
|
18
|
-
async handle(request) {
|
|
19
|
-
const
|
|
20
|
-
const id = Number(rawId)
|
|
19
|
+
async handle(request: RequestInstance) {
|
|
20
|
+
const id = Number(request.getParam('id'))
|
|
21
21
|
if (!Number.isFinite(id) || id <= 0)
|
|
22
22
|
return kanbanError('Invalid label id', 400)
|
|
23
23
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { Board, Label } from '@stacksjs/orm'
|
|
3
4
|
import { kanbanError } from './kanban-response'
|
|
@@ -9,7 +10,7 @@ interface LabelInput {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* `POST /api/dashboard/kanban/labels
|
|
13
|
+
* `POST /api/dashboard/kanban/labels`.
|
|
13
14
|
*
|
|
14
15
|
* Creates a label scoped to a board. Labels are unique per
|
|
15
16
|
* `(board_id, name)` — the migration enforces this via a unique
|
|
@@ -21,8 +22,8 @@ export default new Action({
|
|
|
21
22
|
description: 'Creates a label on a board.',
|
|
22
23
|
method: 'POST',
|
|
23
24
|
apiResponse: true,
|
|
24
|
-
async handle(request) {
|
|
25
|
-
const body = (
|
|
25
|
+
async handle(request: RequestInstance<LabelInput>) {
|
|
26
|
+
const body = request.all()
|
|
26
27
|
|
|
27
28
|
const boardId = Number(body.boardId)
|
|
28
29
|
if (!Number.isFinite(boardId) || boardId <= 0) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { Label } from '@stacksjs/orm'
|
|
3
4
|
import { modelNumber, modelString, refreshModel } from './kanban-model'
|
|
@@ -9,7 +10,7 @@ interface LabelInput {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* `PATCH /api/dashboard/kanban/labels/:id
|
|
13
|
+
* `PATCH /api/dashboard/kanban/labels/:id`.
|
|
13
14
|
*
|
|
14
15
|
* Partial update for label name + color. `board_id` is not movable —
|
|
15
16
|
* a label on board A renaming to "Bug" must not collide with a
|
|
@@ -21,14 +22,13 @@ export default new Action({
|
|
|
21
22
|
description: 'Partial update of a label name / color.',
|
|
22
23
|
method: 'PATCH',
|
|
23
24
|
apiResponse: true,
|
|
24
|
-
async handle(request) {
|
|
25
|
-
const
|
|
26
|
-
const id = Number(rawId)
|
|
25
|
+
async handle(request: RequestInstance<LabelInput>) {
|
|
26
|
+
const id = Number(request.getParam('id'))
|
|
27
27
|
if (!Number.isFinite(id) || id <= 0) {
|
|
28
28
|
return kanbanError('Invalid label id', 400)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const body = (
|
|
31
|
+
const body = request.all()
|
|
32
32
|
const set: Record<string, unknown> = {}
|
|
33
33
|
let renamingTo: string | null = null
|
|
34
34
|
|
|
@@ -3,7 +3,7 @@ import { db } from '@stacksjs/database'
|
|
|
3
3
|
import { kanbanError } from './kanban-response'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* `GET /api/dashboard/kanban/users
|
|
6
|
+
* `GET /api/dashboard/kanban/users`.
|
|
7
7
|
*
|
|
8
8
|
* Lightweight user list for the assignee picker on the card detail
|
|
9
9
|
* modal. Returns `{ id, name, email }` only — no roles, no profile,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { modelNullableNumber, modelNullableString, modelNumber, modelString } from './kanban-model'
|
|
2
|
+
|
|
3
|
+
export function cardCommentResponse(record: object, author?: object | null) {
|
|
4
|
+
return {
|
|
5
|
+
id: modelNumber(record, 'id'),
|
|
6
|
+
uuid: modelNullableString(record, 'uuid'),
|
|
7
|
+
userId: modelNullableNumber(record, 'userId', 'user_id'),
|
|
8
|
+
body: modelString(record, 'body'),
|
|
9
|
+
authorName: author ? modelNullableString(author, 'name') : null,
|
|
10
|
+
authorEmail: author ? modelNullableString(author, 'email') : null,
|
|
11
|
+
createdAt: modelNullableString(record, 'createdAt', 'created_at'),
|
|
12
|
+
updatedAt: modelNullableString(record, 'updatedAt', 'updated_at'),
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { cardCommentResponse } from './kanban-comment'
|
|
2
3
|
import {
|
|
3
4
|
modelBoolean,
|
|
5
|
+
modelNullableNumber,
|
|
4
6
|
modelNullableString,
|
|
5
7
|
modelNumber,
|
|
6
8
|
modelString,
|
|
@@ -31,12 +33,26 @@ describe('kanban model response values', () => {
|
|
|
31
33
|
})
|
|
32
34
|
|
|
33
35
|
test('normalizes nullable strings and persisted booleans', () => {
|
|
36
|
+
expect(modelNullableNumber(record({ value: null }), 'value')).toBeNull()
|
|
37
|
+
expect(modelNullableNumber(record({ value: '42' }), 'value')).toBe(42)
|
|
34
38
|
expect(modelNullableString(record({ value: null }), 'value')).toBeNull()
|
|
35
39
|
expect(modelBoolean(record({ enabled: 1 }), 'enabled')).toBe(true)
|
|
36
40
|
expect(modelBoolean(record({ enabled: 'true' }), 'enabled')).toBe(true)
|
|
37
41
|
expect(modelBoolean(record({ enabled: 0 }), 'enabled')).toBe(false)
|
|
38
42
|
})
|
|
39
43
|
|
|
44
|
+
test('reads raw query rows as well as ORM model records', () => {
|
|
45
|
+
const row = {
|
|
46
|
+
board_id: 4,
|
|
47
|
+
archived: 1,
|
|
48
|
+
created_at: '2026-08-10 12:00:00',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expect(modelNumber(row, 'boardId', 'board_id')).toBe(4)
|
|
52
|
+
expect(modelBoolean(row, 'archived')).toBe(true)
|
|
53
|
+
expect(modelString(row, 'createdAt', 'created_at')).toBe('2026-08-10 12:00:00')
|
|
54
|
+
})
|
|
55
|
+
|
|
40
56
|
test('refreshes generated database values after a write', async () => {
|
|
41
57
|
const stale = {
|
|
42
58
|
...record({ updated_at: null }),
|
|
@@ -46,4 +62,24 @@ describe('kanban model response values', () => {
|
|
|
46
62
|
const refreshed = await refreshModel(stale)
|
|
47
63
|
expect(modelString(refreshed, 'updatedAt', 'updated_at')).toBe('2026-07-31 04:00:00')
|
|
48
64
|
})
|
|
65
|
+
|
|
66
|
+
test('serializes model-backed comments with their author', () => {
|
|
67
|
+
expect(cardCommentResponse(record({
|
|
68
|
+
id: 7,
|
|
69
|
+
uuid: 'comment-uuid',
|
|
70
|
+
userId: 3,
|
|
71
|
+
body: 'Updated note',
|
|
72
|
+
createdAt: '2026-08-10 20:00:00',
|
|
73
|
+
updatedAt: '2026-08-10 21:00:00',
|
|
74
|
+
}), record({ name: 'Chris', email: 'chris@example.com' }))).toEqual({
|
|
75
|
+
id: 7,
|
|
76
|
+
uuid: 'comment-uuid',
|
|
77
|
+
userId: 3,
|
|
78
|
+
body: 'Updated note',
|
|
79
|
+
authorName: 'Chris',
|
|
80
|
+
authorEmail: 'chris@example.com',
|
|
81
|
+
createdAt: '2026-08-10 20:00:00',
|
|
82
|
+
updatedAt: '2026-08-10 21:00:00',
|
|
83
|
+
})
|
|
84
|
+
})
|
|
49
85
|
})
|
|
@@ -10,28 +10,35 @@ export async function refreshModel(record: RefreshableKanbanModelRecord): Promis
|
|
|
10
10
|
return await record.fresh() ?? record
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function modelValue(record:
|
|
14
|
-
const
|
|
13
|
+
export function modelValue(record: object, camelKey: string, snakeKey = camelKey): unknown {
|
|
14
|
+
const model = record as Partial<KanbanModelRecord>
|
|
15
|
+
const row = record as Record<string, unknown>
|
|
16
|
+
const camelValue = typeof model.get === 'function' ? model.get(camelKey) : row[camelKey]
|
|
15
17
|
if (camelValue !== undefined)
|
|
16
18
|
return camelValue
|
|
17
|
-
return
|
|
19
|
+
return typeof model.get === 'function' ? model.get(snakeKey) : row[snakeKey]
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
export function modelNumber(record:
|
|
22
|
+
export function modelNumber(record: object, camelKey: string, snakeKey = camelKey): number {
|
|
21
23
|
return Number(modelValue(record, camelKey, snakeKey))
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
export function
|
|
26
|
+
export function modelNullableNumber(record: object, camelKey: string, snakeKey = camelKey): number | null {
|
|
27
|
+
const value = modelValue(record, camelKey, snakeKey)
|
|
28
|
+
return value === undefined || value === null ? null : Number(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function modelString(record: object, camelKey: string, snakeKey = camelKey): string {
|
|
25
32
|
const value = modelValue(record, camelKey, snakeKey)
|
|
26
33
|
return value === undefined || value === null ? '' : String(value)
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
export function modelNullableString(record:
|
|
36
|
+
export function modelNullableString(record: object, camelKey: string, snakeKey = camelKey): string | null {
|
|
30
37
|
const value = modelValue(record, camelKey, snakeKey)
|
|
31
38
|
return value === undefined || value === null ? null : String(value)
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
export function modelBoolean(record:
|
|
41
|
+
export function modelBoolean(record: object, camelKey: string, snakeKey = camelKey): boolean {
|
|
35
42
|
const value = modelValue(record, camelKey, snakeKey)
|
|
36
43
|
return value === true || value === 1 || value === '1' || value === 'true'
|
|
37
44
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { parseRowId, resolveWritableModel } from './model-write'
|
|
3
4
|
|
|
@@ -13,12 +14,12 @@ export default new Action({
|
|
|
13
14
|
description: 'Deletes one row of a model from the dashboard model browser.',
|
|
14
15
|
method: 'DELETE',
|
|
15
16
|
apiResponse: true,
|
|
16
|
-
async handle(
|
|
17
|
-
const resolved = await resolveWritableModel(
|
|
17
|
+
async handle(request: RequestInstance) {
|
|
18
|
+
const resolved = await resolveWritableModel(request.getParam('slug'), 'destroy')
|
|
18
19
|
if ('error' in resolved)
|
|
19
20
|
return { ok: false, error: resolved.error }
|
|
20
21
|
|
|
21
|
-
const id = parseRowId(
|
|
22
|
+
const id = parseRowId(request.getParam('id'))
|
|
22
23
|
if (id === null)
|
|
23
24
|
return { ok: false, error: 'A numeric row id is required.' }
|
|
24
25
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { env } from '@stacksjs/env'
|
|
3
|
-
import {
|
|
4
|
+
import { response as routerResponse } from '@stacksjs/router'
|
|
4
5
|
import { loadModelIfExists, safeGet } from '../../../../resources/functions/dashboard/data'
|
|
5
6
|
import { isValidModelSlug, type ModelCreateField, type ModelWriteCapabilities, modelCreateFields, modelSchemaColumns, modelWriteCapabilities, slugToPascal } from './model-write'
|
|
6
7
|
|
|
@@ -108,14 +109,11 @@ function typeForColumn(column: string, values: unknown[]): ColumnMeta['type'] {
|
|
|
108
109
|
return inferType(values)
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
params.set(key, Array.isArray(value) ? String(value[0]) : String(value))
|
|
117
|
-
}
|
|
118
|
-
return params
|
|
112
|
+
function queryValue(request: RequestInstance, key: string, fallback = ''): string {
|
|
113
|
+
const value = request.get<unknown>(key, fallback)
|
|
114
|
+
if (Array.isArray(value))
|
|
115
|
+
return value.length > 0 ? String(value[0]) : fallback
|
|
116
|
+
return value == null ? fallback : String(value)
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
/** `filters` arrives as a JSON object so column names stay unambiguous. */
|
|
@@ -176,23 +174,22 @@ export default new Action({
|
|
|
176
174
|
description: 'Queries a single model by URL slug with paging, sorting, search and column filters.',
|
|
177
175
|
method: 'GET',
|
|
178
176
|
apiResponse: true,
|
|
179
|
-
async handle(
|
|
180
|
-
const slug =
|
|
177
|
+
async handle(request: RequestInstance) {
|
|
178
|
+
const slug = request.getParam('slug')
|
|
181
179
|
if (!isValidModelSlug(slug))
|
|
182
180
|
return routerResponse.json({ message: 'Model slug must be lowercase kebab-case.' }, 400)
|
|
183
181
|
|
|
184
182
|
const modelName = slugToPascal(slug)
|
|
185
183
|
const tableName = pluralize(pascalToSnake(modelName))
|
|
186
184
|
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const q = (params.get('q') || '').trim()
|
|
185
|
+
const page = Math.max(1, Number.parseInt(queryValue(request, 'page', '1'), 10) || 1)
|
|
186
|
+
const perPage = Math.min(MAX_PER_PAGE, Math.max(1, Number.parseInt(queryValue(request, 'per_page', String(DEFAULT_PER_PAGE)), 10) || DEFAULT_PER_PAGE))
|
|
187
|
+
const requestedSort = queryValue(request, 'sort').trim()
|
|
188
|
+
const dir: 'asc' | 'desc' = queryValue(request, 'dir') === 'asc' ? 'asc' : 'desc'
|
|
189
|
+
const q = queryValue(request, 'q').trim()
|
|
193
190
|
let filters: Record<string, string>
|
|
194
191
|
try {
|
|
195
|
-
filters = parseFilters(
|
|
192
|
+
filters = parseFilters(queryValue(request, 'filters') || null)
|
|
196
193
|
}
|
|
197
194
|
catch (error) {
|
|
198
195
|
return routerResponse.json({
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { toSnakeCaseKeys } from '@stacksjs/orm'
|
|
3
|
-
import { request } from '@stacksjs/router'
|
|
4
4
|
import { prepareModelFields, resolveWritableModel } from './model-write'
|
|
5
5
|
|
|
6
|
+
interface ModelWriteInput {
|
|
7
|
+
fields?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export default new Action({
|
|
7
11
|
name: 'Dashboard Model Store',
|
|
8
12
|
description: 'Creates one row through a model declared useApi store contract.',
|
|
9
13
|
method: 'POST',
|
|
10
14
|
apiResponse: true,
|
|
11
|
-
async handle(
|
|
12
|
-
|
|
13
|
-
all?: () => Record<string, unknown>
|
|
14
|
-
route?: { params?: { slug?: string } }
|
|
15
|
-
}) {
|
|
16
|
-
const resolved = await resolveWritableModel(String(req?.getParam?.('slug') ?? req?.route?.params?.slug ?? ''), 'store')
|
|
15
|
+
async handle(request: RequestInstance<ModelWriteInput>) {
|
|
16
|
+
const resolved = await resolveWritableModel(request.getParam('slug'), 'store')
|
|
17
17
|
if ('error' in resolved)
|
|
18
18
|
return { ok: false, error: resolved.error }
|
|
19
19
|
|
|
20
|
-
const input =
|
|
20
|
+
const input = request.all()
|
|
21
21
|
const raw = input.fields
|
|
22
22
|
const body = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw as Record<string, unknown> : {}
|
|
23
23
|
const { data, errors } = prepareModelFields(resolved.Model, body)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { toSnakeCaseKeys } from '@stacksjs/orm'
|
|
3
|
-
import { request } from '@stacksjs/router'
|
|
4
4
|
import { parseRowId, prepareModelFields, resolveWritableModel } from './model-write'
|
|
5
5
|
|
|
6
|
+
interface ModelWriteInput {
|
|
7
|
+
fields?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* `PATCH /api/dashboard/models/{slug}/{id}`.
|
|
8
12
|
*
|
|
@@ -16,16 +20,12 @@ export default new Action({
|
|
|
16
20
|
description: 'Updates one row of a model from the dashboard model browser.',
|
|
17
21
|
method: 'PATCH',
|
|
18
22
|
apiResponse: true,
|
|
19
|
-
async handle(
|
|
20
|
-
|
|
21
|
-
all?: () => Record<string, unknown>
|
|
22
|
-
route?: { params?: { slug?: string, id?: string } }
|
|
23
|
-
}) {
|
|
24
|
-
const resolved = await resolveWritableModel(String(req?.getParam?.('slug') ?? req?.route?.params?.slug ?? ''), 'update')
|
|
23
|
+
async handle(request: RequestInstance<ModelWriteInput>) {
|
|
24
|
+
const resolved = await resolveWritableModel(request.getParam('slug'), 'update')
|
|
25
25
|
if ('error' in resolved)
|
|
26
26
|
return { ok: false, error: resolved.error }
|
|
27
27
|
|
|
28
|
-
const id = parseRowId(
|
|
28
|
+
const id = parseRowId(request.getParam('id'))
|
|
29
29
|
if (id === null)
|
|
30
30
|
return { ok: false, error: 'A numeric row id is required.' }
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ export default new Action({
|
|
|
35
35
|
// columns — and `slug` is a real column on plenty of models, so it
|
|
36
36
|
// cannot simply be blocklisted. One level of nesting keeps the two
|
|
37
37
|
// namespaces apart.
|
|
38
|
-
const input =
|
|
38
|
+
const input = request.all()
|
|
39
39
|
const raw = input.fields
|
|
40
40
|
const body = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw as Record<string, unknown> : {}
|
|
41
41
|
const prepared = prepareModelFields(resolved.Model, body, true)
|
|
@@ -3,6 +3,7 @@ import { existsSync, readdirSync } from 'node:fs'
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import process from 'node:process'
|
|
5
5
|
import { countRows } from '../../../../resources/functions/dashboard/data'
|
|
6
|
+
import { modelApiConfiguration } from './model-api'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* `GET /api/dashboard/models` (stacksjs/stacks#1838).
|
|
@@ -135,7 +136,7 @@ export default new Action({
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
const api = Model
|
|
139
|
+
const api = modelApiConfiguration(Model)
|
|
139
140
|
let count: number | null = null
|
|
140
141
|
let error: string | null = null
|
|
141
142
|
try {
|
|
@@ -153,8 +154,8 @@ export default new Action({
|
|
|
153
154
|
attributeCount: Object.keys(Model.attributes || {}).length,
|
|
154
155
|
category: categorize(m),
|
|
155
156
|
source: m.source,
|
|
156
|
-
apiUri:
|
|
157
|
-
apiRoutes:
|
|
157
|
+
apiUri: api.uri ? `/api/${api.uri}` : '',
|
|
158
|
+
apiRoutes: api.routes,
|
|
158
159
|
error,
|
|
159
160
|
}
|
|
160
161
|
}))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const DEFAULT_MODEL_API_ROUTES = ['index', 'show', 'store', 'update', 'destroy'] as const
|
|
2
|
+
|
|
3
|
+
export interface ModelApiConfiguration {
|
|
4
|
+
uri: string
|
|
5
|
+
routes: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a model's useApi trait exactly as the ORM route generator does.
|
|
10
|
+
* An enabled trait without explicit routes receives the complete REST set,
|
|
11
|
+
* while an explicit empty routes array remains intentionally empty.
|
|
12
|
+
*/
|
|
13
|
+
export function modelApiConfiguration(Model: any): ModelApiConfiguration {
|
|
14
|
+
const useApi = Model?.traits?.useApi
|
|
15
|
+
if (!useApi)
|
|
16
|
+
return { uri: '', routes: [] }
|
|
17
|
+
|
|
18
|
+
const config = typeof useApi === 'object' ? useApi : {}
|
|
19
|
+
const fallbackUri = String(Model?.table || `${String(Model?.name || '').toLowerCase()}s`)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
uri: String(config.uri || fallbackUri),
|
|
23
|
+
routes: Array.isArray(config.routes)
|
|
24
|
+
? config.routes.map(String)
|
|
25
|
+
: [...DEFAULT_MODEL_API_ROUTES],
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* bypass every guarantee the ORM makes. Those rows stay read-only.
|
|
8
8
|
*/
|
|
9
9
|
import { loadModelIfExists } from '../../../../resources/functions/dashboard/data'
|
|
10
|
+
import { modelApiConfiguration } from './model-api'
|
|
10
11
|
|
|
11
12
|
/** Never writable from a generic admin table, whatever the model declares. */
|
|
12
13
|
export const PROTECTED_COLUMNS: ReadonlySet<string> = new Set([
|
|
@@ -121,17 +122,8 @@ function isProtectedField(name: string): boolean {
|
|
|
121
122
|
return PROTECTED_COLUMNS.has(name) || PROTECTED_COLUMNS.has(column)
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
function useApiRoutes(Model: any): string[] {
|
|
125
|
-
const useApi = Model?.traits?.useApi
|
|
126
|
-
if (!useApi)
|
|
127
|
-
return []
|
|
128
|
-
if (typeof useApi === 'object' && Array.isArray(useApi.routes))
|
|
129
|
-
return useApi.routes.map(String)
|
|
130
|
-
return ['index', 'store', 'show', 'update', 'destroy']
|
|
131
|
-
}
|
|
132
|
-
|
|
133
125
|
export function modelWriteCapabilities(Model: any): ModelWriteCapabilities {
|
|
134
|
-
const routes =
|
|
126
|
+
const routes = modelApiConfiguration(Model).routes
|
|
135
127
|
return {
|
|
136
128
|
create: routes.includes('store'),
|
|
137
129
|
update: routes.includes('update'),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
|
-
import {
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { dashboardRequestValue } from '../dashboard-request'
|
|
5
6
|
import {
|
|
6
7
|
type NotificationDeliveryRow,
|
|
7
8
|
serializeNotificationDelivery,
|
|
@@ -16,9 +17,7 @@ type DeliveryStatus = typeof STATUSES[number]
|
|
|
16
17
|
type DeliverySort = typeof SORTS[number]
|
|
17
18
|
|
|
18
19
|
function queryValue(request: RequestInstance, key: string): string {
|
|
19
|
-
|
|
20
|
-
const value = query[key]
|
|
21
|
-
return String((Array.isArray(value) ? value[0] : value) || request.get(key) || '').trim()
|
|
20
|
+
return dashboardRequestValue(request, key)
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
function allowedValue<T extends string>(value: string, values: readonly T[]): T | null {
|