@stacksjs/defaults 0.75.242 → 0.75.244

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-actions
3
- description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 672 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
3
+ description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 673 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -11,7 +11,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
11
11
  | Package | Task guidance | Source entry | Retained tests |
12
12
  |---|---|---|---|
13
13
  | `@stacksjs/action-runner` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/action-runner/src/index.ts) | 1 |
14
- | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 74 |
14
+ | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 75 |
15
15
  | `@stacksjs/ai` | [`stacks-ai`](../stacks-ai/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/index.ts) | 17 |
16
16
  | `@stacksjs/alias` | [`stacks-alias`](../stacks-alias/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/alias/src/index.ts) | 1 |
17
17
  | `@stacksjs/analytics` | [`stacks-analytics`](../stacks-analytics/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/analytics/src/index.ts) | 6 |
@@ -21,7 +21,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
21
21
  | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 119 |
22
22
  | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
23
23
  | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
24
- | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 151 |
24
+ | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 152 |
25
25
  | `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 2 |
26
26
  | `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 11 |
27
27
  | `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 |
@@ -65,7 +65,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
65
65
  | `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
66
66
  | `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 16 |
67
67
  | `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
68
- | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 62 |
68
+ | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 64 |
69
69
  | `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
70
70
  | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 11 |
71
71
  | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 28 |
@@ -119,7 +119,7 @@ Declared runtime exports: `BlogAdminError`, `DEFAULT_IOS_DEPENDENCIES`, `DEFAULT
119
119
 
120
120
  Local source modules: [src/action.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/action.ts), [src/add.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/add.ts), [src/blog-admin.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/blog-admin.ts), [src/build/ios-toolchain.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/build/ios-toolchain.ts), [src/commit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/commit.ts), [src/dev/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/dev/index.ts), [src/discover-packages.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/discover-packages.ts), [src/generate/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/generate/index.ts), [src/github-release.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/github-release.ts), [src/helpers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/helpers/index.ts), [src/library/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/library/index.ts), [src/lint/lint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/lint.ts), [src/lint/stx-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/stx-gate.ts), [src/make-command.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-command.ts), [src/make-job.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-job.ts), [src/make-policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-policy.ts), [src/make-resource.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-resource.ts), [src/make.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make.ts), [src/scaffold-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/scaffold-crud.ts), [src/setup/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/setup/index.ts), [src/stacks.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/stacks.ts), [src/upgrade/package-project.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/package-project.ts), [src/upgrade/route-actions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/route-actions.ts).
121
121
 
