@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.
Files changed (125) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +17 -2
  2. package/ai/skills/stacks-browse/scripts/browse.ts +207 -6
  3. package/ai/skills/stacks-dashboard/scripts/audit.ts +0 -1
  4. package/ai/skills/stacks-email/SKILL.md +35 -15
  5. package/app/Actions/Dashboard/Auth/MeAction.ts +8 -7
  6. package/app/Actions/Dashboard/Ci/RepoRunJobsAction.ts +10 -15
  7. package/app/Actions/Dashboard/Ci/RepoRunsAction.ts +12 -14
  8. package/app/Actions/Dashboard/Ci/RunnerHistoryAction.ts +11 -10
  9. package/app/Actions/Dashboard/Ci/StatusAction.ts +2 -11
  10. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +1 -0
  11. package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +3 -4
  12. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +1 -0
  13. package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +1 -0
  14. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -0
  15. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -0
  16. package/app/Actions/Dashboard/Commerce/tax-rate-records.ts +9 -0
  17. package/app/Actions/Dashboard/Content/author-input.ts +1 -1
  18. package/app/Actions/Dashboard/Content/page-input.ts +1 -1
  19. package/app/Actions/Dashboard/DashboardStatsAction.ts +0 -1
  20. package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +3 -2
  21. package/app/Actions/Dashboard/Deployments/GetDeployment.ts +1 -0
  22. package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +3 -2
  23. package/app/Actions/Dashboard/Email/InboxActivityAction.ts +28 -16
  24. package/app/Actions/Dashboard/Email/InboxAttachmentDownloadAction.ts +45 -0
  25. package/app/Actions/Dashboard/Email/InboxDeleteAction.ts +11 -6
  26. package/app/Actions/Dashboard/Email/InboxIndexAction.ts +5 -7
  27. package/app/Actions/Dashboard/Email/InboxMarkReadAction.ts +11 -6
  28. package/app/Actions/Dashboard/Email/InboxMarkUnreadAction.ts +11 -6
  29. package/app/Actions/Dashboard/Email/InboxPreferenceShowAction.ts +10 -4
  30. package/app/Actions/Dashboard/Email/InboxPreferenceUpdateAction.ts +8 -9
  31. package/app/Actions/Dashboard/Email/InboxShowAction.ts +7 -7
  32. package/app/Actions/Dashboard/Email/InboxStatsAction.ts +5 -7
  33. package/app/Actions/Dashboard/Email/inbox-request.ts +33 -0
  34. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +3 -4
  35. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +3 -6
  36. package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +8 -15
  37. package/app/Actions/Dashboard/Jobs/JobRetryAction.ts +4 -3
  38. package/app/Actions/Dashboard/Jobs/JobShowAction.ts +3 -3
  39. package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +5 -5
  40. package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +9 -16
  41. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +4 -3
  42. package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +5 -5
  43. package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +1 -2
  44. package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +4 -3
  45. package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +11 -9
  46. package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +10 -12
  47. package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +19 -43
  48. package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +44 -0
  49. package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +5 -5
  50. package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +6 -6
  51. package/app/Actions/Dashboard/Kanban/CardShowAction.ts +4 -4
  52. package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +7 -6
  53. package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +5 -5
  54. package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +4 -3
  55. package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +5 -5
  56. package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +4 -3
  57. package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +5 -5
  58. package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +4 -3
  59. package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +4 -4
  60. package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +4 -3
  61. package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +5 -5
  62. package/app/Actions/Dashboard/Kanban/UsersListAction.ts +1 -1
  63. package/app/Actions/Dashboard/Kanban/kanban-comment.ts +14 -0
  64. package/app/Actions/Dashboard/Kanban/kanban-model.test.ts +36 -0
  65. package/app/Actions/Dashboard/Kanban/kanban-model.ts +14 -7
  66. package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +4 -3
  67. package/app/Actions/Dashboard/Models/ModelShowAction.ts +15 -18
  68. package/app/Actions/Dashboard/Models/ModelStoreAction.ts +8 -8
  69. package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +9 -9
  70. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +4 -3
  71. package/app/Actions/Dashboard/Models/model-api.ts +27 -0
  72. package/app/Actions/Dashboard/Models/model-write.ts +2 -10
  73. package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +3 -4
  74. package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +6 -4
  75. package/app/Actions/Dashboard/Queries/QueryShowAction.ts +1 -0
  76. package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +4 -4
  77. package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +3 -2
  78. package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +4 -4
  79. package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +4 -4
  80. package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +4 -3
  81. package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +3 -2
  82. package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +4 -5
  83. package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +4 -4
  84. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -0
  85. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +3 -3
  86. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +2 -2
  87. package/app/Actions/Dashboard/dashboard-request.ts +13 -0
  88. package/app/Models/CardComment.ts +3 -4
  89. package/app/Models/commerce/TaxRate.ts +48 -0
  90. package/functions/dashboard-api.ts +51 -10
  91. package/functions/inbox.test.ts +2 -0
  92. package/functions/inbox.ts +27 -2
  93. package/functions/public-application-url.test.ts +13 -0
  94. package/functions/public-application-url.ts +27 -0
  95. package/ide/vscode/package.json +1 -1
  96. package/package.json +1 -1
  97. package/resources/components/Dashboard/Auth/AccessTokens.stx +15 -0
  98. package/resources/components/Dashboard/Ci/CiDashboard.stx +24 -6
  99. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +23 -0
  100. package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +4 -0
  101. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +9 -1
  102. package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +8 -7
  103. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +8 -7
  104. package/resources/components/Dashboard/Content/BlogDashboard.stx +14 -2
  105. package/resources/components/Dashboard/Content/PostsDashboard.stx +7 -1
  106. package/resources/components/Dashboard/Deployments/DeploymentDetail.stx +16 -2
  107. package/resources/components/Dashboard/Email/EmailNavigation.stx +5 -5
  108. package/resources/components/Dashboard/Email/EmailSidebar.stx +1 -1
  109. package/resources/components/Dashboard/Email/InboxAttachmentList.stx +53 -0
  110. package/resources/components/Dashboard/Email/InboxDashboard.stx +5 -4
  111. package/resources/components/Dashboard/Email/InboxMessageDetail.stx +6 -4
  112. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +63 -6
  113. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +3 -6
  114. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +4 -4
  115. package/resources/components/Dashboard/NotFoundDashboard.stx +2 -5
  116. package/resources/components/Dashboard/Queries/QueryDashboard.stx +6 -2
  117. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +4 -4
  118. package/resources/components/Dashboard/UI/EmptyState.stx +2 -2
  119. package/resources/components/Dashboard/UI/PageLayout.stx +3 -3
  120. package/resources/functions/dashboard/sidebar.ts +4 -4
  121. package/routes/dashboard-api.ts +8 -20
  122. package/views/dashboard/.discovered-models.json +24 -0
  123. package/views/dashboard/layouts/default.stx +19 -16
  124. package/views/dashboard/stores/ci.ts +13 -7
  125. package/views/dashboard/stores/kanban.ts +48 -11
