@stoker-platform/web-app 0.5.48 → 0.5.50
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 +15 -0
- package/package.json +4 -4
- package/src/Collection.tsx +77 -10
- package/src/Filters.tsx +11 -2
- package/src/Images.tsx +158 -8
- package/src/Record.tsx +21 -13
- package/src/RecordSidebar.tsx +79 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @stoker-platform/web-app
|
|
2
2
|
|
|
3
|
+
## 0.5.50
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix: prevent clearing relation list filter
|
|
8
|
+
|
|
9
|
+
## 0.5.49
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- feat: add relation record assignment feature
|
|
14
|
+
- @stoker-platform/node-client@0.5.32
|
|
15
|
+
- @stoker-platform/utils@0.5.26
|
|
16
|
+
- @stoker-platform/web-client@0.5.28
|
|
17
|
+
|
|
3
18
|
## 0.5.48
|
|
4
19
|
|
|
5
20
|
### 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.50",
|
|
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.38.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.32",
|
|
55
|
+
"@stoker-platform/utils": "0.5.26",
|
|
56
|
+
"@stoker-platform/web-client": "0.5.28",
|
|
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/Collection.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Assignable,
|
|
2
3
|
CalendarConfig,
|
|
3
4
|
CardsConfig,
|
|
4
5
|
CollectionField,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
GetSomeOptions,
|
|
29
30
|
getTimezone,
|
|
30
31
|
keepTimezone,
|
|
32
|
+
onStokerSignOut,
|
|
31
33
|
subscribeMany,
|
|
32
34
|
SubscribeManyOptions,
|
|
33
35
|
updateRecord,
|
|
@@ -110,6 +112,8 @@ interface CollectionProps {
|
|
|
110
112
|
relationList?: RelationList
|
|
111
113
|
relationCollection?: CollectionSchema
|
|
112
114
|
relationParent?: StokerRecord
|
|
115
|
+
isAssigning?: boolean
|
|
116
|
+
assignable?: Assignable
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
export interface Query {
|
|
@@ -120,7 +124,11 @@ export interface Query {
|
|
|
120
124
|
}[]
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
|
|
127
|
+
let hasFirstPageLoaded: { [key: StokerCollection]: boolean } = {}
|
|
128
|
+
|
|
129
|
+
onStokerSignOut(() => {
|
|
130
|
+
hasFirstPageLoaded = {}
|
|
131
|
+
})
|
|
124
132
|
|
|
125
133
|
function Collection({
|
|
126
134
|
collection,
|
|
@@ -131,6 +139,8 @@ function Collection({
|
|
|
131
139
|
relationList,
|
|
132
140
|
relationCollection,
|
|
133
141
|
relationParent,
|
|
142
|
+
isAssigning,
|
|
143
|
+
assignable,
|
|
134
144
|
}: CollectionProps) {
|
|
135
145
|
const navigate = useNavigate()
|
|
136
146
|
const location = useLocation()
|
|
@@ -216,6 +226,8 @@ function Collection({
|
|
|
216
226
|
const { filters, setFilters, order, setOrder, getFilterConstraints } = useFilters()
|
|
217
227
|
const { orderByField, orderByDirection } = useMemo(() => getOrderBy(collection, order), [order])
|
|
218
228
|
const searchResults = useRef<{ [key: string | number]: string[] | undefined }>({})
|
|
229
|
+
const additionalConstraintsRef = useRef(additionalConstraints)
|
|
230
|
+
additionalConstraintsRef.current = additionalConstraints
|
|
219
231
|
const { currentField: currentFieldAll } = useCache()
|
|
220
232
|
const currentField = currentFieldAll[labels.collection]
|
|
221
233
|
const [backToStartKey, setBackToStartKey] = useState(0)
|
|
@@ -335,6 +347,31 @@ function Collection({
|
|
|
335
347
|
serverListRef.current = serverList
|
|
336
348
|
}, [serverList])
|
|
337
349
|
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
if (isAssigning !== undefined) {
|
|
352
|
+
setBackToStartKey((prev) => prev + 1)
|
|
353
|
+
}
|
|
354
|
+
if (!relationList || !isInitialized) return
|
|
355
|
+
setFilters((prev) => {
|
|
356
|
+
let next = prev
|
|
357
|
+
.filter((filter) => !(filter.type === "relation" && filter.field === relationList.field))
|
|
358
|
+
.map((filter: Filter) => {
|
|
359
|
+
if (filter.type === "status" || filter.type === "range") return filter
|
|
360
|
+
if (filter.type === "select" && filter.defaultValue && isAssigning !== undefined) {
|
|
361
|
+
return {
|
|
362
|
+
...filter,
|
|
363
|
+
value: tryFunction(filter.defaultValue, [relationCollection, relationParent, isAssigning]),
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return filter
|
|
367
|
+
})
|
|
368
|
+
if (!isAssigning) {
|
|
369
|
+
next = [...next, { type: "relation", field: relationList.field, value: relationParent?.id }]
|
|
370
|
+
}
|
|
371
|
+
return next
|
|
372
|
+
})
|
|
373
|
+
}, [isAssigning])
|
|
374
|
+
|
|
338
375
|
// This is to ensure that the optimistic list is set in cases where cached documents exactly match the downloaded server documents
|
|
339
376
|
// In this case, the cache-only snapshot listener does not fire a second time when the cache has loaded because there is no change to the list
|
|
340
377
|
useEffect(() => {
|
|
@@ -562,7 +599,7 @@ function Collection({
|
|
|
562
599
|
[labels.collection],
|
|
563
600
|
[
|
|
564
601
|
...(currentQuery.constraints as QueryConstraint[]),
|
|
565
|
-
...(
|
|
602
|
+
...(additionalConstraintsRef.current?.map((constraint) =>
|
|
566
603
|
where(constraint[0], constraint[1] as WhereFilterOp, constraint[2]),
|
|
567
604
|
) || []),
|
|
568
605
|
],
|
|
@@ -612,7 +649,7 @@ function Collection({
|
|
|
612
649
|
[labels.collection],
|
|
613
650
|
[
|
|
614
651
|
...(query.queries[0].constraints as [string, WhereFilterOp, unknown][]),
|
|
615
|
-
...(
|
|
652
|
+
...(additionalConstraintsRef.current || []),
|
|
616
653
|
],
|
|
617
654
|
options as GetSomeOptions,
|
|
618
655
|
)
|
|
@@ -872,10 +909,31 @@ function Collection({
|
|
|
872
909
|
if (filter.type === "status" || filter.type === "range") {
|
|
873
910
|
return
|
|
874
911
|
}
|
|
875
|
-
if (
|
|
876
|
-
filter.
|
|
912
|
+
if (
|
|
913
|
+
filter.type === "relation" &&
|
|
914
|
+
filter.field === relationList.field &&
|
|
915
|
+
relationParent &&
|
|
916
|
+
!isAssigning
|
|
917
|
+
) {
|
|
918
|
+
filter.value = relationParent.id
|
|
919
|
+
}
|
|
920
|
+
if (filter.type === "select" && filter.defaultValue) {
|
|
921
|
+
filter.value = tryFunction(filter.defaultValue, [
|
|
922
|
+
relationCollection,
|
|
923
|
+
relationParent,
|
|
924
|
+
isAssigning,
|
|
925
|
+
])
|
|
877
926
|
}
|
|
878
927
|
})
|
|
928
|
+
if (
|
|
929
|
+
relationParent &&
|
|
930
|
+
!isAssigning &&
|
|
931
|
+
!filtersClone.some(
|
|
932
|
+
(filter: Filter) => filter.type === "relation" && filter.field === relationList.field,
|
|
933
|
+
)
|
|
934
|
+
) {
|
|
935
|
+
filtersClone.push({ type: "relation", field: relationList.field, value: relationParent.id })
|
|
936
|
+
}
|
|
879
937
|
}
|
|
880
938
|
|
|
881
939
|
if (statusField || softDelete) {
|
|
@@ -1058,7 +1116,10 @@ function Collection({
|
|
|
1058
1116
|
setOrder({ field: recordTitleField, direction: "asc" })
|
|
1059
1117
|
}
|
|
1060
1118
|
|
|
1061
|
-
|
|
1119
|
+
if (!relationList) {
|
|
1120
|
+
hasFirstPageLoaded[labels.collection] = true
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1062
1123
|
setIsInitialized(true)
|
|
1063
1124
|
}
|
|
1064
1125
|
|
|
@@ -1130,17 +1191,19 @@ function Collection({
|
|
|
1130
1191
|
return (
|
|
1131
1192
|
filters
|
|
1132
1193
|
.filter((filter) => filter.type !== "status" && filter.type !== "range")
|
|
1194
|
+
.filter((filter) => !relationList || filter.field !== relationList.field)
|
|
1133
1195
|
.filter(
|
|
1134
1196
|
(filter) =>
|
|
1135
1197
|
(filter.value ||
|
|
1136
1198
|
(filter.type === "select" &&
|
|
1137
1199
|
filter.defaultValue &&
|
|
1138
|
-
tryFunction(filter.defaultValue, [relationCollection, relationParent]) &&
|
|
1200
|
+
tryFunction(filter.defaultValue, [relationCollection, relationParent, isAssigning]) &&
|
|
1139
1201
|
!filter.value)) &&
|
|
1140
1202
|
!(
|
|
1141
1203
|
filter.type === "select" &&
|
|
1142
1204
|
filter.defaultValue &&
|
|
1143
|
-
tryFunction(filter.defaultValue, [relationCollection, relationParent]) ===
|
|
1205
|
+
tryFunction(filter.defaultValue, [relationCollection, relationParent, isAssigning]) ===
|
|
1206
|
+
filter.value
|
|
1144
1207
|
),
|
|
1145
1208
|
)
|
|
1146
1209
|
.filter((filter) => !excludedFilters.includes(filter.field)).length > 0
|
|
@@ -1956,7 +2019,7 @@ function Collection({
|
|
|
1956
2019
|
<Filters
|
|
1957
2020
|
collection={collection}
|
|
1958
2021
|
excluded={excludedFilters}
|
|
1959
|
-
relationList={
|
|
2022
|
+
relationList={relationList}
|
|
1960
2023
|
/>
|
|
1961
2024
|
</SheetContent>
|
|
1962
2025
|
</Sheet>
|
|
@@ -2289,8 +2352,12 @@ function Collection({
|
|
|
2289
2352
|
unsubscribe={unsubscribe}
|
|
2290
2353
|
search={search}
|
|
2291
2354
|
backToStartKey={backToStartKey}
|
|
2292
|
-
relationList={
|
|
2355
|
+
relationList={relationList}
|
|
2356
|
+
relationCollection={relationCollection}
|
|
2357
|
+
relationParent={relationParent}
|
|
2293
2358
|
formList={!!formList}
|
|
2359
|
+
isAssigning={isAssigning}
|
|
2360
|
+
assignable={assignable}
|
|
2294
2361
|
/>
|
|
2295
2362
|
</TabsContent>
|
|
2296
2363
|
<TabsContent value="map">
|
package/src/Filters.tsx
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CollectionField,
|
|
3
|
+
CollectionSchema,
|
|
4
|
+
Filter,
|
|
5
|
+
RelationList,
|
|
6
|
+
StokerCollection,
|
|
7
|
+
StokerRecord,
|
|
8
|
+
} from "@stoker-platform/types"
|
|
2
9
|
import {
|
|
3
10
|
collectionAccess,
|
|
4
11
|
getCachedConfigValue,
|
|
@@ -40,7 +47,7 @@ import { useConnection } from "./providers/ConnectionProvider"
|
|
|
40
47
|
interface FiltersProps {
|
|
41
48
|
collection: CollectionSchema
|
|
42
49
|
excluded: string[]
|
|
43
|
-
relationList?:
|
|
50
|
+
relationList?: RelationList
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export function Filters({ collection, excluded, relationList }: FiltersProps) {
|
|
@@ -83,6 +90,7 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
|
|
|
83
90
|
const fullCollectionAccess = collectionPermissions && collectionAccess("Read", collectionPermissions)
|
|
84
91
|
const dependencyAccess = hasDependencyAccess(relationCollection, schema, permissions)
|
|
85
92
|
if (!fullCollectionAccess && dependencyAccess.length === 0) return false
|
|
93
|
+
if (relationList && relationList.field === filter.field) return false
|
|
86
94
|
return true
|
|
87
95
|
}, [])
|
|
88
96
|
|
|
@@ -719,6 +727,7 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
|
|
|
719
727
|
}
|
|
720
728
|
filters.forEach((filter) => {
|
|
721
729
|
if (filter.type === "status" || filter.type === "range") return
|
|
730
|
+
if (relationList && relationList.field === filter.field) return
|
|
722
731
|
const field = getField(fields, filter.field)
|
|
723
732
|
if (!field) return
|
|
724
733
|
if (filter.type === "select") {
|
package/src/Images.tsx
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Assignable,
|
|
2
3
|
CollectionMeta,
|
|
3
4
|
CollectionSchema,
|
|
4
5
|
ImagesConfig,
|
|
6
|
+
RelationList,
|
|
5
7
|
StokerCollection,
|
|
6
8
|
StokerPermissions,
|
|
7
9
|
StokerRecord,
|
|
8
10
|
} from "@stoker-platform/types"
|
|
9
|
-
import { getCachedConfigValue } from "@stoker-platform/utils"
|
|
11
|
+
import { getCachedConfigValue, getField, getFieldCustomization, tryFunction } from "@stoker-platform/utils"
|
|
10
12
|
import {
|
|
13
|
+
callFunction,
|
|
11
14
|
Cursor,
|
|
12
15
|
getCollectionConfigModule,
|
|
13
16
|
getCurrentUserPermissions,
|
|
@@ -35,6 +38,11 @@ import { localFullTextSearch } from "./utils/localFullTextSearch"
|
|
|
35
38
|
import { Helmet } from "react-helmet"
|
|
36
39
|
import { useConnection } from "./providers/ConnectionProvider"
|
|
37
40
|
import { getSafeUrl } from "./utils/isSafeUrl"
|
|
41
|
+
import { Switch } from "./components/ui/switch"
|
|
42
|
+
import { Label } from "./components/ui/label"
|
|
43
|
+
import { Badge } from "./components/ui/badge"
|
|
44
|
+
import { useGlobalLoading } from "./providers/LoadingProvider"
|
|
45
|
+
import { useToast } from "./hooks/use-toast"
|
|
38
46
|
|
|
39
47
|
export const description = "A list of records as cards. The content area has a search bar in the header."
|
|
40
48
|
|
|
@@ -91,6 +99,11 @@ interface RowData {
|
|
|
91
99
|
lineClamp: string
|
|
92
100
|
recordTitleField: string | undefined
|
|
93
101
|
imagesConfig: ImagesConfig
|
|
102
|
+
isAssigning: boolean | undefined
|
|
103
|
+
assignable: Assignable | undefined
|
|
104
|
+
relationList: RelationList | undefined
|
|
105
|
+
relationCollection: CollectionSchema | undefined
|
|
106
|
+
relationParent: StokerRecord | undefined
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
interface RowProps {
|
|
@@ -101,14 +114,107 @@ interface RowProps {
|
|
|
101
114
|
|
|
102
115
|
const Row = ({ index, style, data }: RowProps) => {
|
|
103
116
|
const goToRecord = useGoToRecord()
|
|
104
|
-
const {
|
|
117
|
+
const {
|
|
118
|
+
collection,
|
|
119
|
+
groupedRecords,
|
|
120
|
+
size,
|
|
121
|
+
cols,
|
|
122
|
+
lineClamp,
|
|
123
|
+
recordTitleField,
|
|
124
|
+
imagesConfig,
|
|
125
|
+
isAssigning,
|
|
126
|
+
assignable,
|
|
127
|
+
relationList,
|
|
128
|
+
relationCollection,
|
|
129
|
+
relationParent,
|
|
130
|
+
} = data
|
|
105
131
|
// eslint-disable-next-line security/detect-object-injection
|
|
106
132
|
const group = groupedRecords[index]
|
|
133
|
+
const customization = getCollectionConfigModule(collection.labels.collection)
|
|
134
|
+
const { setGlobalLoading } = useGlobalLoading()
|
|
135
|
+
const { toast } = useToast()
|
|
136
|
+
|
|
137
|
+
const [checkedDisabled, setCheckedDisabled] = useState(false)
|
|
138
|
+
|
|
139
|
+
const handleCheckedChange = useCallback(async (checked: boolean, record: StokerRecord) => {
|
|
140
|
+
if (!relationCollection) return
|
|
141
|
+
setCheckedDisabled(true)
|
|
142
|
+
setGlobalLoading("+", record.id, true)
|
|
143
|
+
await callFunction(
|
|
144
|
+
`stoker-assign${relationCollection.labels.record.toLowerCase()}${collection.labels.collection.toLowerCase()}`,
|
|
145
|
+
{
|
|
146
|
+
operation: checked ? "add" : "remove",
|
|
147
|
+
parentId: relationParent?.id,
|
|
148
|
+
recordId: record.id,
|
|
149
|
+
},
|
|
150
|
+
).catch(() => {
|
|
151
|
+
toast({
|
|
152
|
+
title: "Error",
|
|
153
|
+
description: `Error assigning ${collection.labels.record} to ${relationCollection.labels.record}`,
|
|
154
|
+
variant: "destructive",
|
|
155
|
+
duration: 10000000,
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
setGlobalLoading("-", record.id, true)
|
|
159
|
+
setCheckedDisabled(false)
|
|
160
|
+
}, [])
|
|
161
|
+
|
|
107
162
|
return (
|
|
108
163
|
<div style={style} className={cn("grid", "gap-4", "pb-4", cols)}>
|
|
109
164
|
{group.map((record) => {
|
|
110
165
|
// eslint-disable-next-line security/detect-object-injection
|
|
111
166
|
const title = recordTitleField ? record[recordTitleField] : record.id
|
|
167
|
+
const checked =
|
|
168
|
+
isAssigning && relationList?.field && relationParent
|
|
169
|
+
? !!record[relationList.field]?.[relationParent.id]
|
|
170
|
+
: undefined
|
|
171
|
+
let unavailable
|
|
172
|
+
if (isAssigning) {
|
|
173
|
+
if (assignable?.unavailableField) {
|
|
174
|
+
const unavailableFieldSchema = getField(collection.fields, assignable?.unavailableField)
|
|
175
|
+
const unavailableFieldCustomization = getFieldCustomization(
|
|
176
|
+
unavailableFieldSchema,
|
|
177
|
+
customization,
|
|
178
|
+
)
|
|
179
|
+
const badge = tryFunction(unavailableFieldCustomization.admin?.badge, [record])
|
|
180
|
+
if (badge === true) {
|
|
181
|
+
unavailable = (
|
|
182
|
+
<Badge variant="outline" className="text-xs text-center">
|
|
183
|
+
{record[assignable.unavailableField]
|
|
184
|
+
? record[assignable.unavailableField]
|
|
185
|
+
: "Not available"}
|
|
186
|
+
</Badge>
|
|
187
|
+
)
|
|
188
|
+
} else {
|
|
189
|
+
unavailable = (
|
|
190
|
+
<Badge
|
|
191
|
+
variant={
|
|
192
|
+
["outline", "destructive", "primary", "secondary"].includes(badge)
|
|
193
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
194
|
+
(badge as any)
|
|
195
|
+
: "outline"
|
|
196
|
+
}
|
|
197
|
+
className={cn(
|
|
198
|
+
"text-xs text-center",
|
|
199
|
+
!["outline", "destructive", "primary", "secondary", true, false].includes(
|
|
200
|
+
badge,
|
|
201
|
+
) && badge,
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
{record[assignable.unavailableField]
|
|
205
|
+
? record[assignable.unavailableField]
|
|
206
|
+
: "Not available"}
|
|
207
|
+
</Badge>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
unavailable = (
|
|
212
|
+
<Badge variant="outline" className="text-xs text-center">
|
|
213
|
+
Not available
|
|
214
|
+
</Badge>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
112
218
|
return (
|
|
113
219
|
<Card key={record.id}>
|
|
114
220
|
<CardHeader
|
|
@@ -122,7 +228,30 @@ const Row = ({ index, style, data }: RowProps) => {
|
|
|
122
228
|
</button>
|
|
123
229
|
</CardHeader>
|
|
124
230
|
<CardContent className="pb-3 md:pb-4">
|
|
125
|
-
<div className={cn("grid", "gap-
|
|
231
|
+
<div className={cn("grid", "gap-4", size)}>
|
|
232
|
+
{isAssigning && assignable && (assignable.isAvailable(record) || checked) && (
|
|
233
|
+
<div>
|
|
234
|
+
<div className="flex items-center justify-center space-x-3 min-h-8">
|
|
235
|
+
<Switch
|
|
236
|
+
id={`${record.id}-assigned`}
|
|
237
|
+
className="data-[state=checked]:bg-blue-500"
|
|
238
|
+
checked={checked}
|
|
239
|
+
disabled={checkedDisabled}
|
|
240
|
+
onCheckedChange={(checked) => handleCheckedChange(checked, record)}
|
|
241
|
+
/>
|
|
242
|
+
{imagesConfig.size !== "sm" && (
|
|
243
|
+
<Label htmlFor={`${record.id}-assigned`}>Assigned</Label>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
{isAssigning && assignable && !assignable.isAvailable(record) && !checked && (
|
|
249
|
+
<div>
|
|
250
|
+
<div className="flex items-center justify-center space-x-3 min-h-8">
|
|
251
|
+
{unavailable}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
126
255
|
<button
|
|
127
256
|
className="relative w-full h-full flex items-center justify-center overflow-hidden"
|
|
128
257
|
onClick={() => goToRecord(collection, record)}
|
|
@@ -130,7 +259,10 @@ const Row = ({ index, style, data }: RowProps) => {
|
|
|
130
259
|
{record[imagesConfig.imageField] ? (
|
|
131
260
|
<RowImage alt={title} src={record[imagesConfig.imageField]} />
|
|
132
261
|
) : (
|
|
133
|
-
<Image
|
|
262
|
+
<Image
|
|
263
|
+
size={imagesConfig.size === "sm" ? 30 : 100}
|
|
264
|
+
className="text-muted-foreground stroke-1 opacity-50"
|
|
265
|
+
/>
|
|
134
266
|
)}
|
|
135
267
|
</button>
|
|
136
268
|
</div>
|
|
@@ -158,8 +290,12 @@ interface ImagesProps {
|
|
|
158
290
|
unsubscribe: React.MutableRefObject<{ [key: string | number]: (() => void)[] }>
|
|
159
291
|
search: string | undefined
|
|
160
292
|
backToStartKey: number
|
|
161
|
-
relationList?:
|
|
293
|
+
relationList?: RelationList
|
|
294
|
+
relationCollection?: CollectionSchema
|
|
295
|
+
relationParent?: StokerRecord
|
|
162
296
|
formList?: boolean
|
|
297
|
+
isAssigning?: boolean
|
|
298
|
+
assignable?: Assignable
|
|
163
299
|
}
|
|
164
300
|
|
|
165
301
|
export const Images = memo(
|
|
@@ -175,7 +311,11 @@ export const Images = memo(
|
|
|
175
311
|
search,
|
|
176
312
|
backToStartKey,
|
|
177
313
|
relationList,
|
|
314
|
+
relationCollection,
|
|
315
|
+
relationParent,
|
|
178
316
|
formList,
|
|
317
|
+
isAssigning,
|
|
318
|
+
assignable,
|
|
179
319
|
}: ImagesProps) => {
|
|
180
320
|
const { labels, recordTitleField, fullTextSearch } = collection
|
|
181
321
|
const customization = getCollectionConfigModule(labels.collection)
|
|
@@ -582,6 +722,7 @@ export const Images = memo(
|
|
|
582
722
|
|
|
583
723
|
const lineClamp = imagesConfig.maxHeaderLines === 2 ? "line-clamp-2" : "line-clamp-1"
|
|
584
724
|
const headerSize = imagesConfig.maxHeaderLines === 2 ? 116 : 82
|
|
725
|
+
const assignedHeight = isAssigning ? 40 : 0
|
|
585
726
|
|
|
586
727
|
const itemData = {
|
|
587
728
|
collection,
|
|
@@ -591,6 +732,11 @@ export const Images = memo(
|
|
|
591
732
|
lineClamp,
|
|
592
733
|
recordTitleField,
|
|
593
734
|
imagesConfig,
|
|
735
|
+
isAssigning,
|
|
736
|
+
assignable,
|
|
737
|
+
relationList,
|
|
738
|
+
relationCollection,
|
|
739
|
+
relationParent,
|
|
594
740
|
}
|
|
595
741
|
|
|
596
742
|
const Meta = () => (
|
|
@@ -622,7 +768,7 @@ export const Images = memo(
|
|
|
622
768
|
<List
|
|
623
769
|
height={height}
|
|
624
770
|
width="100%"
|
|
625
|
-
itemSize={itemSize + headerSize}
|
|
771
|
+
itemSize={itemSize + headerSize + assignedHeight}
|
|
626
772
|
itemCount={itemCount}
|
|
627
773
|
overscanCount={5}
|
|
628
774
|
itemKey={itemKey}
|
|
@@ -647,7 +793,7 @@ export const Images = memo(
|
|
|
647
793
|
<List
|
|
648
794
|
height={height}
|
|
649
795
|
width="100%"
|
|
650
|
-
itemSize={itemSize + headerSize}
|
|
796
|
+
itemSize={itemSize + headerSize + assignedHeight}
|
|
651
797
|
itemCount={itemCount}
|
|
652
798
|
overscanCount={5}
|
|
653
799
|
itemKey={itemKey}
|
|
@@ -663,6 +809,10 @@ export const Images = memo(
|
|
|
663
809
|
)
|
|
664
810
|
}
|
|
665
811
|
},
|
|
666
|
-
(prevProps, nextProps) =>
|
|
812
|
+
(prevProps, nextProps) =>
|
|
813
|
+
prevProps.list === nextProps.list &&
|
|
814
|
+
prevProps.search === nextProps.search &&
|
|
815
|
+
prevProps.isAssigning === nextProps.isAssigning &&
|
|
816
|
+
prevProps.backToStartKey === nextProps.backToStartKey,
|
|
667
817
|
)
|
|
668
818
|
Images.displayName = "Images"
|
package/src/Record.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Assignable,
|
|
2
3
|
CollectionSchema,
|
|
3
4
|
CustomRecordPage,
|
|
4
5
|
RelationField,
|
|
@@ -79,6 +80,9 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
79
80
|
const [breadcrumbs, setBreadcrumbs] = useState<string[] | undefined>(undefined)
|
|
80
81
|
const [customRecordPages, setCustomRecordPages] = useState<CustomRecordPage[] | undefined>(undefined)
|
|
81
82
|
|
|
83
|
+
const [isAssigning, setIsAssigning] = useState<Record<string, boolean>>({})
|
|
84
|
+
const [assignable, setAssignable] = useState<Assignable[] | undefined>(undefined)
|
|
85
|
+
|
|
82
86
|
useEffect(() => {
|
|
83
87
|
if (id && record && record.id !== id) {
|
|
84
88
|
setRecord(location?.state?.relationField?.includeFields ? undefined : recordFromState)
|
|
@@ -108,6 +112,10 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
108
112
|
| CustomRecordPage[]
|
|
109
113
|
| undefined
|
|
110
114
|
setCustomRecordPages(pages || [])
|
|
115
|
+
const assignable = (await getCachedConfigValue(customization, [...collectionAdminPath, "assignable"])) as
|
|
116
|
+
| Assignable[]
|
|
117
|
+
| undefined
|
|
118
|
+
setAssignable(assignable)
|
|
111
119
|
|
|
112
120
|
setIsRouteLoading("+", location.pathname)
|
|
113
121
|
|
|
@@ -195,7 +203,12 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
195
203
|
{record && (
|
|
196
204
|
<CardContent className="px-0">
|
|
197
205
|
<SidebarProvider defaultOpen={true} open={true} className="flex flex-col lg:flex-row">
|
|
198
|
-
<RecordSidebar
|
|
206
|
+
<RecordSidebar
|
|
207
|
+
collection={collection}
|
|
208
|
+
customRecordPages={customRecordPages}
|
|
209
|
+
isAssigning={isAssigning}
|
|
210
|
+
setIsAssigning={setIsAssigning}
|
|
211
|
+
/>
|
|
199
212
|
<Routes>
|
|
200
213
|
<Route
|
|
201
214
|
path="edit"
|
|
@@ -256,18 +269,13 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
256
269
|
relationList={relationList}
|
|
257
270
|
relationCollection={collection}
|
|
258
271
|
relationParent={record}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
],
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
return []
|
|
270
|
-
})()}
|
|
272
|
+
isAssigning={
|
|
273
|
+
isAssigning?.[relationList.collection.toLowerCase()]
|
|
274
|
+
}
|
|
275
|
+
assignable={assignable?.find(
|
|
276
|
+
(item: Assignable) =>
|
|
277
|
+
item.collection === relationList.collection,
|
|
278
|
+
)}
|
|
271
279
|
/>
|
|
272
280
|
</FiltersProvider>
|
|
273
281
|
</main>
|
package/src/RecordSidebar.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileIcon, EditIcon, List as ListIcon, Book, ArrowDown } from "lucide-react"
|
|
1
|
+
import { FileIcon, EditIcon, List as ListIcon, Book, ArrowDown, Pencil, List } from "lucide-react"
|
|
2
2
|
import {
|
|
3
3
|
Sidebar,
|
|
4
4
|
SidebarContent,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./components/ui/sidebar"
|
|
11
11
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./components/ui/dropdown-menu"
|
|
12
12
|
import { useLocation, useNavigate, useParams } from "react-router"
|
|
13
|
-
import { CollectionPermissions, CollectionSchema, CustomRecordPage } from "@stoker-platform/types"
|
|
13
|
+
import { Assignable, CollectionPermissions, CollectionSchema, CustomRecordPage } from "@stoker-platform/types"
|
|
14
14
|
import { collectionAccess, getField, isRelationField, tryFunction, tryPromise } from "@stoker-platform/utils"
|
|
15
15
|
import { getCurrentUserPermissions, getCollectionConfigModule, getSchema } from "@stoker-platform/web-client"
|
|
16
16
|
import { runViewTransition } from "./utils/runViewTransition"
|
|
@@ -20,14 +20,19 @@ interface SidebarItem {
|
|
|
20
20
|
title: string
|
|
21
21
|
page: string
|
|
22
22
|
icon: React.FC
|
|
23
|
+
assignable?: Assignable
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export const RecordSidebar = ({
|
|
26
27
|
collection,
|
|
27
28
|
customRecordPages,
|
|
29
|
+
isAssigning,
|
|
30
|
+
setIsAssigning,
|
|
28
31
|
}: {
|
|
29
32
|
collection: CollectionSchema
|
|
30
33
|
customRecordPages?: CustomRecordPage[]
|
|
34
|
+
isAssigning: Record<string, boolean>
|
|
35
|
+
setIsAssigning: (isAssigning: Record<string, boolean>) => void
|
|
31
36
|
}) => {
|
|
32
37
|
const { labels } = collection
|
|
33
38
|
const { path, id } = useParams()
|
|
@@ -35,8 +40,11 @@ export const RecordSidebar = ({
|
|
|
35
40
|
const location = useLocation()
|
|
36
41
|
|
|
37
42
|
const schema = getSchema()
|
|
43
|
+
const customization = getCollectionConfigModule(collection.labels.collection)
|
|
38
44
|
const permissions = getCurrentUserPermissions()
|
|
39
45
|
const [relationTitles, setRelationTitles] = useState<Record<string, string>>({})
|
|
46
|
+
const [relationIcons, setRelationIcons] = useState<Record<string, React.FC>>({})
|
|
47
|
+
const [assignable, setAssignable] = useState<Assignable[]>([])
|
|
40
48
|
|
|
41
49
|
useEffect(() => {
|
|
42
50
|
;(async () => {
|
|
@@ -51,7 +59,14 @@ export const RecordSidebar = ({
|
|
|
51
59
|
...prev,
|
|
52
60
|
[relationList.collection]: title || relationList.collection,
|
|
53
61
|
}))
|
|
62
|
+
const icon = await tryPromise(relationCustomization.admin?.icon)
|
|
63
|
+
setRelationIcons((prev) => ({
|
|
64
|
+
...prev,
|
|
65
|
+
[relationList.collection]: icon as React.FC,
|
|
66
|
+
}))
|
|
54
67
|
})
|
|
68
|
+
const assignable = await tryPromise(customization.admin?.assignable)
|
|
69
|
+
setAssignable(assignable)
|
|
55
70
|
}
|
|
56
71
|
})()
|
|
57
72
|
}, [])
|
|
@@ -95,7 +110,8 @@ export const RecordSidebar = ({
|
|
|
95
110
|
relationItems.push({
|
|
96
111
|
title: relationTitles[relationList.collection],
|
|
97
112
|
page: relationList.collection.toLowerCase(),
|
|
98
|
-
icon:
|
|
113
|
+
icon: relationIcons[relationList.collection] || (() => null),
|
|
114
|
+
assignable: assignable?.find((item) => item.collection === relationList.collection),
|
|
99
115
|
})
|
|
100
116
|
})
|
|
101
117
|
}
|
|
@@ -126,13 +142,38 @@ export const RecordSidebar = ({
|
|
|
126
142
|
return (
|
|
127
143
|
<SidebarMenuItem key={item.page}>
|
|
128
144
|
<SidebarMenuButton asChild onClick={() => goToRecordPage(item.page)}>
|
|
129
|
-
<
|
|
130
|
-
className={isActive ? "bg-sidebar-accent" : "cursor-pointer"}
|
|
131
|
-
type="button"
|
|
132
|
-
>
|
|
145
|
+
<div className={isActive ? "bg-sidebar-accent" : "cursor-pointer"}>
|
|
133
146
|
<item.icon />
|
|
134
|
-
<
|
|
135
|
-
|
|
147
|
+
<button type="button">{item.title}</button>
|
|
148
|
+
{item.assignable && isActive && !isAssigning?.[item.page] && (
|
|
149
|
+
<button
|
|
150
|
+
className="ml-auto"
|
|
151
|
+
onClick={() =>
|
|
152
|
+
setIsAssigning({
|
|
153
|
+
...isAssigning,
|
|
154
|
+
[item.page]: true,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
type="button"
|
|
158
|
+
>
|
|
159
|
+
<Pencil className="w-4 h-4" />
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
{item.assignable && isActive && isAssigning?.[item.page] && (
|
|
163
|
+
<button
|
|
164
|
+
className="ml-auto"
|
|
165
|
+
onClick={() =>
|
|
166
|
+
setIsAssigning({
|
|
167
|
+
...isAssigning,
|
|
168
|
+
[item.page]: false,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
type="button"
|
|
172
|
+
>
|
|
173
|
+
<List className="w-4 h-4" />
|
|
174
|
+
</button>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
136
177
|
</SidebarMenuButton>
|
|
137
178
|
</SidebarMenuItem>
|
|
138
179
|
)
|
|
@@ -174,6 +215,35 @@ export const RecordSidebar = ({
|
|
|
174
215
|
onClick={() => goToRecordPage(item.page)}
|
|
175
216
|
>
|
|
176
217
|
{item.title}
|
|
218
|
+
|
|
219
|
+
{item.assignable && !isAssigning?.[item.page] && (
|
|
220
|
+
<button
|
|
221
|
+
className="ml-auto"
|
|
222
|
+
onClick={() =>
|
|
223
|
+
setIsAssigning({
|
|
224
|
+
...isAssigning,
|
|
225
|
+
[item.page]: true,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
type="button"
|
|
229
|
+
>
|
|
230
|
+
<Pencil className="w-4 h-4" />
|
|
231
|
+
</button>
|
|
232
|
+
)}
|
|
233
|
+
{item.assignable && isAssigning?.[item.page] && (
|
|
234
|
+
<button
|
|
235
|
+
className="ml-auto"
|
|
236
|
+
onClick={() =>
|
|
237
|
+
setIsAssigning({
|
|
238
|
+
...isAssigning,
|
|
239
|
+
[item.page]: false,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
type="button"
|
|
243
|
+
>
|
|
244
|
+
<List className="w-4 h-4" />
|
|
245
|
+
</button>
|
|
246
|
+
)}
|
|
177
247
|
</DropdownMenuItem>
|
|
178
248
|
))}
|
|
179
249
|
</DropdownMenuContent>
|