122
- Evidence directory: [74 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
122
+ Evidence directory: [75 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
123
123
 
124
124
  ### @stacksjs/ai
125
125
 
@@ -215,7 +215,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
215
215
 
216
216
  Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts).
217
217
 
218
- Evidence directory: [151 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
218
+ Evidence directory: [152 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
219
219
 
220
220
  ### @stacksjs/build
221
221
 
@@ -633,7 +633,7 @@ Declared runtime exports: `Activity`, `AnalyticsEvent`, `Auction`, `AuctionItem`
633
633
 
634
634
  Local source modules: [src/auto-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/auto-crud.ts), [src/batch-loader.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/batch-loader.ts), [src/db.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/db.ts), [src/define-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/define-model.ts), [src/extend-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/extend-model.ts), [src/gdpr/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/gdpr/index.ts), [src/generate-database-schema.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/generate-database-schema.ts), [src/locked-row.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/locked-row.ts), [src/messaging.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/messaging.ts), [src/model-types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/model-types.ts), [src/ownership.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/ownership.ts), [src/paginator-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/paginator-request.ts), [src/row-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/row-token.ts), [src/subquery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/subquery.ts), [src/traits/audit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/audit.ts), [src/traits/billable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/billable.ts), [src/traits/categorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/categorizable.ts), [src/traits/commentable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/commentable.ts), [src/traits/likeable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/likeable.ts), [src/traits/taggable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/taggable.ts), [src/transaction.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/transaction.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/types.ts), [src/utils/encrypted.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/encrypted.ts), [src/utils/prunable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/prunable.ts).
635
635
 
636
- Evidence directory: [62 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
636
+ Evidence directory: [64 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
637
637
 
638
638
  ### @stacksjs/pagination
639
639
 
@@ -193,6 +193,12 @@ export default new Action({
193
193
  })
194
194
  }
195
195
 
196
- return { stats, httpMetrics, services, activities, issues }
196
+ // Which feature-backed sections exist, so the home page does not offer a
197
+ // card for a section the sidebar has hidden and whose pages cannot load.
198
+ // `feature()` is server-side only, and this component runs on the client.
199
+ // See stacksjs/stacks#2928.
200
+ const sections = { commerce, content: feature('cms'), marketing: feature('marketing') }
201
+
202
+ return { stats, httpMetrics, services, activities, issues, sections }
197
203
  },
198
204
  })
@@ -0,0 +1,144 @@
1
+ import type { DashboardHqDriverOptions } from '@stacksjs/types'
2
+
3
+ /**
4
+ * The one way a dashboard section talks to a hosted HQ product.
5
+ *
6
+ * `dashboard-provider.ts` decides *which* provider a section reads from and
7
+ * guarantees that a remote one cannot take the page down. This module is the
8
+ * other half: how a remote read is actually performed, so that each domain
9
+ * driver is a path and a shape rather than its own copy of base-URL joining,
10
+ * header handling and failure translation.
11
+ *
12
+ * Everything here exists to make one of `readThroughProvider`'s three degrade
13
+ * paths real:
14
+ *
15
+ * it rejects — an unreachable host, a non-2xx answer, or a body that
16
+ * is not JSON, all raised as {@link HqReadError}
17
+ * it never answers — a hung connection, aborted at `timeoutMs`. Nothing
18
+ * else in the seam imposes a deadline, so without this
19
+ * the "never answers" case was documented rather than
20
+ * handled: the request simply hung and took the
21
+ * section's request with it
22
+ * the wrong shape — left to the caller, which knows what its section
23
+ * renders; this module parses JSON and no more
24
+ *
25
+ * The read key only ever travels in `Authorization`. A URL is logged by
26
+ * proxies, kept in browser history and attached to error reports, and a
27
+ * read-scoped key is still a credential, so it never becomes a query
28
+ * parameter even though several hosted APIs would accept one.
29
+ */
30
+
31
+ /** How long a hosted read may take before it is treated as unreachable. */
32
+ export const HQ_DEFAULT_TIMEOUT_MS = 10_000
33
+
34
+ /** The upper bound on a configured timeout, so one cannot hold a section open. */
35
+ export const HQ_MAX_TIMEOUT_MS = 60_000
36
+
37
+ /**
38
+ * A hosted read that did not produce a payload.
39
+ *
40
+ * Carries `status` when the product answered and the answer was not a 2xx, so
41
+ * an operator reading the log can tell a misconfigured key (401/403) from a
42
+ * wrong project id (404) from the product being unwell (5xx). `status` is
43
+ * absent when no answer arrived at all.
44
+ */
45
+ export class HqReadError extends Error {
46
+ readonly status?: number
47
+ readonly url: string
48
+
49
+ constructor(message: string, url: string, status?: number) {
50
+ super(message)
51
+ this.name = 'HqReadError'
52
+ this.url = url
53
+ this.status = status
54
+ }
55
+ }
56
+
57
+ /** Whether the options carry enough to attempt a read at all. */
58
+ export function hqConfigured(options: DashboardHqDriverOptions | null | undefined): boolean {
59
+ return Boolean(options?.baseUrl && options?.token)
60
+ }
61
+
62
+ /**
63
+ * `base` and `path` joined into one URL, with exactly one slash between them.
64
+ *
65
+ * A base URL is hand-written in a config file, so it arrives with or without
66
+ * a trailing slash, and a driver's path is written as a literal that reads
67
+ * best with a leading one. Both spellings have to land on the same address.
68
+ */
69
+ export function hqUrl(base: string, path: string, query?: Record<string, string | number | undefined>): string {
70
+ const url = new URL(path.replace(/^\/+/, ''), base.endsWith('/') ? base : `${base}/`)
71
+ for (const [key, value] of Object.entries(query ?? {})) {
72
+ if (value !== undefined && value !== '')
73
+ url.searchParams.set(key, String(value))
74
+ }
75
+ return url.toString()
76
+ }
77
+
78
+ /** A configured timeout, clamped so neither a typo nor a zero disables it. */
79
+ export function hqTimeout(options: DashboardHqDriverOptions | null | undefined): number {
80
+ const asked = Number(options?.timeoutMs)
81
+ if (!Number.isFinite(asked) || asked <= 0)
82
+ return HQ_DEFAULT_TIMEOUT_MS
83
+ return Math.min(HQ_MAX_TIMEOUT_MS, asked)
84
+ }
85
+
86
+ /**
87
+ * One read from a hosted HQ product, as JSON.
88
+ *
89
+ * Throws {@link HqReadError} for every failure, which is what the caller
90
+ * wants: `readThroughProvider` turns a rejection into the section's empty
91
+ * payload with a reason, so a driver never has to handle a failure itself.
92
+ *
93
+ * `fetchImpl` exists for tests. Nothing in the application passes it.
94
+ */
95
+ export async function hqRead<T = unknown>(
96
+ options: DashboardHqDriverOptions | null | undefined,
97
+ path: string,
98
+ query?: Record<string, string | number | undefined>,
99
+ fetchImpl: typeof fetch = fetch,
100
+ ): Promise<T> {
101
+ if (!hqConfigured(options)) {
102
+ // Not a network failure, and worth saying so differently: a section that
103
+ // reads empty because nobody filled in a base URL is a configuration
104
+ // mistake an operator can fix, not an outage to wait out.
105
+ throw new HqReadError('The hosted provider has no baseUrl and token configured.', '')
106
+ }
107
+
108
+ const url = hqUrl(options!.baseUrl!, path, query)
109
+ let answer: Response
110
+ try {
111
+ answer = await fetchImpl(url, {
112
+ headers: {
113
+ // The read key, and only here. See the note at the top of this file.
114
+ authorization: `Bearer ${options!.token}`,
115
+ accept: 'application/json',
116
+ },
117
+ signal: AbortSignal.timeout(hqTimeout(options)),
118
+ })
119
+ }
120
+ catch (cause) {
121
+ // An abort and a refused connection are the same thing to a section: no
122
+ // data arrived. They are told apart in the message so the log is useful.
123
+ const aborted = (cause as { name?: string })?.name === 'TimeoutError'
124
+ || (cause as { name?: string })?.name === 'AbortError'
125
+ throw new HqReadError(
126
+ aborted
127
+ ? `The hosted provider did not answer within ${hqTimeout(options)}ms.`
128
+ : `The hosted provider could not be reached: ${(cause as Error)?.message ?? String(cause)}`,
129
+ url,
130
+ )
131
+ }
132
+
133
+ if (!answer.ok)
134
+ throw new HqReadError(`The hosted provider answered ${answer.status}.`, url, answer.status)
135
+
136
+ try {
137
+ return await answer.json() as T
138
+ }
139
+ catch {
140
+ // A 2xx that is not JSON is usually a sign-in page or a proxy's error
141
+ // document, which is a configuration problem wearing a success code.
142
+ throw new HqReadError('The hosted provider answered with something that is not JSON.', url, answer.status)
143
+ }
144
+ }
@@ -142,38 +142,48 @@ export default defineModel({
142
142
  rule: schema.string().max(255),
143
143
  },
144
144
  },
145
+ // `text`, all of these: an EXPLAIN plan, the tables a query touched and the
146
+ // suggestions made about it run past the varchar(255) Postgres enforces, and
147
+ // a log row that does not fit is dropped ("value too long for type character
148
+ // varying(255)") - losing exactly the slow queries this table exists for.
145
149
  tags: {
146
150
  fillable: true,
151
+ type: 'text',
147
152
  validation: {
148
153
  rule: schema.string(),
149
154
  },
150
155
  },
151
156
  affected_tables: {
152
157
  fillable: true,
158
+ type: 'text',
153
159
  validation: {
154
160
  rule: schema.string(),
155
161
  },
156
162
  },
157
163
  indexes_used: {
158
164
  fillable: true,
165
+ type: 'text',
159
166
  validation: {
160
167
  rule: schema.string(),
161
168
  },
162
169
  },
163
170
  missing_indexes: {
164
171
  fillable: true,
172
+ type: 'text',
165
173
  validation: {
166
174
  rule: schema.string(),
167
175
  },
168
176
  },
169
177
  explain_plan: {
170
178
  fillable: true,
179
+ type: 'text',
171
180
  validation: {
172
181
  rule: schema.string(),
173
182
  },
174
183
  },
175
184
  optimization_suggestions: {
176
185
  fillable: true,
186
+ type: 'text',
177
187
  validation: {
178
188
  rule: schema.string(),
179
189
  },
@@ -39,6 +39,11 @@ export interface DashboardIssue {
39
39
  }
40
40
 
41
41
  interface DashboardHomeResponse {
42
+ /**
43
+ * Which feature-backed sections the server has available, so the home page
44
+ * only offers cards for sections that exist. See stacksjs/stacks#2928.
45
+ */
46
+ sections?: Record<string, boolean>
42
47
  stats?: Array<{ label: string, value: string }>
43
48
  httpMetrics?: DashboardHttpMetric[]
44
49
  services?: SystemHealthItem[]
@@ -98,6 +103,7 @@ export async function fetchDashboardHome(): Promise<DashboardHomeResponse> {
98
103
  }
99
104
 
100
105
  export function useDashboard() {
106
+ const sections = ref<Record<string, boolean>>({})
101
107
  const stats = ref<DashboardStats[]>([])
102
108
  const httpMetrics = ref<DashboardHttpMetric[]>([])
103
109
  const recentActivity = ref<ActivityItem[]>([])
@@ -109,6 +115,7 @@ export function useDashboard() {
109
115
  async function fetchDashboardStats(timeRange = '7d') {
110
116
  void timeRange
111
117
  const data = await fetchDashboardHome()
118
+ sections.value = data.sections && typeof data.sections === 'object' ? data.sections : {}
112
119
  stats.value = normalizeStats(data.stats)
113
120
  issues.value = Array.isArray(data.issues) ? data.issues : []
114
121
  }
@@ -132,7 +139,8 @@ export function useDashboard() {
132
139
 
133
140
  try {
134
141
  const data = await fetchDashboardHome()
135
- stats.value = normalizeStats(data.stats)
142
+ sections.value = data.sections && typeof data.sections === 'object' ? data.sections : {}
143
+ stats.value = normalizeStats(data.stats)
136
144
  httpMetrics.value = Array.isArray(data.httpMetrics) ? data.httpMetrics : []
137
145
  recentActivity.value = normalizeActivities(data.activities)
138
146
  systemHealth.value = Array.isArray(data.services) ? data.services : []
@@ -157,6 +165,7 @@ export function useDashboard() {
157
165
  }
158
166
 
159
167
  return {
168
+ sections,
160
169
  stats,
161
170
  httpMetrics,
162
171
  recentActivity,
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.242",
5
+ "version": "0.75.244",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.242",
5
+ "version": "0.75.244",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@iconify-json/f7": "^1.2.2",
46
46
  "@iconify-json/hugeicons": "^1.2.35",
47
- "@stacksjs/mobile": "0.75.242",
47
+ "@stacksjs/mobile": "0.75.244",
48
48
  "@stacksjs/sanitizer": "^0.2.534",
49
49
  "@xterm/addon-fit": "^0.11.0",
50
50
  "@xterm/xterm": "^6.0.0",
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import type { ManufacturerRecord } from '../../../../app/Actions/Dashboard/Commerce/manufacturer-records'
3
5
 
@@ -5,67 +7,62 @@ const records = useReactiveProp('records', [] as ManufacturerRecord[])
5
7
  const loading = useReactiveProp('loading', false)
6
8
  const emit = defineEmits()
7
9
 
8
- function formatDate(value: string): string {
9
- if (!value)
10
- return 'Not recorded'
11
- const normalized = /^\d{4}-\d{2}-\d{2} \d/.test(value)
12
- ? `${value.replace(' ', 'T')}Z`
13
- : value
14
- const date = new Date(normalized)
15
- return Number.isNaN(date.getTime())
16
- ? value
17
- : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
10
+ /**
11
+ * `featured` becomes a flag badge beside the name rather than the starred line
12
+ * it used to be under it. The information is the same and it reads as a table
13
+ * column; the star is dropped because a `cell` slot cannot carry markup yet
14
+ * (stacksjs/stx#2095, stacksjs/stx#2096).
15
+ *
16
+ * `createdAt` is a `date` column, and `UI/Table` now normalises SQLite's
17
+ * space-separated timestamps the way this component used to do for itself.
18
+ */
19
+ const COLUMNS = [
20
+ { key: 'name', label: 'Manufacturer', sortable: true, emptyText: 'Unnamed manufacturer', class: 'min-w-48 font-medium text-gray-900 dark:text-white' },
21
+ {
22
+ key: 'featuredFlag',
23
+ label: '',
24
+ type: 'badge' as const,
25
+ emptyText: '',
26
+ badgeClasses: { featured: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40' },
27
+ badgeLabels: { featured: 'Featured' },
28
+ searchable: false,
29
+ },
30
+ { key: 'description', label: 'Description', emptyText: 'No description recorded.', class: 'max-w-md min-w-64 line-clamp-2 text-gray-500 dark:text-neutral-400' },
31
+ { key: 'country', label: 'Country', sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
32
+ { key: 'productCount', label: 'Products', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono' },
33
+ { key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, class: 'whitespace-nowrap' },
34
+ ]
35
+
36
+ const ROW_ACTIONS = [
37
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
38
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
39
+ ]
40
+
41
+ /** A badge column maps a row field, so the flag needs one. */
42
+ const rows = derived(() => records().map(record => ({
43
+ ...record,
44
+ featuredFlag: record.featured ? 'featured' : '',
45
+ })))
46
+
47
+ function handleRowAction(payload: { action: string, row: ManufacturerRecord }): void {
48
+ emit(payload.action, payload.row)
18
49
  }
19
50
  </script>
20
51
 
21
- <div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
22
- <div :if="loading() && records().length === 0" class="flex items-center justify-center p-10 text-gray-500 text-sm dark:text-neutral-400">
23
- <span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
24
- Loading manufacturers
25
- </div>
26
- <div :else-if="records().length === 0" class="p-10 text-center">
27
- <p class="font-medium text-gray-900 text-sm dark:text-white">No manufacturers found</p>
28
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Add a manufacturer or adjust the current filters.</p>
29
- </div>
30
- <div :else class="overflow-x-auto">
31
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
32
- <thead class="bg-gray-50 dark:bg-neutral-900/50">
33
- <tr>
34
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Manufacturer</th>
35
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Description</th>
36
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Country</th>
37
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-right text-xs uppercase dark:text-neutral-400">Products</th>
38
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Created</th>
39
- <th scope="col" class="relative px-4 py-3"><span class="sr-only">Actions</span></th>
40
- </tr>
41
- </thead>
42
- <tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
43
- <template :for="record in records()" :key="record.id">
44
- <tr class="hover:bg-gray-50/60 dark:hover:bg-neutral-700/40">
45
- <td class="px-4 py-4 min-w-48">
46
- <p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.name || 'Unnamed manufacturer' }}</p>
47
- <span :if="record.featured" class="inline-flex items-center mt-1 font-medium text-blue-700 text-xs dark:text-blue-300">
48
- <span class="mr-1 h-3.5 w-3.5 i-hugeicons-star"></span>
49
- Featured
50
- </span>
51
- </td>
52
- <td class="px-4 py-4 max-w-md min-w-64 text-gray-500 text-sm dark:text-neutral-400">
53
- <p class="line-clamp-2">{{ record.description || 'No description recorded.' }}</p>
54
- </td>
55
- <td class="px-4 py-4 text-gray-700 text-sm whitespace-nowrap dark:text-neutral-200">{{ record.country || 'Not recorded' }}</td>
56
- <td class="px-4 py-4 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ record.productCount }}</td>
57
- <td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatDate(record.createdAt) }}</td>
58
- <td class="px-4 py-3 text-right whitespace-nowrap">
59
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.name" @click="emit('edit', record)">
60
- <span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
61
- </Button>
62
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.name" @click="emit('delete', record)">
63
- <span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
64
- </Button>
65
- </td>
66
- </tr>
67
- </template>
68
- </tbody>
69
- </table>
70
- </div>
71
- </div>
52
+ {{--
53
+ No `searchable` or `filterable`: the parent dashboard already owns the
54
+ search box and the filter selects, and a second set inside the table would
55
+ duplicate them. Paging is what was missing, and Table brings it.
56
+ --}}
57
+ <Table
58
+ :columns="COLUMNS"
59
+ :data="rows()"
60
+ :loading="loading()"
61
+ :row-actions="ROW_ACTIONS"
62
+ action-label-key="name"
63
+ paginated
64
+ :page-size="25"
65
+ empty-title="No manufacturers found"
66
+ empty-description="Add a manufacturer or adjust the current filters."
67
+ @row-action="handleRowAction"
68
+ />
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import type { ProductUnitRecord } from '../../../../app/Actions/Dashboard/Commerce/product-unit-records'
3
5
 
@@ -5,67 +7,63 @@ const records = useReactiveProp('records', [] as ProductUnitRecord[])
5
7
  const loading = useReactiveProp('loading', false)
6
8
  const emit = defineEmits()
7
9
 
8
- function formatDate(value: string): string {
9
- if (!value)
10
- return 'Unknown'
11
- const date = new Date(value)
12
- return Number.isNaN(date.getTime())
13
- ? value
14
- : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
15
- }
16
- </script>
10
+ /**
11
+ * No slots. `UI/Table`'s `cell` and `actions` slots cannot be used yet -
12
+ * slot content loses its `{{ … }}` (stacksjs/stx#2095) and `stx typecheck`
13
+ * rejects every scoped slot prop (stacksjs/stx#2096) - so the two custom
14
+ * cells here are expressed as column options instead: `subtitleKey` for the
15
+ * abbreviation under the name, and a flag `badge` column for the default
16
+ * marker.
17
+ */
18
+ const COLUMNS = [
19
+ { key: 'name', label: 'Unit', sortable: true, subtitleKey: 'abbreviation', class: 'font-medium text-gray-900 dark:text-white' },
20
+ {
21
+ key: 'defaultFlag',
22
+ label: '',
23
+ type: 'badge' as const,
24
+ // A flag rather than a status: only the default unit is marked, and the
25
+ // rest of the column stays blank.
26
+ emptyText: '',
27
+ badgeClasses: { default: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40' },
28
+ badgeLabels: { default: 'Default' },
29
+ searchable: false,
30
+ },
31
+ { key: 'type', label: 'Type', sortable: true, class: 'whitespace-nowrap' },
32
+ { key: 'productName', label: 'Product', sortable: true, emptyText: 'Unassigned' },
33
+ { key: 'description', label: 'Description', class: 'max-w-sm', emptyText: 'No description' },
34
+ { key: 'createdAt', label: 'Created', type: 'datetime' as const, sortable: true, class: 'whitespace-nowrap' },
35
+ ]
17
36
 
18
- <div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
19
- <div :if="loading() && records().length === 0" class="flex items-center justify-center py-16 text-gray-500 text-sm dark:text-neutral-400">
20
- <span class="mr-2 h-5 w-5 animate-spin i-hugeicons-loading-03"></span>
21
- Loading product units
22
- </div>
37
+ const ROW_ACTIONS = [
38
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
39
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
40
+ ]
23
41
 
24
- <div :else-if="records().length === 0" class="px-6 py-16 text-center">
25
- <span class="inline-block h-8 w-8 text-gray-400 i-hugeicons-package"></span>
26
- <p class="mt-3 font-medium text-gray-900 text-sm dark:text-white">No product units found</p>
27
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the filters or add a ProductUnit record.</p>
28
- </div>
42
+ /** `defaultFlag` carries the badge value, since a badge column maps a row field. */
43
+ const rows = derived(() => records().map(record => ({
44
+ ...record,
45
+ defaultFlag: record.isDefault ? 'default' : '',
46
+ })))
47
+
48
+ function handleRowAction(payload: { action: string, row: ProductUnitRecord }): void {
49
+ emit(payload.action, payload.row)
50
+ }
51
+ </script>
29
52
 
30
- <div :else class="overflow-x-auto">
31
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
32
- <thead class="bg-gray-50 dark:bg-neutral-900/50">
33
- <tr>
34
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Unit</th>
35
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Type</th>
36
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Product</th>
37
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Description</th>
38
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Created</th>
39
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-right text-xs dark:text-neutral-300"><span class="sr-only">Actions</span></th>
40
- </tr>
41
- </thead>
42
- <tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
43
- <template :for="record in records()" :key="record.id">
44
- <tr class="align-top">
45
- <td class="px-4 py-4">
46
- <div class="flex gap-2 items-center">
47
- <p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.name }}</p>
48
- <span :if="record.isDefault" class="px-2 py-0.5 font-medium text-emerald-700 text-xs dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40 rounded-full">Default</span>
49
- </div>
50
- <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ record.abbreviation }}</p>
51
- </td>
52
- <td class="px-4 py-4 text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ record.type }}</td>
53
- <td class="px-4 py-4 text-gray-600 text-sm dark:text-neutral-300">{{ record.productName || 'Unassigned' }}</td>
54
- <td class="px-4 py-4 max-w-sm text-gray-600 text-sm dark:text-neutral-300">{{ record.description || 'No description' }}</td>
55
- <td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatDate(record.createdAt) }}</td>
56
- <td class="px-4 py-4">
57
- <div class="flex gap-1 justify-end">
58
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.name" @click="emit('edit', record)">
59
- <span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
60
- </Button>
61
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.name" @click="emit('delete', record)">
62
- <span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span>
63
- </Button>
64
- </div>
65
- </td>
66
- </tr>
67
- </template>
68
- </tbody>
69
- </table>
70
- </div>
71
- </div>
53
+ {{--
54
+ No `searchable` or `filterable`: the parent dashboard already owns the
55
+ search box and the filter selects, and a second set inside the table would
56
+ duplicate them. Paging is what was missing, and Table brings it.
57
+ --}}
58
+ <Table
59
+ :columns="COLUMNS"
60
+ :data="rows()"
61
+ :loading="loading()"
62
+ :row-actions="ROW_ACTIONS"
63
+ action-label-key="name"
64
+ paginated
65
+ :page-size="25"
66
+ empty-title="No product units found"
67
+ empty-description="Adjust the filters or add a ProductUnit record."
68
+ @row-action="handleRowAction"
69
+ />