@stoker-platform/web-app 0.5.47 → 0.5.49
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 +18 -0
- package/package.json +4 -4
- package/src/Collection.tsx +93 -11
- package/src/Filters.tsx +10 -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,23 @@
|
|
|
1
1
|
# @stoker-platform/web-app
|
|
2
2
|
|
|
3
|
+
## 0.5.49
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat: add relation record assignment feature
|
|
8
|
+
- @stoker-platform/node-client@0.5.32
|
|
9
|
+
- @stoker-platform/utils@0.5.26
|
|
10
|
+
- @stoker-platform/web-client@0.5.28
|
|
11
|
+
|
|
12
|
+
## 0.5.48
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- feat: add parent relation args to admin options
|
|
17
|
+
- @stoker-platform/node-client@0.5.31
|
|
18
|
+
- @stoker-platform/utils@0.5.25
|
|
19
|
+
- @stoker-platform/web-client@0.5.27
|
|
20
|
+
|
|
3
21
|
## 0.5.47
|
|
4
22
|
|
|
5
23
|
### 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.49",
|
|
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
|
)
|
|
@@ -698,7 +735,7 @@ function Collection({
|
|
|
698
735
|
const statusFilterState = state[`collection-status-filter-${labels.collection.toLowerCase()}`]
|
|
699
736
|
const cacheState = state[`collection-range-field-${labels.collection.toLowerCase()}`]
|
|
700
737
|
const rangeState = state[`collection-range-${labels.collection.toLowerCase()}`]
|
|
701
|
-
const defaultView = tryFunction(customization.admin?.defaultView)
|
|
738
|
+
const defaultView = tryFunction(customization.admin?.defaultView, [relationCollection, relationParent])
|
|
702
739
|
if (!relationList) {
|
|
703
740
|
if (tabState) {
|
|
704
741
|
setTab(tabState)
|
|
@@ -734,8 +771,14 @@ function Collection({
|
|
|
734
771
|
setState(`collection-range-${labels.collection.toLowerCase()}`, "range", rangeState)
|
|
735
772
|
}
|
|
736
773
|
} else {
|
|
737
|
-
|
|
738
|
-
|
|
774
|
+
if (defaultView) {
|
|
775
|
+
setTab(defaultView)
|
|
776
|
+
tabRef.current = defaultView
|
|
777
|
+
setState(`collection-tab-${labels.collection.toLowerCase()}`, "tab", defaultView)
|
|
778
|
+
} else {
|
|
779
|
+
setTab("list")
|
|
780
|
+
tabRef.current = "list"
|
|
781
|
+
}
|
|
739
782
|
}
|
|
740
783
|
if (rangeSelectorState) {
|
|
741
784
|
setRangeSelector(rangeSelectorState as "range" | "week" | "month" | undefined)
|
|
@@ -861,6 +904,36 @@ function Collection({
|
|
|
861
904
|
}
|
|
862
905
|
})
|
|
863
906
|
}
|
|
907
|
+
} else {
|
|
908
|
+
filtersClone.forEach((filter: Filter) => {
|
|
909
|
+
if (filter.type === "status" || filter.type === "range") {
|
|
910
|
+
return
|
|
911
|
+
}
|
|
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
|
+
])
|
|
926
|
+
}
|
|
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
|
+
}
|
|
864
937
|
}
|
|
865
938
|
|
|
866
939
|
if (statusField || softDelete) {
|
|
@@ -1043,7 +1116,10 @@ function Collection({
|
|
|
1043
1116
|
setOrder({ field: recordTitleField, direction: "asc" })
|
|
1044
1117
|
}
|
|
1045
1118
|
|
|
1046
|
-
|
|
1119
|
+
if (!relationList) {
|
|
1120
|
+
hasFirstPageLoaded[labels.collection] = true
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1047
1123
|
setIsInitialized(true)
|
|
1048
1124
|
}
|
|
1049
1125
|
|
|
@@ -1115,17 +1191,19 @@ function Collection({
|
|
|
1115
1191
|
return (
|
|
1116
1192
|
filters
|
|
1117
1193
|
.filter((filter) => filter.type !== "status" && filter.type !== "range")
|
|
1194
|
+
.filter((filter) => !relationList || filter.field !== relationList.field)
|
|
1118
1195
|
.filter(
|
|
1119
1196
|
(filter) =>
|
|
1120
1197
|
(filter.value ||
|
|
1121
1198
|
(filter.type === "select" &&
|
|
1122
1199
|
filter.defaultValue &&
|
|
1123
|
-
tryFunction(filter.defaultValue) &&
|
|
1200
|
+
tryFunction(filter.defaultValue, [relationCollection, relationParent, isAssigning]) &&
|
|
1124
1201
|
!filter.value)) &&
|
|
1125
1202
|
!(
|
|
1126
1203
|
filter.type === "select" &&
|
|
1127
1204
|
filter.defaultValue &&
|
|
1128
|
-
tryFunction(filter.defaultValue) ===
|
|
1205
|
+
tryFunction(filter.defaultValue, [relationCollection, relationParent, isAssigning]) ===
|
|
1206
|
+
filter.value
|
|
1129
1207
|
),
|
|
1130
1208
|
)
|
|
1131
1209
|
.filter((filter) => !excludedFilters.includes(filter.field)).length > 0
|
|
@@ -1941,7 +2019,7 @@ function Collection({
|
|
|
1941
2019
|
<Filters
|
|
1942
2020
|
collection={collection}
|
|
1943
2021
|
excluded={excludedFilters}
|
|
1944
|
-
relationList={
|
|
2022
|
+
relationList={relationList}
|
|
1945
2023
|
/>
|
|
1946
2024
|
</SheetContent>
|
|
1947
2025
|
</Sheet>
|
|
@@ -2274,8 +2352,12 @@ function Collection({
|
|
|
2274
2352
|
unsubscribe={unsubscribe}
|
|
2275
2353
|
search={search}
|
|
2276
2354
|
backToStartKey={backToStartKey}
|
|
2277
|
-
relationList={
|
|
2355
|
+
relationList={relationList}
|
|
2356
|
+
relationCollection={relationCollection}
|
|
2357
|
+
relationParent={relationParent}
|
|
2278
2358
|
formList={!!formList}
|
|
2359
|
+
isAssigning={isAssigning}
|
|
2360
|
+
assignable={assignable}
|
|
2279
2361
|
/>
|
|
2280
2362
|
</TabsContent>
|
|
2281
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
|
|
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>
|