@kernhq/module-inventory 0.1.2 → 0.2.0
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/README.md +55 -9
- package/dist/contract/capabilities.d.ts +49 -0
- package/dist/contract/capabilities.d.ts.map +1 -0
- package/dist/contract/capabilities.js +52 -0
- package/dist/contract/capabilities.js.map +1 -0
- package/dist/contract/events.d.ts +33 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +22 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +15 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +15 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +146 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +107 -0
- package/dist/contract/models.js.map +1 -0
- package/dist/contract/permissions.d.ts +22 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +26 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/{contract.d.ts → contract/router.d.ts} +24 -85
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +42 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/contract/settings.d.ts +18 -0
- package/dist/contract/settings.d.ts.map +1 -0
- package/dist/contract/settings.js +29 -0
- package/dist/contract/settings.js.map +1 -0
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/{_impl.d.ts → router.d.ts} +33 -8
- package/dist/server/{_impl.d.ts.map → router.d.ts.map} +1 -1
- package/dist/server/router.js +83 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +25 -11
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +28 -10
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/assets.d.ts +73 -0
- package/dist/server/services/assets.d.ts.map +1 -0
- package/dist/server/services/assets.js +261 -0
- package/dist/server/services/assets.js.map +1 -0
- package/dist/server/services/index.d.ts +10 -0
- package/dist/server/services/index.d.ts.map +1 -0
- package/dist/server/services/index.js +15 -0
- package/dist/server/services/index.js.map +1 -0
- package/dist/server/services/notify.d.ts +63 -0
- package/dist/server/services/notify.d.ts.map +1 -0
- package/dist/server/services/notify.js +105 -0
- package/dist/server/services/notify.js.map +1 -0
- package/migrations/0000_init.sql +12 -3
- package/migrations/0001_rls.sql +24 -0
- package/migrations/meta/0000_snapshot.json +40 -13
- package/migrations/meta/_journal.json +2 -2
- package/package.json +5 -4
- package/src/client/api.ts +1 -1
- package/src/client/components/AssetFormDialog.svelte +130 -47
- package/src/client/i18n.ts +11 -166
- package/src/client/index.ts +8 -1
- package/src/client/messages.test.ts +169 -0
- package/src/client/messages.ts +399 -0
- package/src/client/mock.test.ts +161 -0
- package/src/client/mock.ts +267 -45
- package/src/client/module.ts +22 -2
- package/src/client/pages/AssetsPage.svelte +358 -137
- package/src/client/permissions.ts +1 -1
- package/src/client/price.test.ts +106 -0
- package/src/client/price.ts +135 -0
- package/src/client/query.test.ts +58 -0
- package/src/client/query.ts +15 -2
- package/src/client/settings/GeneralSettings.svelte +0 -0
- package/src/client/settings/core-api.ts +32 -0
- package/src/client/widgets/OverviewWidget.svelte +16 -3
- package/src/contract/capabilities.ts +55 -0
- package/src/contract/events.ts +34 -0
- package/src/contract/index.ts +15 -0
- package/src/contract/models.ts +123 -0
- package/src/contract/permissions.ts +26 -0
- package/src/contract/router.ts +46 -0
- package/src/contract/settings.ts +30 -0
- package/src/module.test.ts +140 -7
- package/src/server/index.ts +16 -13
- package/src/server/inventory.int.test.ts +819 -0
- package/src/server/migrations.test.ts +138 -0
- package/src/server/router.ts +118 -0
- package/src/server/schema.ts +27 -10
- package/src/server/services/assets.ts +368 -0
- package/src/server/services/index.ts +23 -0
- package/src/server/services/notify.ts +151 -0
- package/tsconfig.base.json +22 -0
- package/tsconfig.client.json +1 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +18 -3
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js +0 -119
- package/dist/contract.js.map +0 -1
- package/dist/server/_impl.js +0 -204
- package/dist/server/_impl.js.map +0 -1
- package/src/contract.ts +0 -143
- package/src/server/_impl.ts +0 -275
package/src/client/mock.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Asset } from '../contract.js'
|
|
1
|
+
import type { Asset, AssetStatus } from '../contract/index.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The in-memory implementation of this module's API.
|
|
@@ -8,64 +8,269 @@ import type { Asset } from '../contract.js'
|
|
|
8
8
|
* never checks an env var itself.
|
|
9
9
|
*
|
|
10
10
|
* Keep it in step with the contract. A module whose mock is missing a procedure has a working page
|
|
11
|
-
* and a broken demo, in exactly the environment used to show the product
|
|
11
|
+
* and a broken demo, in exactly the environment used to show the product — and one whose mock
|
|
12
|
+
* ignores a filter has a demo that contradicts its own controls.
|
|
13
|
+
*
|
|
14
|
+
* **It contradicted the product in five measured ways, and this file is the answer to all five.**
|
|
15
|
+
* It returned seed order while the server defaults to newest first; it issued `INV-0013` for the
|
|
16
|
+
* seventh asset because one counter was doing two jobs; it served page one for ever for any cursor
|
|
17
|
+
* it did not recognise, which is an endless "Load more" where the server answers `BAD_REQUEST`; and
|
|
18
|
+
* it ignored `sort`, `categoryId` and `custodianUserId` entirely. So the ordering, the paging and
|
|
19
|
+
* the cursor format below are deliberately the same as `src/server/services/assets.ts` — read them
|
|
20
|
+
* as one pair, and change them as one pair.
|
|
21
|
+
*/
|
|
22
|
+
type AssetSort = 'recent' | 'name' | 'code'
|
|
23
|
+
|
|
24
|
+
interface Seed {
|
|
25
|
+
code: string
|
|
26
|
+
name: string
|
|
27
|
+
status: AssetStatus
|
|
28
|
+
serialNumber?: string | null
|
|
29
|
+
location?: string | null
|
|
30
|
+
warrantyUntil?: string | null
|
|
31
|
+
archived?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SEEDS: Seed[] = [
|
|
35
|
+
{
|
|
36
|
+
code: 'INV-0001',
|
|
37
|
+
name: 'MacBook Pro 14"',
|
|
38
|
+
status: 'assigned',
|
|
39
|
+
serialNumber: 'C02X1234JGH7',
|
|
40
|
+
location: 'Istanbul · 3rd floor',
|
|
41
|
+
warrantyUntil: '2027-03-14',
|
|
42
|
+
},
|
|
43
|
+
{ code: 'INV-0002', name: 'Dell UltraSharp 27"', status: 'in_stock', location: 'Istanbul · store room' },
|
|
44
|
+
{
|
|
45
|
+
code: 'INV-0003',
|
|
46
|
+
name: 'iPhone 15',
|
|
47
|
+
status: 'under_repair',
|
|
48
|
+
serialNumber: 'F17GX9QKLM',
|
|
49
|
+
location: 'With the repairer',
|
|
50
|
+
},
|
|
51
|
+
{ code: 'INV-0004', name: 'Herman Miller Aeron', status: 'assigned', location: 'Istanbul · 2nd floor' },
|
|
52
|
+
{ code: 'INV-0005', name: 'Canon EOS R6', status: 'reserved', location: 'Istanbul · store room' },
|
|
53
|
+
{ code: 'INV-0006', name: 'ThinkPad X1 Carbon', status: 'retired', archived: true },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An error shaped like one the real client surfaces.
|
|
58
|
+
*
|
|
59
|
+
* oRPC hands a failure to the screen as an `Error` carrying the contract's `code`, and the screens
|
|
60
|
+
* show `error.message`. A mock that throws a bare `Error` teaches a demo that every failure looks
|
|
61
|
+
* the same, and hides the one branch — `BAD_REQUEST` on a stale page marker — this file exists to
|
|
62
|
+
* reproduce.
|
|
12
63
|
*/
|
|
13
|
-
|
|
64
|
+
class MockApiError extends Error {
|
|
65
|
+
constructor(
|
|
66
|
+
readonly code: 'BAD_REQUEST' | 'NOT_FOUND',
|
|
67
|
+
message: string,
|
|
68
|
+
) {
|
|
69
|
+
super(message)
|
|
70
|
+
this.name = 'MockApiError'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The server's page boundary, byte for byte: base64url of `{i: <row id>, s: <sort>}`.
|
|
76
|
+
*
|
|
77
|
+
* `btoa` rather than `Buffer` because this runs in a browser, and it is safe here for the reason
|
|
78
|
+
* the server's version is small: the payload is a uuid and a sort name, so it is pure ASCII.
|
|
79
|
+
*/
|
|
80
|
+
const base64url = (text: string) => btoa(text).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
81
|
+
|
|
82
|
+
const fromBase64url = (text: string) =>
|
|
83
|
+
atob(
|
|
84
|
+
text
|
|
85
|
+
.replace(/-/g, '+')
|
|
86
|
+
.replace(/_/g, '/')
|
|
87
|
+
.padEnd(Math.ceil(text.length / 4) * 4, '='),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
91
|
+
|
|
92
|
+
const encodeCursor = (bookmark: { i: string; s: AssetSort }) => base64url(JSON.stringify(bookmark))
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Refuse a marker this list did not issue, exactly where the server refuses it.
|
|
96
|
+
*
|
|
97
|
+
* The old mock read an unknown cursor as "start at 0", so `nextCursor` was non-null for ever and
|
|
98
|
+
* "Load more" served page one until somebody closed the tab. A cursor bound to the wrong sort is
|
|
99
|
+
* the same class of bug and is refused for the same reason.
|
|
100
|
+
*/
|
|
101
|
+
function decodeCursor(cursor: string, sort: AssetSort): { i: string; s: AssetSort } {
|
|
102
|
+
const refuse = () => new MockApiError('BAD_REQUEST', 'That page marker is not one this list issued')
|
|
103
|
+
let parsed: { i?: unknown; s?: unknown } | null
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(fromBase64url(cursor)) as { i?: unknown; s?: unknown } | null
|
|
106
|
+
} catch {
|
|
107
|
+
throw refuse()
|
|
108
|
+
}
|
|
109
|
+
if (typeof parsed?.i !== 'string' || !UUID.test(parsed.i)) throw refuse()
|
|
110
|
+
if (parsed.s !== sort) throw refuse()
|
|
111
|
+
return { i: parsed.i, s: sort }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Which field an ordering sorts on. `recent` is the id, because a uuidv7 is already in time order. */
|
|
115
|
+
const sortKeyOf = (asset: Asset, sort: AssetSort) =>
|
|
116
|
+
sort === 'name' ? asset.name : sort === 'code' ? asset.code : asset.id
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `(sortKey, id)` against `(sortKey, id)` — the tuple the server's row comparison uses.
|
|
120
|
+
*
|
|
121
|
+
* The id tiebreak is what makes the boundary total: two assets with the same name would otherwise
|
|
122
|
+
* have no defined order between them, and a page boundary that lands between them would repeat or
|
|
123
|
+
* skip a row. JS compares by code point where Postgres compares by collation, which is close enough
|
|
124
|
+
* for six seeded rows and is the one place this file is an approximation rather than a mirror.
|
|
125
|
+
*/
|
|
126
|
+
function compare(a: Asset, b: Asset, sort: AssetSort): number {
|
|
127
|
+
const ka = sortKeyOf(a, sort)
|
|
128
|
+
const kb = sortKeyOf(b, sort)
|
|
129
|
+
if (ka !== kb) return ka < kb ? -1 : 1
|
|
130
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
131
|
+
}
|
|
14
132
|
|
|
15
133
|
export function createMockInventoryApi() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Two counters, because they were one and it showed.
|
|
136
|
+
*
|
|
137
|
+
* `blank()` incremented the id counter and `create` then formatted a code out of it, so six seeds
|
|
138
|
+
* consumed six numbers and the seventh asset somebody added in a demo was filed as `INV-0013`
|
|
139
|
+
* beside `INV-0006`. An id and a human-readable tag are separate sequences on the server too.
|
|
140
|
+
*/
|
|
141
|
+
let nextIdNumber = 1
|
|
142
|
+
let nextCodeNumber = SEEDS.length + 1
|
|
20
143
|
|
|
21
|
-
|
|
144
|
+
const newId = () => `01920000-0000-7000-8000-${String(nextIdNumber++).padStart(12, '0')}`
|
|
145
|
+
const pad = (n: number) => `INV-${String(n).padStart(4, '0')}`
|
|
146
|
+
|
|
147
|
+
function blank(code: string, seed: Partial<Seed> & { name?: string } = {}): Asset {
|
|
22
148
|
const now = new Date().toISOString()
|
|
23
149
|
return {
|
|
24
|
-
id,
|
|
150
|
+
id: newId(),
|
|
25
151
|
workspaceId: '' as Asset['workspaceId'],
|
|
26
|
-
code,
|
|
27
|
-
name,
|
|
152
|
+
code: seed.code ?? code,
|
|
153
|
+
name: seed.name ?? '',
|
|
28
154
|
description: '',
|
|
29
155
|
categoryId: null,
|
|
30
|
-
status,
|
|
156
|
+
status: seed.status ?? 'in_stock',
|
|
31
157
|
custodianUserId: null,
|
|
32
158
|
custodySince: null,
|
|
33
|
-
serialNumber: null,
|
|
34
|
-
location: null,
|
|
159
|
+
serialNumber: seed.serialNumber ?? null,
|
|
160
|
+
location: seed.location ?? null,
|
|
35
161
|
purchasedOn: null,
|
|
36
162
|
purchasedFrom: null,
|
|
37
163
|
priceMinor: null,
|
|
38
164
|
currency: null,
|
|
39
|
-
warrantyUntil: null,
|
|
165
|
+
warrantyUntil: seed.warrantyUntil ?? null,
|
|
40
166
|
photoFileId: null,
|
|
167
|
+
custom: {},
|
|
41
168
|
createdAt: now,
|
|
42
169
|
updatedAt: now,
|
|
43
|
-
archivedAt: null,
|
|
170
|
+
archivedAt: seed.archived ? now : null,
|
|
44
171
|
}
|
|
45
172
|
}
|
|
46
173
|
|
|
174
|
+
const assets: Asset[] = SEEDS.map((seed, i) => blank(pad(i + 1), seed))
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The workspace every seeded row belongs to, learned from the first call that names one.
|
|
178
|
+
*
|
|
179
|
+
* The seeds are built before any workspace exists, and only `list` used to stamp its argument
|
|
180
|
+
* onto what it returned — so the same asset came back with a real `workspaceId` from the list and
|
|
181
|
+
* an empty one from `get`, `update` and `archive`. A demo where one row disagrees with itself
|
|
182
|
+
* about which workspace it is in is the kind of thing nobody notices until a screen keys a cache
|
|
183
|
+
* off it.
|
|
184
|
+
*/
|
|
185
|
+
let workspace = '' as Asset['workspaceId']
|
|
186
|
+
const remember = (id?: string) => {
|
|
187
|
+
if (id) workspace = id as Asset['workspaceId']
|
|
188
|
+
}
|
|
189
|
+
const stamp = (asset: Asset): Asset => ({ ...asset, workspaceId: workspace })
|
|
190
|
+
|
|
191
|
+
const matches = (a: Asset, q?: string) =>
|
|
192
|
+
!q ||
|
|
193
|
+
[a.name, a.code, a.serialNumber ?? ''].some((field) => field.toLowerCase().includes(q.toLowerCase()))
|
|
194
|
+
|
|
195
|
+
const find = (assetId: string) => {
|
|
196
|
+
const asset = assets.find((a) => a.id === assetId)
|
|
197
|
+
if (!asset) throw new MockApiError('NOT_FOUND', 'Asset not found')
|
|
198
|
+
return asset
|
|
199
|
+
}
|
|
200
|
+
|
|
47
201
|
return {
|
|
48
202
|
assets: {
|
|
49
|
-
list: async ({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
203
|
+
list: async ({
|
|
204
|
+
workspaceId,
|
|
205
|
+
q,
|
|
206
|
+
status,
|
|
207
|
+
categoryId,
|
|
208
|
+
custodianUserId,
|
|
209
|
+
archived = false,
|
|
210
|
+
sort = 'recent',
|
|
211
|
+
limit = 50,
|
|
212
|
+
cursor,
|
|
213
|
+
}: {
|
|
214
|
+
workspaceId: string
|
|
215
|
+
q?: string
|
|
216
|
+
status?: AssetStatus
|
|
217
|
+
categoryId?: string
|
|
218
|
+
custodianUserId?: string
|
|
219
|
+
archived?: boolean
|
|
220
|
+
sort?: AssetSort
|
|
221
|
+
limit?: number
|
|
222
|
+
cursor?: string
|
|
223
|
+
}) => {
|
|
224
|
+
remember(workspaceId)
|
|
225
|
+
// Every control the screen offers filters here, or the demo argues with its own toolbar.
|
|
226
|
+
const all = assets
|
|
227
|
+
.filter((a) => matches(a, q))
|
|
228
|
+
.filter((a) => (status ? a.status === status : true))
|
|
229
|
+
.filter((a) => (categoryId ? a.categoryId === categoryId : true))
|
|
230
|
+
.filter((a) => (custodianUserId ? a.custodianUserId === custodianUserId : true))
|
|
231
|
+
.filter((a) => (archived ? true : !a.archivedAt))
|
|
232
|
+
.map(stamp)
|
|
233
|
+
|
|
234
|
+
// `recent` is the only descending order, and it is the contract's default — the list opens
|
|
235
|
+
// on what was added last, which is what the server does and what the old mock did not.
|
|
236
|
+
const descending = sort === 'recent'
|
|
237
|
+
all.sort((a, b) => (descending ? -compare(a, b, sort) : compare(a, b, sort)))
|
|
238
|
+
|
|
239
|
+
let rows = all
|
|
240
|
+
if (cursor) {
|
|
241
|
+
const mark = decodeCursor(cursor, sort)
|
|
242
|
+
const bookmarked = assets.find((a) => a.id === mark.i)
|
|
243
|
+
/**
|
|
244
|
+
* A row archived or removed between two pages leaves nothing to compare against and the
|
|
245
|
+
* page simply ends — the server's deliberate choice, because somebody else editing while
|
|
246
|
+
* you read is an ordinary race rather than a malformed request.
|
|
247
|
+
*/
|
|
248
|
+
if (!bookmarked) rows = []
|
|
249
|
+
else
|
|
250
|
+
rows = all.filter((a) => {
|
|
251
|
+
const order = compare(a, bookmarked, sort)
|
|
252
|
+
return descending ? order < 0 : order > 0
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// limit + 1 to learn whether there is a next page, exactly as the server does.
|
|
257
|
+
const window = rows.slice(0, limit + 1)
|
|
258
|
+
const items = window.slice(0, limit)
|
|
259
|
+
const last = items.at(-1)
|
|
260
|
+
const nextCursor = window.length > limit && last ? encodeCursor({ i: last.id, s: sort }) : null
|
|
261
|
+
return { items, nextCursor }
|
|
59
262
|
},
|
|
263
|
+
|
|
264
|
+
get: async ({ workspaceId, assetId }: { workspaceId?: string; assetId: string }) => {
|
|
265
|
+
remember(workspaceId)
|
|
266
|
+
return stamp(find(assetId))
|
|
267
|
+
},
|
|
268
|
+
|
|
60
269
|
create: async ({ workspaceId, ...rest }: { workspaceId: string } & Record<string, unknown>) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
String(rest.name),
|
|
66
|
-
'in_stock',
|
|
67
|
-
),
|
|
68
|
-
workspaceId: workspaceId as Asset['workspaceId'],
|
|
270
|
+
remember(workspaceId)
|
|
271
|
+
const asset: Asset = {
|
|
272
|
+
...blank(pad(nextCodeNumber++)),
|
|
273
|
+
name: String(rest.name ?? ''),
|
|
69
274
|
description: String(rest.description ?? ''),
|
|
70
275
|
serialNumber: (rest.serialNumber as string | undefined) ?? null,
|
|
71
276
|
location: (rest.location as string | undefined) ?? null,
|
|
@@ -75,20 +280,37 @@ export function createMockInventoryApi() {
|
|
|
75
280
|
priceMinor: (rest.priceMinor as number | undefined) ?? null,
|
|
76
281
|
currency: (rest.currency as string | undefined) ?? null,
|
|
77
282
|
}
|
|
78
|
-
assets.
|
|
79
|
-
return
|
|
283
|
+
assets.push(asset)
|
|
284
|
+
return stamp(asset)
|
|
80
285
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
286
|
+
|
|
287
|
+
update: async ({
|
|
288
|
+
assetId,
|
|
289
|
+
workspaceId,
|
|
290
|
+
...rest
|
|
291
|
+
}: { assetId: string; workspaceId?: string } & Record<string, unknown>) => {
|
|
292
|
+
remember(workspaceId)
|
|
293
|
+
const asset = find(assetId)
|
|
294
|
+
// `workspaceId` is not among the fields assigned: it is routing, not a field of the asset,
|
|
295
|
+
// and letting a patch carry it means a demo can move a row to a workspace that is not real.
|
|
296
|
+
Object.assign(asset, rest, { updatedAt: new Date().toISOString() })
|
|
297
|
+
return stamp(asset)
|
|
86
298
|
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
299
|
+
|
|
300
|
+
archive: async ({
|
|
301
|
+
workspaceId,
|
|
302
|
+
assetId,
|
|
303
|
+
archived,
|
|
304
|
+
}: {
|
|
305
|
+
workspaceId?: string
|
|
306
|
+
assetId: string
|
|
307
|
+
archived?: boolean
|
|
308
|
+
}) => {
|
|
309
|
+
remember(workspaceId)
|
|
310
|
+
const asset = find(assetId)
|
|
90
311
|
asset.archivedAt = archived === false ? null : new Date().toISOString()
|
|
91
|
-
|
|
312
|
+
asset.updatedAt = new Date().toISOString()
|
|
313
|
+
return stamp(asset)
|
|
92
314
|
},
|
|
93
315
|
},
|
|
94
316
|
}
|
package/src/client/module.ts
CHANGED
|
@@ -89,8 +89,28 @@ export const inventoryClientModule = defineClientModule({
|
|
|
89
89
|
},
|
|
90
90
|
],
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
92
|
+
/**
|
|
93
|
+
* A settings page's `id` is its URL: the shell mounts it at `/<ws>/settings/inventory/<id>`.
|
|
94
|
+
*
|
|
95
|
+
* This was `[]` while the server enforced `assetCodePrefix` and `assetCodePad` on every asset it
|
|
96
|
+
* created — two settings nobody, at any permission level, could change. `core.modules.manage`
|
|
97
|
+
* rather than an inventory permission, because that is what core gates
|
|
98
|
+
* `workspaces.modules.updateSettings` on: offering the page on a wider permission shows somebody
|
|
99
|
+
* a form the server then refuses on save.
|
|
100
|
+
*/
|
|
101
|
+
settingsPages: [
|
|
102
|
+
{
|
|
103
|
+
id: 'general',
|
|
104
|
+
get label() {
|
|
105
|
+
return t('settings_general')
|
|
106
|
+
},
|
|
107
|
+
icon: 'sliders-vertical',
|
|
108
|
+
scope: 'workspace',
|
|
109
|
+
permission: 'core.modules.manage',
|
|
110
|
+
order: 1,
|
|
111
|
+
component: () => import('./settings/GeneralSettings.svelte'),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
94
114
|
})
|
|
95
115
|
|
|
96
116
|
export default inventoryClientModule
|