@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,7 +1,8 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { NotificationDelivery } from '@stacksjs/orm'
|
|
4
|
-
import {
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { dashboardRequestValue } from '../dashboard-request'
|
|
5
6
|
import { parseDeliveryMetadata } from './notification-delivery'
|
|
6
7
|
|
|
7
8
|
type DashboardDeliveryChannel = 'email' | 'sms'
|
|
@@ -29,9 +30,10 @@ export default new Action({
|
|
|
29
30
|
method: 'GET',
|
|
30
31
|
apiResponse: true,
|
|
31
32
|
async handle(request: RequestInstance) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const channel = resolveDashboardDeliveryChannel(
|
|
34
|
+
request.url,
|
|
35
|
+
dashboardRequestValue(request, 'channel'),
|
|
36
|
+
)
|
|
35
37
|
if (!channel)
|
|
36
38
|
return response.json({ message: 'Channel must be email or sms.' }, 422)
|
|
37
39
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { deletePermission } from '@stacksjs/auth'
|
|
3
4
|
import { response } from '@stacksjs/router'
|
|
@@ -14,13 +15,12 @@ export default new Action({
|
|
|
14
15
|
description: 'Hard-delete a permission + its pivot rows.',
|
|
15
16
|
method: 'DELETE',
|
|
16
17
|
apiResponse: true,
|
|
17
|
-
async handle(request) {
|
|
18
|
-
const name =
|
|
18
|
+
async handle(request: RequestInstance) {
|
|
19
|
+
const name = request.getParam('name').trim()
|
|
19
20
|
if (!name) {
|
|
20
21
|
return response.json({ error: '`name` route param is required.' }, 400)
|
|
21
22
|
}
|
|
22
|
-
const
|
|
23
|
-
const guardName = (url.searchParams.get('guard') || 'web').trim()
|
|
23
|
+
const guardName = String(request.get('guard', 'web')).trim()
|
|
24
24
|
if (!guardName || guardName.length > 60) {
|
|
25
25
|
return response.json({ error: 'Invalid guard name.' }, 400)
|
|
26
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { createPermission, findPermission } from '@stacksjs/auth'
|
|
3
4
|
import { response } from '@stacksjs/router'
|
|
@@ -20,8 +21,8 @@ export default new Action({
|
|
|
20
21
|
description: 'Create a new permission.',
|
|
21
22
|
method: 'POST',
|
|
22
23
|
apiResponse: true,
|
|
23
|
-
async handle(request) {
|
|
24
|
-
const body = (
|
|
24
|
+
async handle(request: RequestInstance<PermissionInput>) {
|
|
25
|
+
const body = request.all()
|
|
25
26
|
|
|
26
27
|
const name = typeof body.name === 'string' ? body.name.trim() : ''
|
|
27
28
|
if (!name || name.length > 100) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { deleteRole } from '@stacksjs/auth'
|
|
3
4
|
import { response } from '@stacksjs/router'
|
|
@@ -16,13 +17,12 @@ export default new Action({
|
|
|
16
17
|
description: 'Hard-delete a role + its pivot rows.',
|
|
17
18
|
method: 'DELETE',
|
|
18
19
|
apiResponse: true,
|
|
19
|
-
async handle(request) {
|
|
20
|
-
const name =
|
|
20
|
+
async handle(request: RequestInstance) {
|
|
21
|
+
const name = request.getParam('name').trim()
|
|
21
22
|
if (!name) {
|
|
22
23
|
return response.json({ error: '`name` route param is required.' }, 400)
|
|
23
24
|
}
|
|
24
|
-
const
|
|
25
|
-
const guardName = (url.searchParams.get('guard') || 'web').trim()
|
|
25
|
+
const guardName = String(request.get('guard', 'web')).trim()
|
|
26
26
|
if (!guardName || guardName.length > 60) {
|
|
27
27
|
return response.json({ error: 'Invalid guard name.' }, 400)
|
|
28
28
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { findRole, getRolePermissions } from '@stacksjs/auth'
|
|
3
4
|
import { response } from '@stacksjs/router'
|
|
@@ -15,13 +16,12 @@ export default new Action({
|
|
|
15
16
|
description: 'List permissions attached to one role.',
|
|
16
17
|
method: 'GET',
|
|
17
18
|
apiResponse: true,
|
|
18
|
-
async handle(request) {
|
|
19
|
-
const name =
|
|
19
|
+
async handle(request: RequestInstance) {
|
|
20
|
+
const name = request.getParam('name').trim()
|
|
20
21
|
if (!name) {
|
|
21
22
|
return response.json({ error: '`name` route param is required.' }, 400)
|
|
22
23
|
}
|
|
23
|
-
const
|
|
24
|
-
const guardName = (url.searchParams.get('guard') || 'web').trim()
|
|
24
|
+
const guardName = String(request.get('guard', 'web')).trim()
|
|
25
25
|
if (!guardName || guardName.length > 60) {
|
|
26
26
|
return response.json({ error: 'Invalid guard name.' }, 400)
|
|
27
27
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { getRolePermissions, findRole, syncRolePermissions } from '@stacksjs/auth'
|
|
3
4
|
import { response } from '@stacksjs/router'
|
|
@@ -19,13 +20,13 @@ export default new Action({
|
|
|
19
20
|
description: 'Replace the permission set attached to one role.',
|
|
20
21
|
method: 'POST',
|
|
21
22
|
apiResponse: true,
|
|
22
|
-
async handle(request) {
|
|
23
|
-
const roleName =
|
|
23
|
+
async handle(request: RequestInstance<SyncInput>) {
|
|
24
|
+
const roleName = request.getParam('name').trim()
|
|
24
25
|
if (!roleName) {
|
|
25
26
|
return response.json({ error: '`name` route param is required.' }, 400)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const body = (
|
|
29
|
+
const body = request.all()
|
|
29
30
|
if (!Array.isArray(body.permissions)) {
|
|
30
31
|
return response.json({ error: '`permissions` must be an array of permission names (possibly empty).' }, 400)
|
|
31
32
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { createRole, findRole } from '@stacksjs/auth'
|
|
3
4
|
import { response } from '@stacksjs/router'
|
|
@@ -25,8 +26,8 @@ export default new Action({
|
|
|
25
26
|
description: 'Create a new role.',
|
|
26
27
|
method: 'POST',
|
|
27
28
|
apiResponse: true,
|
|
28
|
-
async handle(request) {
|
|
29
|
-
const body = (
|
|
29
|
+
async handle(request: RequestInstance<RoleInput>) {
|
|
30
|
+
const body = request.all()
|
|
30
31
|
|
|
31
32
|
const name = typeof body.name === 'string' ? body.name.trim() : ''
|
|
32
33
|
if (!name || name.length > 60) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { getUserRoles } from '@stacksjs/auth'
|
|
3
4
|
import { User } from '@stacksjs/orm'
|
|
@@ -16,14 +17,12 @@ export default new Action({
|
|
|
16
17
|
description: 'List roles attached to one user.',
|
|
17
18
|
method: 'GET',
|
|
18
19
|
apiResponse: true,
|
|
19
|
-
async handle(request) {
|
|
20
|
-
const
|
|
21
|
-
const userId = Number(rawId)
|
|
20
|
+
async handle(request: RequestInstance) {
|
|
21
|
+
const userId = Number(request.getParam('id'))
|
|
22
22
|
if (!Number.isFinite(userId) || userId <= 0) {
|
|
23
23
|
return response.json({ error: 'Invalid user id.' }, 400)
|
|
24
24
|
}
|
|
25
|
-
const
|
|
26
|
-
const guardName = (url.searchParams.get('guard') || 'web').trim()
|
|
25
|
+
const guardName = String(request.get('guard', 'web')).trim()
|
|
27
26
|
if (!guardName || guardName.length > 60) {
|
|
28
27
|
return response.json({ error: 'Invalid guard name.' }, 400)
|
|
29
28
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
1
2
|
import { Action } from '@stacksjs/actions'
|
|
2
3
|
import { getUserRoles, syncRoles } from '@stacksjs/auth'
|
|
3
4
|
import { User } from '@stacksjs/orm'
|
|
@@ -24,14 +25,13 @@ export default new Action({
|
|
|
24
25
|
description: 'Replace the role set attached to one user.',
|
|
25
26
|
method: 'POST',
|
|
26
27
|
apiResponse: true,
|
|
27
|
-
async handle(request) {
|
|
28
|
-
const
|
|
29
|
-
const userId = Number(rawId)
|
|
28
|
+
async handle(request: RequestInstance<SyncInput>) {
|
|
29
|
+
const userId = Number(request.getParam('id'))
|
|
30
30
|
if (!Number.isFinite(userId) || userId <= 0) {
|
|
31
31
|
return response.json({ error: 'Invalid user id.' }, 400)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const body = (
|
|
34
|
+
const body = request.all()
|
|
35
35
|
if (!Array.isArray(body.roles)) {
|
|
36
36
|
return response.json({ error: '`roles` must be an array of role names (possibly empty).' }, 400)
|
|
37
37
|
}
|
|
@@ -25,12 +25,12 @@ export default new Action({
|
|
|
25
25
|
method: 'POST',
|
|
26
26
|
apiResponse: true,
|
|
27
27
|
|
|
28
|
-
async handle(request: RequestInstance) {
|
|
28
|
+
async handle(request: RequestInstance<InviteInput>) {
|
|
29
29
|
const teamId = parsePositiveId(request.getParam('id'))
|
|
30
30
|
if (!teamId)
|
|
31
31
|
return response.json({ message: 'Invalid team id.' }, 400)
|
|
32
32
|
|
|
33
|
-
const input = (
|
|
33
|
+
const input = request.all()
|
|
34
34
|
const email = normalizeInvitationEmail(input.email)
|
|
35
35
|
const role = normalizeInvitationRole(input.role ?? 'member')
|
|
36
36
|
if (!email)
|
|
@@ -83,7 +83,7 @@ export default new Action({
|
|
|
83
83
|
const token = generateInvitationToken()
|
|
84
84
|
const tokenHash = hashInvitationToken(token)
|
|
85
85
|
const expiresAt = invitationExpiresAt()
|
|
86
|
-
const requester =
|
|
86
|
+
const requester = await request.user()
|
|
87
87
|
const invitedByUserId = requester && Number.isInteger(Number(requester.id))
|
|
88
88
|
? Number(requester.id)
|
|
89
89
|
: null
|
|
@@ -16,7 +16,7 @@ export default new Action({
|
|
|
16
16
|
method: 'PATCH',
|
|
17
17
|
apiResponse: true,
|
|
18
18
|
|
|
19
|
-
async handle(request: RequestInstance) {
|
|
19
|
+
async handle(request: RequestInstance<UpdateInput>) {
|
|
20
20
|
const teamId = parsePositiveId(request.getParam('id'))
|
|
21
21
|
const memberId = parsePositiveId(request.getParam('memberId'))
|
|
22
22
|
if (!teamId || !memberId)
|
|
@@ -33,7 +33,7 @@ export default new Action({
|
|
|
33
33
|
if (member.role === 'owner')
|
|
34
34
|
return response.json({ message: 'Transfer team ownership before changing the owner.' }, 409)
|
|
35
35
|
|
|
36
|
-
const input = (
|
|
36
|
+
const input = request.all()
|
|
37
37
|
const role = input.role === undefined ? String(member.role) : normalizeInvitationRole(input.role)
|
|
38
38
|
const status = input.status === undefined ? String(member.status) : String(input.status).trim().toLowerCase()
|
|
39
39
|
if (!role)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
|
|
3
|
+
/** Reads one scalar dashboard input value through the native request bag. */
|
|
4
|
+
export function dashboardRequestValue(
|
|
5
|
+
request: RequestInstance,
|
|
6
|
+
key: string,
|
|
7
|
+
fallback = '',
|
|
8
|
+
): string {
|
|
9
|
+
const value = request.get<unknown>(key, fallback)
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.length > 0 ? String(value[0]).trim() : fallback
|
|
12
|
+
return value == null ? fallback : String(value).trim()
|
|
13
|
+
}
|
|
@@ -2,13 +2,12 @@ import { defineModel } from '@stacksjs/orm'
|
|
|
2
2
|
import { schema } from '@stacksjs/validation'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Card comment
|
|
5
|
+
* Card comment for the dashboard Kanban surface.
|
|
6
6
|
*
|
|
7
7
|
* Each comment belongs to a card and (optionally) to the user who
|
|
8
8
|
* wrote it. Anonymous comments are allowed via nullable `user_id` —
|
|
9
9
|
* makes the dev dashboard usable on localhost without an auth round-
|
|
10
|
-
* trip, and supports system-generated activity entries
|
|
11
|
-
* (eventually — Phase 4 could write status-change comments here).
|
|
10
|
+
* trip, and supports system-generated activity entries.
|
|
12
11
|
*
|
|
13
12
|
* Distinct from `Comment` (which is post-specific) so the schemas
|
|
14
13
|
* don't fight: post comments carry `author_name`/`author_email` for
|
|
@@ -35,7 +34,7 @@ export default defineModel({
|
|
|
35
34
|
|
|
36
35
|
useApi: {
|
|
37
36
|
uri: 'card-comments',
|
|
38
|
-
routes: ['index', 'store', 'show', 'destroy'],
|
|
37
|
+
routes: ['index', 'store', 'show', 'update', 'destroy'],
|
|
39
38
|
middleware: ['auth'],
|
|
40
39
|
},
|
|
41
40
|
},
|
|
@@ -111,6 +111,54 @@ export default defineModel({
|
|
|
111
111
|
},
|
|
112
112
|
factory: () => false,
|
|
113
113
|
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A stable name for this component, for code to reference.
|
|
117
|
+
*
|
|
118
|
+
* `name` is what an operator reads and edits in the dashboard, so it is
|
|
119
|
+
* the wrong thing for an application to branch on — renaming "State sales
|
|
120
|
+
* tax" to "Sales tax (CA)" should not change what gets charged. A code is
|
|
121
|
+
* the identifier that survives the rename.
|
|
122
|
+
*
|
|
123
|
+
* Free-form on purpose. What counts as a component differs by
|
|
124
|
+
* jurisdiction: `vat`, `gst`, `excise`, `city`, `eco-fee`.
|
|
125
|
+
*/
|
|
126
|
+
code: {
|
|
127
|
+
order: 8,
|
|
128
|
+
fillable: true,
|
|
129
|
+
default: '',
|
|
130
|
+
validation: {
|
|
131
|
+
rule: schema.string().max(64),
|
|
132
|
+
message: {
|
|
133
|
+
max: 'Code must have a maximum of 64 characters',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
factory: faker => faker.helpers.arrayElement(['vat', 'gst', 'sales', 'excise', 'city']),
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether a qualifying exemption removes this component.
|
|
141
|
+
*
|
|
142
|
+
* Most places tax in parts, and an exemption usually lifts some of them
|
|
143
|
+
* and not others: groceries escape VAT but not a deposit levy, and a
|
|
144
|
+
* Californian medical cannabis patient is exempt from sales tax while
|
|
145
|
+
* still paying excise and the city's business tax. Modelling tax as one
|
|
146
|
+
* blended number cannot say that, so an app either over-charges the
|
|
147
|
+
* exempt customer or under-collects tax it owes.
|
|
148
|
+
*
|
|
149
|
+
* What *qualifies* is the application's business — a card, a resale
|
|
150
|
+
* certificate, a charity registration. This only records that the
|
|
151
|
+
* component is the kind that can be lifted.
|
|
152
|
+
*/
|
|
153
|
+
exemptible: {
|
|
154
|
+
order: 9,
|
|
155
|
+
fillable: true,
|
|
156
|
+
default: false,
|
|
157
|
+
validation: {
|
|
158
|
+
rule: schema.boolean(),
|
|
159
|
+
},
|
|
160
|
+
factory: () => false,
|
|
161
|
+
},
|
|
114
162
|
},
|
|
115
163
|
|
|
116
164
|
dashboard: {
|
|
@@ -37,16 +37,9 @@ export class DashboardApiError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
* Call a dashboard API endpoint and return its parsed JSON.
|
|
42
|
-
*
|
|
43
|
-
* Throws on a non-2xx response, using the server's `message` / `error`
|
|
44
|
-
* field when there is one so callers can surface something better than a
|
|
45
|
-
* bare status code.
|
|
46
|
-
*/
|
|
47
|
-
export async function dashboardApi<T = unknown>(path: string, options: ApiRequestOptions = {}): Promise<T> {
|
|
40
|
+
async function dashboardResponse(path: string, options: ApiRequestOptions, accept: string): Promise<Response> {
|
|
48
41
|
const method = options.method ?? 'GET'
|
|
49
|
-
const headers: Record<string, string> = { accept
|
|
42
|
+
const headers: Record<string, string> = { accept }
|
|
50
43
|
const authToken = useAuth().getToken()
|
|
51
44
|
|
|
52
45
|
if (options.body !== undefined && options.formData)
|
|
@@ -62,7 +55,7 @@ export async function dashboardApi<T = unknown>(path: string, options: ApiReques
|
|
|
62
55
|
? headers
|
|
63
56
|
: withCsrfHeader(headers)
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
return fetch(path, {
|
|
66
59
|
method,
|
|
67
60
|
headers: requestHeaders,
|
|
68
61
|
credentials: 'same-origin',
|
|
@@ -70,6 +63,33 @@ export async function dashboardApi<T = unknown>(path: string, options: ApiReques
|
|
|
70
63
|
...(options.body !== undefined && { body: JSON.stringify(options.body) }),
|
|
71
64
|
...(options.formData && { body: options.formData }),
|
|
72
65
|
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function dashboardResponseError(res: Response): Promise<DashboardApiError> {
|
|
69
|
+
const text = await res.text()
|
|
70
|
+
let payload: ApiErrorBody | null = null
|
|
71
|
+
try {
|
|
72
|
+
payload = text ? JSON.parse(text) as ApiErrorBody : null
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
payload = null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const failure = describeResponseError(res.status, payload)
|
|
79
|
+
if (failure.unexpected)
|
|
80
|
+
console.error('[dashboard-api] Unexpected request failure:', failure.cause)
|
|
81
|
+
return new DashboardApiError(failure.message, res.status, failure.fields)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Call a dashboard API endpoint and return its parsed JSON.
|
|
86
|
+
*
|
|
87
|
+
* Throws on a non-2xx response, using the server's `message` / `error`
|
|
88
|
+
* field when there is one so callers can surface something better than a
|
|
89
|
+
* bare status code.
|
|
90
|
+
*/
|
|
91
|
+
export async function dashboardApi<T = unknown>(path: string, options: ApiRequestOptions = {}): Promise<T> {
|
|
92
|
+
const res = await dashboardResponse(path, options, 'application/json')
|
|
73
93
|
|
|
74
94
|
const text = await res.text()
|
|
75
95
|
let payload: any = null
|
|
@@ -89,3 +109,24 @@ export async function dashboardApi<T = unknown>(path: string, options: ApiReques
|
|
|
89
109
|
|
|
90
110
|
return payload as T
|
|
91
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Download a guarded dashboard resource while preserving bearer and session
|
|
115
|
+
* authentication. The object URL only exists for the duration of the browser
|
|
116
|
+
* download and is then released.
|
|
117
|
+
*/
|
|
118
|
+
export async function dashboardDownload(path: string, filename: string): Promise<void> {
|
|
119
|
+
const res = await dashboardResponse(path, {}, 'application/octet-stream')
|
|
120
|
+
if (!res.ok)
|
|
121
|
+
throw await dashboardResponseError(res)
|
|
122
|
+
|
|
123
|
+
const objectUrl = URL.createObjectURL(await res.blob())
|
|
124
|
+
const anchor = document.createElement('a')
|
|
125
|
+
anchor.href = objectUrl
|
|
126
|
+
anchor.download = filename
|
|
127
|
+
anchor.hidden = true
|
|
128
|
+
document.body.append(anchor)
|
|
129
|
+
anchor.click()
|
|
130
|
+
anchor.remove()
|
|
131
|
+
setTimeout(() => URL.revokeObjectURL(objectUrl), 0)
|
|
132
|
+
}
|
package/functions/inbox.test.ts
CHANGED
package/functions/inbox.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { ref } from '@stacksjs/stx'
|
|
8
8
|
import { get } from './api'
|
|
9
|
-
import { dashboardApi } from './dashboard-api'
|
|
9
|
+
import { dashboardApi, dashboardDownload } from './dashboard-api'
|
|
10
10
|
import { pushToast } from './toasts'
|
|
11
11
|
|
|
12
12
|
export type InboxActivityRange = 'day' | 'week' | 'month' | 'year'
|
|
@@ -71,6 +71,15 @@ export interface DashboardInboxEmail {
|
|
|
71
71
|
date: string
|
|
72
72
|
read: boolean
|
|
73
73
|
hasAttachments: boolean
|
|
74
|
+
attachments: DashboardInboxAttachment[]
|
|
75
|
+
detailsLoaded: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DashboardInboxAttachment {
|
|
79
|
+
id: string
|
|
80
|
+
name: string
|
|
81
|
+
size: number
|
|
82
|
+
lastModified?: string
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
interface DashboardInboxEntry {
|
|
@@ -94,6 +103,7 @@ interface DashboardInboxResponse {
|
|
|
94
103
|
interface DashboardInboxBodyResponse {
|
|
95
104
|
html?: string
|
|
96
105
|
text?: string
|
|
106
|
+
attachments?: DashboardInboxAttachment[]
|
|
97
107
|
error?: string
|
|
98
108
|
}
|
|
99
109
|
|
|
@@ -166,6 +176,8 @@ export function mapDashboardInboxEntry(item: DashboardInboxEntry): DashboardInbo
|
|
|
166
176
|
date: item.date,
|
|
167
177
|
read: item.read === true,
|
|
168
178
|
hasAttachments: item.hasAttachments === true,
|
|
179
|
+
attachments: [],
|
|
180
|
+
detailsLoaded: false,
|
|
169
181
|
}
|
|
170
182
|
}
|
|
171
183
|
|
|
@@ -179,7 +191,7 @@ export async function fetchDashboardInbox(mailbox?: string): Promise<LoadedDashb
|
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
|
|
182
|
-
export async function fetchDashboardInboxBody(messageId: string, mailbox?: string): Promise<Pick<DashboardInboxEmail, 'bodyHtml' | 'bodyText'>> {
|
|
194
|
+
export async function fetchDashboardInboxBody(messageId: string, mailbox?: string): Promise<Pick<DashboardInboxEmail, 'attachments' | 'bodyHtml' | 'bodyText' | 'detailsLoaded' | 'hasAttachments'>> {
|
|
183
195
|
const query = mailbox ? `?mailbox=${encodeURIComponent(mailbox)}` : ''
|
|
184
196
|
const data = await dashboardApi<DashboardInboxBodyResponse>(`/api/dashboard/email/inbox/${encodeURIComponent(messageId)}${query}`)
|
|
185
197
|
if (data.error)
|
|
@@ -187,9 +199,22 @@ export async function fetchDashboardInboxBody(messageId: string, mailbox?: strin
|
|
|
187
199
|
return {
|
|
188
200
|
bodyHtml: data.html || '',
|
|
189
201
|
bodyText: data.text || '',
|
|
202
|
+
attachments: data.attachments || [],
|
|
203
|
+
detailsLoaded: true,
|
|
204
|
+
hasAttachments: (data.attachments || []).length > 0,
|
|
190
205
|
}
|
|
191
206
|
}
|
|
192
207
|
|
|
208
|
+
export async function downloadDashboardInboxAttachment(
|
|
209
|
+
messageId: string,
|
|
210
|
+
attachment: DashboardInboxAttachment,
|
|
211
|
+
mailbox?: string,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
const query = mailbox ? `?mailbox=${encodeURIComponent(mailbox)}` : ''
|
|
214
|
+
const path = `/api/dashboard/email/inbox/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachment.id)}${query}`
|
|
215
|
+
await dashboardDownload(path, attachment.name)
|
|
216
|
+
}
|
|
217
|
+
|
|
193
218
|
export async function fetchInboxActivity(range: InboxActivityRange): Promise<InboxActivity | null> {
|
|
194
219
|
try {
|
|
195
220
|
return await dashboardApi<InboxActivity>(`/api/dashboard/email/activity?range=${range}`)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { publicApplicationUrl } from './public-application-url'
|
|
3
|
+
|
|
4
|
+
describe('publicApplicationUrl', () => {
|
|
5
|
+
it('uses HTTPS for application domains and removes duplicate separators', () => {
|
|
6
|
+
expect(publicApplicationUrl('/blog', 'stacks.localhost/')).toBe('https://stacks.localhost/blog')
|
|
7
|
+
expect(publicApplicationUrl('/blog/post', 'https://stacksjs.com/')).toBe('https://stacksjs.com/blog/post')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('keeps explicit local development protocols', () => {
|
|
11
|
+
expect(publicApplicationUrl('/blog', 'http://localhost:3000')).toBe('http://localhost:3000/blog')
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
function configuredApplicationUrl(): string {
|
|
4
|
+
const frontendUrl = process.env.FRONTEND_APP_URL?.trim()
|
|
5
|
+
if (frontendUrl && !frontendUrl.includes('${'))
|
|
6
|
+
return frontendUrl
|
|
7
|
+
|
|
8
|
+
return process.env.APP_URL?.trim() || 'http://localhost:3000'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function withProtocol(baseUrl: string): string {
|
|
12
|
+
if (/^https?:\/\//i.test(baseUrl))
|
|
13
|
+
return baseUrl
|
|
14
|
+
|
|
15
|
+
const host = baseUrl.split('/')[0].split(':')[0].toLowerCase()
|
|
16
|
+
const protocol = ['localhost', '127.0.0.1', '0.0.0.0', '[::1]'].includes(host)
|
|
17
|
+
? 'http'
|
|
18
|
+
: 'https'
|
|
19
|
+
return `${protocol}://${baseUrl}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Build an absolute URL for a page served by the public Stacks application. */
|
|
23
|
+
export function publicApplicationUrl(pathname = '/', baseUrl = configuredApplicationUrl()): string {
|
|
24
|
+
const normalizedBase = withProtocol(baseUrl.trim()).replace(/\/+$/, '')
|
|
25
|
+
const normalizedPath = pathname.replace(/^\/+/, '')
|
|
26
|
+
return new URL(normalizedPath, `${normalizedBase}/`).toString()
|
|
27
|
+
}
|
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.355",
|
|
6
6
|
"description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
|
+
import { authStore } from '../../../../views/dashboard/stores/auth'
|
|
3
4
|
|
|
4
5
|
interface AccessTokenRow {
|
|
5
6
|
id: number
|
|
@@ -106,6 +107,20 @@ async function loadTokens(): Promise<void> {
|
|
|
106
107
|
loading.set(true)
|
|
107
108
|
loadError.set('')
|
|
108
109
|
try {
|
|
110
|
+
await authStore.load()
|
|
111
|
+
|
|
112
|
+
if (authStore.error()) {
|
|
113
|
+
tokens.set([])
|
|
114
|
+
loadError.set(authStore.error() || 'The signed-in account could not be verified.')
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (authStore.unauthenticated()) {
|
|
119
|
+
tokens.set([])
|
|
120
|
+
authenticationRequired.set(true)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
const response = await dashboardApi<TokenListResponse>('/auth/tokens')
|
|
110
125
|
tokens.set(response.tokens || [])
|
|
111
126
|
authenticationRequired.set(false)
|