@@ -1,26 +1,19 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { FailedJob, Job } from '@stacksjs/orm'
3
- import { request, response } from '@stacksjs/router'
4
+ import { response } from '@stacksjs/router'
4
5
  import { matchesJobSearch, normalizeActiveJob, normalizeFailedJob } from './job-records'
5
6
 
6
7
  export default new Action({
7
8
  name: 'JobIndexAction',
8
9
  description: 'Returns a paginated list of jobs across the active queue and the failed_jobs table.',
9
10
  method: 'GET',
10
- async handle() {
11
- // bun-router populates `request.query` as a Record<string, string|string[]>.
12
- // Outside a request context (e.g. ad-hoc tests) it's undefined — fall back
13
- // to defaults so the action stays callable.
14
- const query = ((request as any).query || {}) as Record<string, string | string[] | undefined>
15
- const pick = (key: string) => {
16
- const v = query[key]
17
- return Array.isArray(v) ? v[0] : v
18
- }
19
- const page = Math.max(1, Number(pick('page')) || 1)
20
- const perPage = Math.min(200, Math.max(1, Number(pick('per_page')) || 25))
21
- const queueFilter = String(pick('queue') || '').trim()
22
- const statusFilter = String(pick('status') || '').trim().toLowerCase()
23
- const search = String(pick('search') || '').trim().toLowerCase()
11
+ async handle(request: RequestInstance) {
12
+ const page = Math.max(1, Number(request.get('page', 1)) || 1)
13
+ const perPage = Math.min(200, Math.max(1, Number(request.get('per_page', 25)) || 25))
14
+ const queueFilter = String(request.get('queue', '')).trim()
15
+ const statusFilter = String(request.get('status', '')).trim().toLowerCase()
16
+ const search = String(request.get('search', '')).trim().toLowerCase()
24
17
 
25
18
  try {
26
19
  const [activeJobs, failedJobs] = await Promise.all([
@@ -1,5 +1,6 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
- import { request, response } from '@stacksjs/router'
3
+ import { response } from '@stacksjs/router'
3
4
  import { retryFailedJob } from '@stacksjs/queue'
4
5
  import { parseJobReference } from './job-records'
5
6
 
@@ -7,8 +8,8 @@ export default new Action({
7
8
  name: 'JobRetryAction',
8
9
  description: 'Retries a single failed job by its failed_jobs id.',
9
10
  method: 'POST',
10
- async handle() {
11
- const reference = String((request as any).params?.id || '')
11
+ async handle(request: RequestInstance) {
12
+ const reference = request.getParam('id')
12
13
  const parsed = parseJobReference(reference)
13
14
  const id = Number(parsed.id)
14
15
  if (parsed.source === 'job') {
@@ -1,14 +1,14 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { FailedJob, Job } from '@stacksjs/orm'
3
- import { request } from '@stacksjs/router'
4
4
  import { normalizeActiveJob, normalizeFailedJob, parseJobReference } from './job-records'
5
5
 
6
6
  export default new Action({
7
7
  name: 'JobShowAction',
8
8
  description: 'Returns one native Job or FailedJob record for dashboard inspection.',
9
9
  method: 'GET',
10
- async handle() {
11
- const reference = String((request as any).params?.id || '')
10
+ async handle(request: RequestInstance) {
11
+ const reference = request.getParam('id')
12
12
  const parsed = parseJobReference(reference)
13
13
  const id = Number(parsed.id)
14
14
 
@@ -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/boards/:id` (stacksjs/stacks#1846 Phase 2).
7
+ * `DELETE /api/dashboard/kanban/boards/:id`.
7
8
  *
8
9
  * Hard delete with cascade: removes the board, its columns, its cards,
9
10
  * its labels, and the relevant pivot rows in `card_labels` and
@@ -23,9 +24,8 @@ export default new Action({
23
24
  description: 'Hard-deletes a board and all of its columns, cards, labels, and pivot rows.',
24
25
  method: 'DELETE',
25
26
  apiResponse: true,
26
- async handle(request) {
27
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
28
- const id = Number(rawId)
27
+ async handle(request: RequestInstance) {
28
+ const id = Number(request.getParam('id'))
29
29
  if (!Number.isFinite(id) || id <= 0) {
30
30
  return kanbanError('Invalid board id', 400)
31
31
  }
@@ -42,7 +42,7 @@ export default new Action({
42
42
  'DELETE FROM card_assignees WHERE card_id IN (SELECT id FROM cards WHERE board_id = ?)',
43
43
  [id],
44
44
  ).execute()
45
- // Card comments (Phase 3, migration 0000000112). Same
45
+ // Card comments use the same
46
46
  // card-scoped pattern as the pivots.
47
47
  await qb.unsafe(
48
48
  'DELETE FROM card_comments WHERE card_id IN (SELECT id FROM cards WHERE board_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 { modelBoolean } from './kanban-model'
@@ -56,8 +57,8 @@ interface LabelRow {
56
57
  *
57
58
  * Returns a single board with its columns + cards + label palette
58
59
  * nested in render order. Powers the kanban board page
59
- * (`/kanban/[id]`) Phase 2 hits this on mount, the drag handler
60
- * then mutates positions via the (Phase 2) reorder endpoint and
60
+ * (`/kanban/[id]`). The page loads this on mount, then its drag handler
61
+ * mutates positions through the reorder endpoint and
61
62
  * relies on optimistic updates for snappy UX.
62
63
  *
63
64
  * Query layout (3 queries, all indexed):
@@ -67,24 +68,16 @@ interface LabelRow {
67
68
  * `column_id, position`).
68
69
  * 4. All labels for the board.
69
70
  *
70
- * Card-label pivot reads land in Phase 3 alongside the card detail
71
- * modal the board view shows label chips inline, hydrated from a
72
- * single pivot query batched with the card fetch.
71
+ * Card-label pivots hydrate label chips inline through a single query
72
+ * batched with the card fetch.
73
73
  */
74
74
  export default new Action({
75
75
  name: 'Kanban Board Show',
76
76
  description: 'Returns a single kanban board with its columns + cards + label palette.',
77
77
  method: 'GET',
78
78
  apiResponse: true,
79
- async handle(request) {
80
- // Route param resolution: bun-router exposes `:id` via
81
- // `request.param('id')` / `request.params.id`. Both shapes appear
82
- // in this codebase depending on how the route was registered;
83
- // covering both keeps the action portable.
84
- const rawId = (request as any)?.params?.id
85
- ?? (request as any)?.param?.('id')
86
- ?? null
87
- const id = Number(rawId)
79
+ async handle(request: RequestInstance) {
80
+ const id = Number(request.getParam('id'))
88
81
  if (!Number.isFinite(id) || id <= 0) {
89
82
  return kanbanError('Invalid board id', 400)
90
83
  }
@@ -114,8 +107,8 @@ export default new Action({
114
107
  ).execute() as Promise<LabelRow[]>,
115
108
  ])
116
109
 
117
- // Pivot lookups for label + assignee chips on card previews
118
- // (stacksjs/stacks#1846 Phase 3). Two batched JOINs keyed off the
110
+ // Pivot lookups for label + assignee chips on card previews.
111
+ // Two batched JOINs keyed off the
119
112
  // board's card list — single round-trip per pivot, then group
120
113
  // client-side into `cardId → list` maps.
121
114
  const [cardLabelRows, cardAssigneeRows] = await Promise.all([
@@ -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 } from '@stacksjs/orm'
@@ -11,7 +12,7 @@ interface BoardInput {
11
12
  }
12
13
 
13
14
  /**
14
- * `POST /api/dashboard/kanban/boards` (stacksjs/stacks#1846 Phase 2).
15
+ * `POST /api/dashboard/kanban/boards`.
15
16
  *
16
17
  * Creates a new board with sensible defaults and appends it to the
17
18
  * end of the boards list (`position = max(position) + 1` so the new
@@ -28,8 +29,8 @@ export default new Action({
28
29
  description: 'Creates a new kanban board.',
29
30
  method: 'POST',
30
31
  apiResponse: true,
31
- async handle(request) {
32
- const body = (request as any).jsonBody as BoardInput | undefined ?? {}
32
+ async handle(request: RequestInstance<BoardInput>) {
33
+ const body = request.all()
33
34
 
34
35
  const name = typeof body.name === 'string' ? body.name.trim() : ''
35
36
  if (!name || name.length > 120) {
@@ -1,3 +1,4 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { Board } from '@stacksjs/orm'
3
4
  import { modelBoolean, modelNullableString, modelNumber, modelString, refreshModel } from './kanban-model'
@@ -12,7 +13,7 @@ interface BoardInput {
12
13
  }
13
14
 
14
15
  /**
15
- * `PATCH /api/dashboard/kanban/boards/:id` (stacksjs/stacks#1846 Phase 2).
16
+ * `PATCH /api/dashboard/kanban/boards/:id`.
16
17
  *
17
18
  * Partial update — only the fields present in the body are written.
18
19
  * Omitting `archived` does NOT change the archive flag; passing
@@ -30,14 +31,13 @@ export default new Action({
30
31
  description: 'Partial update of a kanban board.',
31
32
  method: 'PATCH',
32
33
  apiResponse: true,
33
- async handle(request) {
34
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
35
- const id = Number(rawId)
34
+ async handle(request: RequestInstance<BoardInput>) {
35
+ const id = Number(request.getParam('id'))
36
36
  if (!Number.isFinite(id) || id <= 0) {
37
37
  return kanbanError('Invalid board id', 400)
38
38
  }
39
39
 
40
- const body = (request as any).jsonBody as BoardInput | undefined ?? {}
40
+ const body = request.all()
41
41
  const set: Record<string, unknown> = {}
42
42
 
43
43
  if (typeof body.name === 'string') {
@@ -30,8 +30,7 @@ interface BoardRow {
30
30
  * group (boards are a dev-mode surface per #1843). The action body
31
31
  * doesn't re-check the role; the middleware did.
32
32
  *
33
- * Phase 1 read-only. Phase 2 lands store / update / destroy + the
34
- * reorder endpoint that moves a board's position.
33
+ * Mutations use the sibling store, update, destroy, and reorder actions.
35
34
  */
36
35
  export default new Action({
37
36
  name: 'Kanban Boards Index',
@@ -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'
@@ -14,7 +15,7 @@ interface ReorderInput {
14
15
  }
15
16
 
16
17
  /**
17
- * `POST /api/dashboard/kanban/boards/reorder` (stacksjs/stacks#1846 Phase 2).
18
+ * `POST /api/dashboard/kanban/boards/reorder`.
18
19
  *
19
20
  * Rewrites the `position` column on every board in the submitted list
20
21
  * to match its index. Wrapped in a transaction so a partial failure
@@ -33,8 +34,8 @@ export default new Action({
33
34
  description: 'Bulk-rewrite `position` on a list of boards to match the submitted order.',
34
35
  method: 'POST',
35
36
  apiResponse: true,
36
- async handle(request) {
37
- const body = (request as any).jsonBody as ReorderInput | undefined ?? {}
37
+ async handle(request: RequestInstance<ReorderInput>) {
38
+ const body = request.all()
38
39
  if (!Array.isArray(body.order) || body.order.length === 0) {
39
40
  return kanbanError('`order` must be a non-empty array of board ids.', 400)
40
41
  }
@@ -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'
@@ -7,7 +8,7 @@ interface SyncInput {
7
8
  }
8
9
 
9
10
  /**
10
- * `POST /api/dashboard/kanban/cards/:id/assignees` (stacksjs/stacks#1846 Phase 3).
11
+ * `POST /api/dashboard/kanban/cards/:id/assignees`.
11
12
  *
12
13
  * Replaces the set of users assigned to a card. Sync semantics —
13
14
  * pass the new full list of user ids, the action diffs against
@@ -20,21 +21,20 @@ interface SyncInput {
20
21
  *
21
22
  * Cross-validation: each user id must exist. Doesn't validate
22
23
  * board-scoped membership — kanban users aren't (currently) team-
23
- * scoped; any platform user can be assigned to any card. Phase 4
24
- * could tighten this if board-membership becomes a concept.
24
+ * scoped; any platform user can be assigned to any card. A future
25
+ * board-membership feature can tighten this contract.
25
26
  */
26
27
  export default new Action({
27
28
  name: 'Kanban Card Assignees Sync',
28
29
  description: 'Replaces the set of users assigned to a card.',
29
30
  method: 'POST',
30
31
  apiResponse: true,
31
- async handle(request) {
32
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
33
- const cardId = Number(rawId)
32
+ async handle(request: RequestInstance<SyncInput>) {
33
+ const cardId = Number(request.getParam('id'))
34
34
  if (!Number.isFinite(cardId) || cardId <= 0)
35
35
  return kanbanError('Invalid card id', 400)
36
36
 
37
- const body = (request as any).jsonBody as SyncInput | undefined ?? {}
37
+ const body = request.all()
38
38
  if (!Array.isArray(body.userIds))
39
39
  return kanbanError('`userIds` must be an array of user ids (possibly empty).', 400)
40
40
 
@@ -63,8 +63,10 @@ export default new Action({
63
63
  return kanbanError('One or more user ids do not exist.', 400)
64
64
  }
65
65
 
66
- const requester = (request as any).user ?? (request as any)._authenticatedUser ?? null
67
- const assignedByUserId = requester && typeof requester.id === 'number' ? requester.id : null
66
+ const requester = await request.user()
67
+ const assignedByUserId = requester && Number.isInteger(Number(requester.id))
68
+ ? Number(requester.id)
69
+ : null
68
70
 
69
71
  await db.transaction(async (rawTrx) => {
70
72
  const qb = rawTrx as unknown as typeof db
@@ -1,29 +1,27 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
- import { db } from '@stacksjs/database'
3
+ import { CardComment } from '@stacksjs/orm'
3
4
  import { kanbanError } from './kanban-response'
4
5
 
5
6
  /**
6
- * `DELETE /api/dashboard/kanban/comments/:id` (stacksjs/stacks#1846 Phase 3).
7
- *
8
- * Hard-deletes a card comment. No authorship check yet — Phase 4
9
- * could add "only the author or an admin can delete" semantics by
10
- * looking at `request.user.id` vs the comment's `user_id`. For now
11
- * the dashboard's role-gated visibility (only admin+dev see the
12
- * surface) is the gate.
7
+ * `DELETE /api/dashboard/kanban/comments/:id`.
8
+ * The dashboard route applies its local-development or admin guard.
13
9
  */
14
10
  export default new Action({
15
11
  name: 'Kanban Card Comment Destroy',
16
12
  description: 'Hard-deletes a single card comment.',
17
13
  method: 'DELETE',
18
14
  apiResponse: true,
19
- async handle(request) {
20
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
21
- const id = Number(rawId)
15
+ async handle(request: RequestInstance) {
16
+ const id = Number(request.getParam('id'))
22
17
  if (!Number.isFinite(id) || id <= 0)
23
18
  return kanbanError('Invalid comment id', 400)
24
19
 
25
20
  try {
26
- await db.deleteFrom('card_comments').where('id', '=', id).execute()
21
+ const comment = await CardComment.find(id)
22
+ if (!comment)
23
+ return kanbanError('Comment not found.', 404)
24
+ await comment.delete()
27
25
  return { deleted: true, id }
28
26
  }
29
27
  catch (err) {
@@ -1,5 +1,8 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
- import { db } from '@stacksjs/database'
3
+ import { Card, CardComment } from '@stacksjs/orm'
4
+ import { cardCommentResponse } from './kanban-comment'
5
+ import { refreshModel } from './kanban-model'
3
6
  import { kanbanError } from './kanban-response'
4
7
 
5
8
  interface CommentInput {
@@ -7,72 +10,45 @@ interface CommentInput {
7
10
  }
8
11
 
9
12
  /**
10
- * `POST /api/dashboard/kanban/cards/:id/comments` (stacksjs/stacks#1846 Phase 3).
13
+ * `POST /api/dashboard/kanban/cards/:id/comments`.
11
14
  *
12
15
  * Appends a comment to a card. Stamps `user_id` from the request's
13
16
  * authenticated user when available; leaves it null on the no-auth
14
17
  * dev dashboard so the surface stays usable locally.
15
18
  *
16
- * Returns the inserted row with denormalised author name/email for
17
- * the optimistic UI to render without a round-trip back through
18
- * `/cards/:id`.
19
+ * Writes through the model so generated API validation, timestamps, and
20
+ * observers stay aligned with the rest of the application.
19
21
  */
20
22
  export default new Action({
21
23
  name: 'Kanban Card Comment Store',
22
24
  description: 'Adds a comment to a card.',
23
25
  method: 'POST',
24
26
  apiResponse: true,
25
- async handle(request) {
26
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
27
- const cardId = Number(rawId)
27
+ async handle(request: RequestInstance<CommentInput>) {
28
+ const cardId = Number(request.getParam('id'))
28
29
  if (!Number.isFinite(cardId) || cardId <= 0)
29
30
  return kanbanError('Invalid card id', 400)
30
31
 
31
- const body = (request as any).jsonBody as CommentInput | undefined ?? {}
32
+ const body = request.all()
32
33
  const text = typeof body.body === 'string' ? body.body.trim() : ''
33
34
  if (!text || text.length > 10000)
34
35
  return kanbanError('`body` is required and must be 1-10000 characters.', 400)
35
36
 
36
37
  try {
37
- const cardRows = await db.unsafe('SELECT id FROM cards WHERE id = ? LIMIT 1', [cardId]).execute() as Array<{ id: number }>
38
- if (!cardRows?.length)
38
+ const card = await Card.find(cardId)
39
+ if (!card)
39
40
  return kanbanError('Card not found.', 404)
40
41
 
41
- const user = (request as any).user ?? (request as any)._authenticatedUser ?? null
42
- const userId = user && typeof user.id === 'number' ? user.id : null
43
-
44
- await db.insertInto('card_comments').values({
45
- card_id: cardId,
46
- user_id: userId,
42
+ const user = await request.user()
43
+ const userId = user && Number.isInteger(Number(user.id)) ? Number(user.id) : null
44
+ const comment = await refreshModel(await CardComment.create({
45
+ cardId,
46
+ userId,
47
47
  body: text,
48
- }).execute()
49
-
50
- // Pull the inserted row + author denormalisation. Ordered by id
51
- // DESC so the freshest matching row wins.
52
- const rows = await db.unsafe(
53
- `SELECT cc.id, cc.uuid, cc.user_id, cc.body, cc.created_at, cc.updated_at, u.name, u.email
54
- FROM card_comments cc
55
- LEFT JOIN users u ON u.id = cc.user_id
56
- WHERE cc.card_id = ? AND cc.body = ?
57
- ORDER BY cc.id DESC
58
- LIMIT 1`,
59
- [cardId, text],
60
- ).execute() as Array<{ id: number, uuid: string | null, user_id: number | null, body: string, created_at: string | null, updated_at: string | null, name: string | null, email: string | null }>
61
- const r = rows?.[0]
62
- if (!r)
63
- return kanbanError('Comment insert succeeded but follow-up read returned nothing.', 500)
48
+ }))
64
49
 
65
50
  return {
66
- comment: {
67
- id: r.id,
68
- uuid: r.uuid,
69
- userId: r.user_id,
70
- body: r.body,
71
- authorName: r.name,
72
- authorEmail: r.email,
73
- createdAt: r.created_at,
74
- updatedAt: r.updated_at,
75
- },
51
+ comment: cardCommentResponse(comment, user),
76
52
  }
77
53
  }
78
54
  catch (err) {
@@ -0,0 +1,44 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { CardComment, User } from '@stacksjs/orm'
4
+ import { cardCommentResponse } from './kanban-comment'
5
+ import { modelNullableNumber, refreshModel } from './kanban-model'
6
+ import { kanbanError } from './kanban-response'
7
+
8
+ interface CommentInput {
9
+ body?: unknown
10
+ }
11
+
12
+ /** `PATCH /api/dashboard/kanban/comments/:id`. */
13
+ export default new Action({
14
+ name: 'Kanban Card Comment Update',
15
+ description: 'Edits the body of a card comment.',
16
+ method: 'PATCH',
17
+ apiResponse: true,
18
+ async handle(request: RequestInstance<CommentInput>) {
19
+ const id = Number(request.getParam('id'))
20
+ if (!Number.isFinite(id) || id <= 0)
21
+ return kanbanError('Invalid comment id', 400)
22
+
23
+ const input = request.all()
24
+ const body = typeof input.body === 'string' ? input.body.trim() : ''
25
+ if (!body || body.length > 10000)
26
+ return kanbanError('`body` is required and must be 1-10000 characters.', 400)
27
+
28
+ try {
29
+ const comment = await CardComment.find(id)
30
+ if (!comment)
31
+ return kanbanError('Comment not found.', 404)
32
+
33
+ const updated = await refreshModel(await comment.update({ body }))
34
+ const userId = modelNullableNumber(updated, 'userId', 'user_id')
35
+ const author = userId === null ? null : await User.find(userId)
36
+
37
+ return { comment: cardCommentResponse(updated, author) }
38
+ }
39
+ catch (error) {
40
+ console.error('[dashboard/kanban] CardCommentUpdateAction failed:', error)
41
+ return kanbanError(error instanceof Error ? error.message : 'unknown error', 500)
42
+ }
43
+ },
44
+ })
@@ -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/cards/:id` (stacksjs/stacks#1846 Phase 2).
7
+ * `DELETE /api/dashboard/kanban/cards/:id`.
7
8
  *
8
9
  * Hard-deletes a card and clears its pivot rows in `card_labels` /
9
10
  * `card_assignees`. The columns and board are untouched — only the
@@ -18,9 +19,8 @@ export default new Action({
18
19
  description: 'Hard-deletes a card and its pivot rows.',
19
20
  method: 'DELETE',
20
21
  apiResponse: true,
21
- async handle(request) {
22
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
23
- const id = Number(rawId)
22
+ async handle(request: RequestInstance) {
23
+ const id = Number(request.getParam('id'))
24
24
  if (!Number.isFinite(id) || id <= 0) {
25
25
  return kanbanError('Invalid card id', 400)
26
26
  }
@@ -30,7 +30,7 @@ export default new Action({
30
30
  const qb = rawTrx as unknown as typeof db
31
31
  await qb.deleteFrom('card_labels').where('card_id', '=', id).execute()
32
32
  await qb.deleteFrom('card_assignees').where('card_id', '=', id).execute()
33
- // Card comments (Phase 3, stacksjs/stacks#1846).
33
+ // Card comments are card-scoped children.
34
34
  await qb.deleteFrom('card_comments').where('card_id', '=', id).execute()
35
35
  await qb.deleteFrom('cards').where('id', '=', id).execute()
36
36
  })
@@ -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'
@@ -7,7 +8,7 @@ interface SyncInput {
7
8
  }
8
9
 
9
10
  /**
10
- * `POST /api/dashboard/kanban/cards/:id/labels` (stacksjs/stacks#1846 Phase 3).
11
+ * `POST /api/dashboard/kanban/cards/:id/labels`.
11
12
  *
12
13
  * Replaces the set of labels attached to a card. Sync semantics —
13
14
  * pass the new full list of label ids, the action diffs against
@@ -26,13 +27,12 @@ export default new Action({
26
27
  description: 'Replaces the set of labels attached to a card.',
27
28
  method: 'POST',
28
29
  apiResponse: true,
29
- async handle(request) {
30
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
31
- const cardId = Number(rawId)
30
+ async handle(request: RequestInstance<SyncInput>) {
31
+ const cardId = Number(request.getParam('id'))
32
32
  if (!Number.isFinite(cardId) || cardId <= 0)
33
33
  return kanbanError('Invalid card id', 400)
34
34
 
35
- const body = (request as any).jsonBody as SyncInput | undefined ?? {}
35
+ const body = request.all()
36
36
  if (!Array.isArray(body.labelIds))
37
37
  return kanbanError('`labelIds` must be an array of label ids (possibly empty).', 400)
38
38
 
@@ -56,7 +56,7 @@ export default new Action({
56
56
  return kanbanError('Card not found.', 404)
57
57
 
58
58
  // Every label must belong to the same board (labels are
59
- // board-scoped per Phase 1's `labels.board_id` design).
59
+ // board-scoped through `labels.board_id`).
60
60
  if (uniqueLabelIds.length > 0) {
61
61
  const placeholders = uniqueLabelIds.map(() => '?').join(',')
62
62
  const labelRows = await db.unsafe(
@@ -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 { modelBoolean } from './kanban-model'
@@ -20,7 +21,7 @@ interface CardRow {
20
21
  }
21
22
 
22
23
  /**
23
- * `GET /api/dashboard/kanban/cards/:id` (stacksjs/stacks#1846 Phase 3).
24
+ * `GET /api/dashboard/kanban/cards/:id`.
24
25
  *
25
26
  * Fetches a single card with everything the detail modal needs in one
26
27
  * round-trip: labels, assignees, comments. The board view's cards
@@ -39,9 +40,8 @@ export default new Action({
39
40
  description: 'Returns a single card with its labels, assignees, and comments thread.',
40
41
  method: 'GET',
41
42
  apiResponse: true,
42
- async handle(request) {
43
- const rawId = (request as any)?.params?.id ?? (request as any)?.param?.('id') ?? null
44
- const id = Number(rawId)
43
+ async handle(request: RequestInstance) {
44
+ const id = Number(request.getParam('id'))
45
45
  if (!Number.isFinite(id) || id <= 0) {
46
46
  return kanbanError('Invalid card id', 400)
47
47
  }
@@ -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 { BoardColumn, Card } from '@stacksjs/orm'
@@ -11,7 +12,7 @@ interface CardInput {
11
12
  }
12
13
 
13
14
  /**
14
- * `POST /api/dashboard/kanban/cards` (stacksjs/stacks#1846 Phase 2).
15
+ * `POST /api/dashboard/kanban/cards`.
15
16
  *
16
17
  * Creates a card at the end of the target column. `boardId` is
17
18
  * resolved server-side from `column.board_id` (denormalised onto the
@@ -19,7 +20,7 @@ interface CardInput {
19
20
  * would invite drift if the column was moved between boards (which
20
21
  * isn't a feature, but still).
21
22
  *
22
- * Stamps `created_by_user_id` from `request.user` when the request is
23
+ * Stamps `created_by_user_id` from `request.user()` when the request is
23
24
  * authenticated; leaves it null for the no-auth dev dashboard so the
24
25
  * surface still works on localhost.
25
26
  */
@@ -28,8 +29,8 @@ export default new Action({
28
29
  description: 'Creates a new card in a column.',
29
30
  method: 'POST',
30
31
  apiResponse: true,
31
- async handle(request) {
32
- const body = (request as any).jsonBody as CardInput | undefined ?? {}
32
+ async handle(request: RequestInstance<CardInput>) {
33
+ const body = request.all()
33
34
 
34
35
  const columnId = Number(body.columnId)
35
36
  if (!Number.isFinite(columnId) || columnId <= 0) {
@@ -57,8 +58,8 @@ export default new Action({
57
58
  ).execute() as Array<{ m: number }>
58
59
  const nextPosition = (Number(maxRow?.[0]?.m ?? -1) + 1) || 0
59
60
 
60
- const user = (request as any).user ?? (request as any)._authenticatedUser ?? null
61
- const createdByUserId = user && typeof user.id === 'number' ? user.id : null
61
+ const user = await request.user()
62
+ const createdByUserId = user && Number.isInteger(Number(user.id)) ? Number(user.id) : null
62
63
 
63
64
  const card = await Card.create({
64
65
  columnId,