@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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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]`)
|
|
60
|
-
*
|
|
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
|
|
71
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
|
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 = (
|
|
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
|
-
*
|
|
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
|
|
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 = (
|
|
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
|
|
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.
|
|
24
|
-
*
|
|
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
|
|
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 = (
|
|
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 =
|
|
67
|
-
const assignedByUserId = requester &&
|
|
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 {
|
|
3
|
+
import { CardComment } from '@stacksjs/orm'
|
|
3
4
|
import { kanbanError } from './kanban-response'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* `DELETE /api/dashboard/kanban/comments/:id
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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 = (
|
|
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
|
|
38
|
-
if (!
|
|
38
|
+
const card = await Card.find(cardId)
|
|
39
|
+
if (!card)
|
|
39
40
|
return kanbanError('Card not found.', 404)
|
|
40
41
|
|
|
41
|
-
const user =
|
|
42
|
-
const userId = user &&
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = (
|
|
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 =
|
|
61
|
-
const createdByUserId = user &&
|
|
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,
|
|
@@ -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') {
|