@stacksjs/defaults 0.75.267 → 0.75.268
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.
|
@@ -162,9 +162,17 @@ export default defineModel({
|
|
|
162
162
|
*/
|
|
163
163
|
|
|
164
164
|
/** Legacy owner column, written as a copy of `tokenable_id`. */
|
|
165
|
+
/**
|
|
166
|
+
* The legacy owner column, a copy of `tokenableId` kept for installs that
|
|
167
|
+
* predate the pair. `ensureAuthTables()` gives it no constraint, since a
|
|
168
|
+
* token can belong to an author as well as a user, so it is not a foreign
|
|
169
|
+
* key. Without saying so, convention inferred `users.id` from the name and
|
|
170
|
+
* a fresh app's first `buddy doctor` failed on it.
|
|
171
|
+
*/
|
|
165
172
|
userId: {
|
|
166
173
|
order: 10,
|
|
167
174
|
guarded: true,
|
|
175
|
+
foreignKey: false,
|
|
168
176
|
validation: {
|
|
169
177
|
rule: schema.number(),
|
|
170
178
|
},
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run `parseDashboardDate` in a process pinned to `zone`, since a calendar
|
|
5
|
+
* date only goes wrong west of UTC and a test process's zone is fixed at
|
|
6
|
+
* start-up.
|
|
7
|
+
*/
|
|
8
|
+
async function inZone(zone: string, expression: string): Promise<string> {
|
|
9
|
+
const child = Bun.spawn([process.execPath, '--no-env-file', '-e', `
|
|
10
|
+
const { parseDashboardDate } = await import(${JSON.stringify(`${import.meta.dir}/dashboard-dates.ts`)})
|
|
11
|
+
console.log(${expression})
|
|
12
|
+
`], { env: { ...process.env, TZ: zone }, stdout: 'pipe', stderr: 'pipe' })
|
|
13
|
+
const [code, out, err] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()])
|
|
14
|
+
expect(code, err).toBe(0)
|
|
15
|
+
return out.trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const calendarDay = `[parseDashboardDate('2026-10-01')].map(d => [d.getFullYear(), d.getMonth() + 1, d.getDate()].join('-'))[0]`
|
|
19
|
+
|
|
20
|
+
describe('parseDashboardDate', () => {
|
|
21
|
+
for (const zone of ['America/Los_Angeles', 'UTC', 'Asia/Manila', 'Pacific/Kiritimati']) {
|
|
22
|
+
it(`reads a calendar date as that day in ${zone}`, async () => {
|
|
23
|
+
expect(await inZone(zone, calendarDay)).toBe('2026-10-1')
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it('reads an offsetless database timestamp as UTC, wherever it runs', async () => {
|
|
28
|
+
for (const zone of ['America/Los_Angeles', 'Asia/Manila'])
|
|
29
|
+
expect(await inZone(zone, `parseDashboardDate('2026-10-01 14:30:00').toISOString()`)).toBe('2026-10-01T14:30:00.000Z')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('leaves an ISO string with an offset, and a Date, as they are', async () => {
|
|
33
|
+
expect(await inZone('America/Los_Angeles', `parseDashboardDate('2026-10-01T14:30:00+08:00').toISOString()`)).toBe('2026-10-01T06:30:00.000Z')
|
|
34
|
+
const at = new Date('2026-10-01T00:00:00Z')
|
|
35
|
+
const { parseDashboardDate } = await import('./dashboard-dates')
|
|
36
|
+
expect(parseDashboardDate(at)).toBe(at)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('the reading it replaces is the bug: a calendar date lands on the day before west of UTC', async () => {
|
|
40
|
+
// The control. Without it, every assertion above would pass in a zone
|
|
41
|
+
// where the two readings agree.
|
|
42
|
+
expect(await inZone('America/Los_Angeles', `[new Date('2026-10-01')].map(d => d.getDate())[0]`)).toBe('30')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a date value from a dashboard row.
|
|
3
|
+
*
|
|
4
|
+
* A calendar date (`2026-10-01`) is a day, not an instant, so it is read as
|
|
5
|
+
* local midnight. `new Date('2026-10-01')` reads it as UTC midnight instead,
|
|
6
|
+
* which every zone west of UTC renders as the day before: a coupon starting
|
|
7
|
+
* on October 1st showed September 30th in Los Angeles.
|
|
8
|
+
*
|
|
9
|
+
* A database timestamp written without an offset (`2026-10-01 14:30:00`) is
|
|
10
|
+
* UTC, which is how the framework writes it, so it gains the `Z` it lacks.
|
|
11
|
+
* Anything else, ISO strings with an offset among them, is left to `Date`.
|
|
12
|
+
*/
|
|
13
|
+
export function parseDashboardDate(value: unknown): Date {
|
|
14
|
+
if (value instanceof Date)
|
|
15
|
+
return value
|
|
16
|
+
const text = String(value)
|
|
17
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text))
|
|
18
|
+
return new Date(`${text}T00:00:00`)
|
|
19
|
+
if (/^\d{4}-\d{2}-\d{2} \d/.test(text))
|
|
20
|
+
return new Date(`${text.replace(' ', 'T')}Z`)
|
|
21
|
+
return new Date(text)
|
|
22
|
+
}
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.268",
|
|
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.
|
|
5
|
+
"version": "0.75.268",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@iconify-json/f7": "^1.2.2",
|
|
47
47
|
"@iconify-json/hugeicons": "^1.2.35",
|
|
48
|
-
"@stacksjs/mobile": "0.75.
|
|
49
|
-
"@stacksjs/sanitizer": "0.2.
|
|
50
|
-
"@stacksjs/stx": "0.2.
|
|
48
|
+
"@stacksjs/mobile": "0.75.268",
|
|
49
|
+
"@stacksjs/sanitizer": "0.2.667",
|
|
50
|
+
"@stacksjs/stx": "0.2.667",
|
|
51
51
|
"@xterm/addon-fit": "^0.11.0",
|
|
52
52
|
"@xterm/xterm": "^6.0.0",
|
|
53
53
|
"ts-qr-codes": "^0.1.8"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
|
+
import { parseDashboardDate } from '../../../../functions/dashboard-dates'
|
|
3
4
|
/**
|
|
4
5
|
* The data table.
|
|
5
6
|
*
|
|
@@ -276,14 +277,14 @@ function displayValue(row: DashboardTableRow, column: DashboardTableColumn): str
|
|
|
276
277
|
return Number.isFinite(amount) ? amount.toLocaleString() : String(value)
|
|
277
278
|
}
|
|
278
279
|
if (column.type === 'date') {
|
|
279
|
-
const at =
|
|
280
|
+
const at = parseDashboardDate(value)
|
|
280
281
|
return Number.isNaN(at.getTime()) ? String(value) : at.toLocaleDateString()
|
|
281
282
|
}
|
|
282
283
|
// An event or log row is ordered by when it happened, and to the minute, so
|
|
283
284
|
// the date alone is not enough to read it. Separate from `date` rather than
|
|
284
285
|
// a flag on it because the two render differently everywhere they appear.
|
|
285
286
|
if (column.type === 'datetime') {
|
|
286
|
-
const at =
|
|
287
|
+
const at = parseDashboardDate(value)
|
|
287
288
|
return Number.isNaN(at.getTime())
|
|
288
289
|
? String(value)
|
|
289
290
|
: at.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
|
|
@@ -346,20 +347,6 @@ function hasBadge(row: DashboardTableRow, column: DashboardTableColumn): boolean
|
|
|
346
347
|
return badgeLabel(row, column) !== ''
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
/**
|
|
350
|
-
* Parse a stored timestamp.
|
|
351
|
-
*
|
|
352
|
-
* SQLite hands back `2026-01-01 12:00:00`, with a space and no zone. `new
|
|
353
|
-
* Date()` reads that as local time where it reads it at all, so every row
|
|
354
|
-
* shifted by the viewer's offset - a date near midnight landing on the wrong
|
|
355
|
-
* day. The hand-rolled tables each normalised it themselves; this is the same
|
|
356
|
-
* knowledge in the one place that needs it.
|
|
357
|
-
*/
|
|
358
|
-
function toDate(value: unknown): Date {
|
|
359
|
-
const text = String(value)
|
|
360
|
-
return new Date(/^\d{4}-\d{2}-\d{2} \d/.test(text) ? `${text.replace(' ', 'T')}Z` : text)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
350
|
/** The pill's text: the mapped label, else the value with its underscores read as spaces. */
|
|
364
351
|
function badgeLabel(row: DashboardTableRow, column: DashboardTableColumn): string {
|
|
365
352
|
const value = rawValue(row, column.key)
|
|
@@ -397,7 +384,7 @@ function compare(a: any, b: any, type: DashboardTableColumn['type']): number {
|
|
|
397
384
|
// comparison below, which is only accidentally right for ISO timestamps and
|
|
398
385
|
// wrong for every other format.
|
|
399
386
|
if (type === 'date' || type === 'datetime')
|
|
400
|
-
return
|
|
387
|
+
return parseDashboardDate(a).getTime() - parseDashboardDate(b).getTime()
|
|
401
388
|
if (type === 'boolean')
|
|
402
389
|
return (a ? 1 : 0) - (b ? 1 : 0)
|
|
403
390
|
|