@stoker-platform/web-app 0.5.209 → 0.5.211
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/CHANGELOG.md +17 -0
- package/package.json +4 -4
- package/src/Images.tsx +131 -35
- package/src/Record.tsx +10 -6
- package/src/utils/getRelationFields.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @stoker-platform/web-app
|
|
2
2
|
|
|
3
|
+
## 0.5.211
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- perf: improve Record page performance
|
|
8
|
+
|
|
9
|
+
## 0.5.210
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- feat: improve assigment behaviour
|
|
14
|
+
- feat: enable clipboard write access in CSP
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
- @stoker-platform/web-client@0.5.86
|
|
17
|
+
- @stoker-platform/node-client@0.5.82
|
|
18
|
+
- @stoker-platform/utils@0.5.73
|
|
19
|
+
|
|
3
20
|
## 0.5.209
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stoker-platform/web-app",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.211",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"scripts": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
52
52
|
"@react-google-maps/api": "^2.20.8",
|
|
53
53
|
"@sentry/react": "^10.56.0",
|
|
54
|
-
"@stoker-platform/node-client": "0.5.
|
|
55
|
-
"@stoker-platform/utils": "0.5.
|
|
56
|
-
"@stoker-platform/web-client": "0.5.
|
|
54
|
+
"@stoker-platform/node-client": "0.5.82",
|
|
55
|
+
"@stoker-platform/utils": "0.5.73",
|
|
56
|
+
"@stoker-platform/web-client": "0.5.86",
|
|
57
57
|
"@tanstack/react-table": "^8.21.3",
|
|
58
58
|
"@types/react": "18.3.13",
|
|
59
59
|
"@types/react-dom": "18.3.1",
|
package/src/Images.tsx
CHANGED
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
getCollectionConfigModule,
|
|
16
16
|
getCurrentUserPermissions,
|
|
17
17
|
onStokerPermissionsChange,
|
|
18
|
+
onStokerSignOut,
|
|
18
19
|
subscribeOne,
|
|
19
20
|
} from "@stoker-platform/web-client"
|
|
20
|
-
import { createElement, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
21
|
+
import { createElement, memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"
|
|
21
22
|
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card"
|
|
22
23
|
import { useGoToRecord } from "./utils/goToRecord"
|
|
23
24
|
import { LoadingSpinner } from "./components/ui/loading-spinner"
|
|
@@ -118,6 +119,54 @@ interface RowProps {
|
|
|
118
119
|
data: RowData
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
const pendingAssign = new Map<string, boolean>()
|
|
123
|
+
const assignQueue = new Map<string, Promise<unknown>>()
|
|
124
|
+
const assignSettledAt = new Map<string, number>()
|
|
125
|
+
const assignListeners = new Set<() => void>()
|
|
126
|
+
let assignVersion = 0
|
|
127
|
+
|
|
128
|
+
const ASSIGN_SETTLE_MS = 3000
|
|
129
|
+
|
|
130
|
+
const notifyAssign = () => {
|
|
131
|
+
assignVersion++
|
|
132
|
+
assignListeners.forEach((listener) => listener())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const subscribeAssign = (listener: () => void) => {
|
|
136
|
+
assignListeners.add(listener)
|
|
137
|
+
return () => {
|
|
138
|
+
assignListeners.delete(listener)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let setAssignGlobalLoading: (operation: "+" | "-", id: string, server?: boolean, cache?: boolean) => void | undefined
|
|
143
|
+
|
|
144
|
+
onStokerSignOut(() => {
|
|
145
|
+
pendingAssign.clear()
|
|
146
|
+
assignQueue.clear()
|
|
147
|
+
assignSettledAt.clear()
|
|
148
|
+
assignListeners.clear()
|
|
149
|
+
assignVersion = 0
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const assignKey = (recordId: string, parentId: string) => `${recordId}:${parentId}`
|
|
153
|
+
|
|
154
|
+
const enqueueAssign = (recordId: string, call: () => Promise<unknown>) => {
|
|
155
|
+
const wasIdle = !assignQueue.has(recordId)
|
|
156
|
+
if (wasIdle) setAssignGlobalLoading?.("+", recordId)
|
|
157
|
+
const next = (assignQueue.get(recordId) ?? Promise.resolve()).catch(() => undefined).then(call)
|
|
158
|
+
assignQueue.set(recordId, next)
|
|
159
|
+
next.finally(() => {
|
|
160
|
+
if (assignQueue.get(recordId) === next) {
|
|
161
|
+
assignQueue.delete(recordId)
|
|
162
|
+
assignSettledAt.set(recordId, Date.now())
|
|
163
|
+
setAssignGlobalLoading?.("-", recordId)
|
|
164
|
+
setTimeout(notifyAssign, ASSIGN_SETTLE_MS + 50)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
return next
|
|
168
|
+
}
|
|
169
|
+
|
|
121
170
|
const Row = ({ index, style, data }: RowProps) => {
|
|
122
171
|
const goToRecord = useGoToRecord()
|
|
123
172
|
const {
|
|
@@ -138,42 +187,85 @@ const Row = ({ index, style, data }: RowProps) => {
|
|
|
138
187
|
const group = groupedRecords[index]
|
|
139
188
|
const customization = getCollectionConfigModule(collection.labels.collection)
|
|
140
189
|
const { setGlobalLoading } = useGlobalLoading()
|
|
190
|
+
setAssignGlobalLoading = setGlobalLoading
|
|
141
191
|
const { toast } = useToast()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
192
|
+
useSyncExternalStore(subscribeAssign, () => assignVersion)
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!relationParent || !relationList?.field) return
|
|
196
|
+
let changed = false
|
|
197
|
+
for (const record of group) {
|
|
198
|
+
const key = assignKey(record.id, relationParent.id)
|
|
199
|
+
const pending = pendingAssign.get(key)
|
|
200
|
+
if (pending === undefined || assignQueue.has(record.id)) continue
|
|
201
|
+
const settledAt = assignSettledAt.get(record.id) ?? 0
|
|
202
|
+
if (Date.now() - settledAt < ASSIGN_SETTLE_MS) continue
|
|
203
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
204
|
+
const serverChecked = !!record[relationList.field]?.[relationParent.id]
|
|
205
|
+
if (pending === serverChecked) {
|
|
206
|
+
pendingAssign.delete(key)
|
|
207
|
+
assignSettledAt.delete(record.id)
|
|
208
|
+
changed = true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (changed) notifyAssign()
|
|
212
|
+
}, [group, relationParent, relationList?.field, assignVersion])
|
|
213
|
+
|
|
214
|
+
const handleCheckedChange = useCallback(
|
|
215
|
+
(checked: boolean, record: StokerRecord) => {
|
|
216
|
+
if (!relationCollection || !relationList?.field || !relationParent) return
|
|
217
|
+
|
|
218
|
+
const key = assignKey(record.id, relationParent.id)
|
|
219
|
+
pendingAssign.set(key, checked)
|
|
220
|
+
notifyAssign()
|
|
221
|
+
|
|
222
|
+
enqueueAssign(record.id, () =>
|
|
223
|
+
callFunction(
|
|
224
|
+
`stoker-assign${relationCollection.labels.record.toLowerCase()}${collection.labels.collection.toLowerCase()}`,
|
|
225
|
+
{
|
|
226
|
+
operation: checked ? "add" : "remove",
|
|
227
|
+
parentId: relationParent.id,
|
|
228
|
+
recordId: record.id,
|
|
229
|
+
},
|
|
230
|
+
).catch(() => {
|
|
231
|
+
if (pendingAssign.get(key) === checked) {
|
|
232
|
+
pendingAssign.delete(key)
|
|
233
|
+
notifyAssign()
|
|
234
|
+
}
|
|
235
|
+
toast({
|
|
236
|
+
title: "Error",
|
|
237
|
+
description: `Error assigning ${collection.labels.record} to ${relationCollection.labels.record}`,
|
|
238
|
+
variant: "destructive",
|
|
239
|
+
duration: 10000000,
|
|
240
|
+
})
|
|
241
|
+
}),
|
|
242
|
+
)
|
|
243
|
+
},
|
|
244
|
+
[
|
|
245
|
+
relationCollection,
|
|
246
|
+
relationList?.field,
|
|
247
|
+
relationParent,
|
|
248
|
+
collection.labels.collection,
|
|
249
|
+
collection.labels.record,
|
|
250
|
+
toast,
|
|
251
|
+
],
|
|
252
|
+
)
|
|
167
253
|
|
|
168
254
|
return (
|
|
169
255
|
<div style={style} className={cn("grid", "gap-4", "pb-4", cols)}>
|
|
170
256
|
{group.map((record) => {
|
|
171
257
|
// eslint-disable-next-line security/detect-object-injection
|
|
172
258
|
const title = recordTitleField ? record[recordTitleField] : record.id
|
|
259
|
+
const pending =
|
|
260
|
+
isAssigning && relationParent
|
|
261
|
+
? pendingAssign.get(assignKey(record.id, relationParent.id))
|
|
262
|
+
: undefined
|
|
173
263
|
const checked =
|
|
174
264
|
isAssigning && relationList?.field && relationParent
|
|
175
|
-
?
|
|
265
|
+
? // eslint-disable-next-line security/detect-object-injection
|
|
266
|
+
(pending ?? !!record[relationList.field]?.[relationParent.id])
|
|
176
267
|
: undefined
|
|
268
|
+
const hasPending = pending !== undefined
|
|
177
269
|
let unavailable
|
|
178
270
|
if (isAssigning) {
|
|
179
271
|
if (assignable?.unavailableField) {
|
|
@@ -258,17 +350,17 @@ const Row = ({ index, style, data }: RowProps) => {
|
|
|
258
350
|
}),
|
|
259
351
|
hooks: import.meta.glob("./hooks/*.{ts,tsx}", { eager: true }),
|
|
260
352
|
utils: import.meta.glob("./lib/*.{ts,tsx}", { eager: true }),
|
|
353
|
+
setGlobalLoading,
|
|
261
354
|
})}
|
|
262
355
|
</div>
|
|
263
356
|
)}
|
|
264
|
-
{isAssigning && assignable && (assignable.isAvailable(record) || checked) && (
|
|
357
|
+
{isAssigning && assignable && (assignable.isAvailable(record) || checked || hasPending) && (
|
|
265
358
|
<div className="pb-4">
|
|
266
359
|
<div className="flex items-center justify-center space-x-3 min-h-8">
|
|
267
360
|
<Switch
|
|
268
361
|
id={`${record.id}-assigned`}
|
|
269
362
|
className="data-[state=checked]:bg-blue-500"
|
|
270
363
|
checked={checked}
|
|
271
|
-
disabled={checkedDisabled}
|
|
272
364
|
onCheckedChange={(checked) => handleCheckedChange(checked, record)}
|
|
273
365
|
/>
|
|
274
366
|
{imagesConfig.size !== "sm" && (
|
|
@@ -277,13 +369,17 @@ const Row = ({ index, style, data }: RowProps) => {
|
|
|
277
369
|
</div>
|
|
278
370
|
</div>
|
|
279
371
|
)}
|
|
280
|
-
{isAssigning &&
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
372
|
+
{isAssigning &&
|
|
373
|
+
assignable &&
|
|
374
|
+
!assignable.isAvailable(record) &&
|
|
375
|
+
!checked &&
|
|
376
|
+
!hasPending && (
|
|
377
|
+
<div className="pb-4">
|
|
378
|
+
<div className="flex items-center justify-center space-x-3 min-h-8">
|
|
379
|
+
{unavailable}
|
|
380
|
+
</div>
|
|
284
381
|
</div>
|
|
285
|
-
|
|
286
|
-
)}
|
|
382
|
+
)}
|
|
287
383
|
<div className={cn("grid", "gap-4", size)}>
|
|
288
384
|
{record[imagesConfig.imageField] ? (
|
|
289
385
|
<CopyImageOverlay src={record[imagesConfig.imageField]} className="w-full h-full">
|
package/src/Record.tsx
CHANGED
|
@@ -36,6 +36,10 @@ import { Breadcrumbs } from "./Breadcrumbs"
|
|
|
36
36
|
import { Separator } from "./components/ui/separator"
|
|
37
37
|
import { loadAssigning } from "./utils/relationListFiltersState"
|
|
38
38
|
|
|
39
|
+
const customPageComponents = import.meta.glob("./components/ui/*.tsx", { eager: true })
|
|
40
|
+
const customPageHooks = import.meta.glob("./hooks/*.{ts,tsx}", { eager: true })
|
|
41
|
+
const customPageUtils = import.meta.glob("./lib/*.{ts,tsx}", { eager: true })
|
|
42
|
+
|
|
39
43
|
export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
40
44
|
const { labels, fields, recordTitleField } = collection
|
|
41
45
|
const { path: pathString, id } = useParams()
|
|
@@ -207,7 +211,9 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
207
211
|
</Button>
|
|
208
212
|
</Card>
|
|
209
213
|
<Card className="flex items-center gap-2 h-12 min-w-[300px] p-5">
|
|
210
|
-
<div
|
|
214
|
+
<div className="flex size-6 shrink-0 items-center justify-center">
|
|
215
|
+
{icon ? createElement(icon) : null}
|
|
216
|
+
</div>
|
|
211
217
|
<h1 className="truncate">{recordTitle}</h1>
|
|
212
218
|
</Card>
|
|
213
219
|
</header>
|
|
@@ -346,11 +352,9 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
346
352
|
{createElement(page.component, {
|
|
347
353
|
record,
|
|
348
354
|
collection,
|
|
349
|
-
components:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
hooks: import.meta.glob("./hooks/*.{ts,tsx}", { eager: true }),
|
|
353
|
-
utils: import.meta.glob("./lib/*.{ts,tsx}", { eager: true }),
|
|
355
|
+
components: customPageComponents,
|
|
356
|
+
hooks: customPageHooks,
|
|
357
|
+
utils: customPageUtils,
|
|
354
358
|
...page.props,
|
|
355
359
|
})}
|
|
356
360
|
</main>
|
|
@@ -30,7 +30,7 @@ export const getRelationFields = (collection: CollectionSchema) => {
|
|
|
30
30
|
if (refs.length === 0) continue
|
|
31
31
|
const titleField = field.titleField
|
|
32
32
|
if (
|
|
33
|
-
(
|
|
33
|
+
(fieldCustomization.admin?.condition?.form === undefined || condition) &&
|
|
34
34
|
(!(titleField && field.includeFields?.includes(titleField)) || queryFullRecord)
|
|
35
35
|
) {
|
|
36
36
|
relationFields.push(field.name)
|