@stoker-platform/web-app 0.5.217 → 0.5.219
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 +16 -0
- package/package.json +4 -4
- package/src/Calendar.tsx +387 -54
- package/src/Collection.tsx +260 -16
- package/src/Form.tsx +10 -4
- package/src/Record.tsx +3 -0
- package/src/utils/goToRecord.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @stoker-platform/web-app
|
|
2
2
|
|
|
3
|
+
## 0.5.219
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @stoker-platform/web-client@0.5.90
|
|
9
|
+
|
|
10
|
+
## 0.5.218
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- feat: add Calendar additional collections
|
|
15
|
+
- @stoker-platform/node-client@0.5.85
|
|
16
|
+
- @stoker-platform/utils@0.5.76
|
|
17
|
+
- @stoker-platform/web-client@0.5.89
|
|
18
|
+
|
|
3
19
|
## 0.5.217
|
|
4
20
|
|
|
5
21
|
### 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.219",
|
|
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.85",
|
|
55
|
+
"@stoker-platform/utils": "0.5.76",
|
|
56
|
+
"@stoker-platform/web-client": "0.5.90",
|
|
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/Calendar.tsx
CHANGED
|
@@ -93,6 +93,49 @@ function getInclusiveEnd(exclusiveEnd: Date, timezone: string): Date {
|
|
|
93
93
|
return DateTime.fromJSDate(exclusiveEnd).setZone(timezone).startOf("day").minus({ days: 1 }).toJSDate()
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
const calendarConfigOverrideKeys: (keyof CalendarConfig)[] = [
|
|
97
|
+
"dataStart",
|
|
98
|
+
"dataEnd",
|
|
99
|
+
"dataStartOffset",
|
|
100
|
+
"dataEndOffset",
|
|
101
|
+
"fullCalendarLarge",
|
|
102
|
+
"fullCalendarSmall",
|
|
103
|
+
"roles",
|
|
104
|
+
"title",
|
|
105
|
+
"unscheduled",
|
|
106
|
+
"additionalCollections",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
export function mergeCalendarConfig(main: CalendarConfig, additional: CalendarConfig): CalendarConfig {
|
|
110
|
+
const merged = { ...additional }
|
|
111
|
+
for (const key of calendarConfigOverrideKeys) {
|
|
112
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
113
|
+
if (main[key] !== undefined) {
|
|
114
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
115
|
+
;(merged as Record<keyof CalendarConfig, unknown>)[key] = main[key]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return merged
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function applyOptimisticUpdates(
|
|
122
|
+
records: StokerRecord[],
|
|
123
|
+
collectionOptimisticUpdates: StokerRecord[] | undefined,
|
|
124
|
+
): StokerRecord[] {
|
|
125
|
+
if (!collectionOptimisticUpdates?.length) return records
|
|
126
|
+
const updatedRecords = cloneDeep(records)
|
|
127
|
+
collectionOptimisticUpdates.forEach((optimisticRecord) => {
|
|
128
|
+
const index = updatedRecords.findIndex((record) => record.id === optimisticRecord.id)
|
|
129
|
+
if (index !== -1) {
|
|
130
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
131
|
+
updatedRecords[index] = optimisticRecord
|
|
132
|
+
} else {
|
|
133
|
+
updatedRecords.push(optimisticRecord)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
return updatedRecords
|
|
137
|
+
}
|
|
138
|
+
|
|
96
139
|
function isAllDayEvent(record: StokerRecord, calendarConfig: CalendarConfig): boolean {
|
|
97
140
|
if (calendarConfig.allDayField !== undefined) {
|
|
98
141
|
return !!record[calendarConfig.allDayField]
|
|
@@ -137,11 +180,11 @@ function Row({
|
|
|
137
180
|
ref={drag}
|
|
138
181
|
tabIndex={0}
|
|
139
182
|
onClick={() => {
|
|
140
|
-
goToRecord(collection, record)
|
|
183
|
+
goToRecord(collection, record, undefined, true)
|
|
141
184
|
}}
|
|
142
185
|
onKeyDown={(event) => {
|
|
143
186
|
if (event.key === "Enter" || event.key === " ") {
|
|
144
|
-
goToRecord(collection, record)
|
|
187
|
+
goToRecord(collection, record, undefined, true)
|
|
145
188
|
}
|
|
146
189
|
}}
|
|
147
190
|
className={className}
|
|
@@ -169,6 +212,18 @@ interface CalendarProps {
|
|
|
169
212
|
unsubscribe: React.MutableRefObject<{ [key: string | number]: ((direction?: "first" | "last") => void)[] }>
|
|
170
213
|
setOptimisticList: () => void
|
|
171
214
|
canAddRecords: boolean
|
|
215
|
+
disableCreate: boolean
|
|
216
|
+
creatableCalendarCollections: StokerCollection[]
|
|
217
|
+
additionalOfflineUpdateDisabled: Record<StokerCollection, boolean> | undefined
|
|
218
|
+
additionalTitles:
|
|
219
|
+
| Record<
|
|
220
|
+
StokerCollection,
|
|
221
|
+
{
|
|
222
|
+
collection: string
|
|
223
|
+
record: string
|
|
224
|
+
}
|
|
225
|
+
>
|
|
226
|
+
| undefined
|
|
172
227
|
onDateSelection?: (dateSelectionData: { startDate: Date; endDate?: Date }) => void
|
|
173
228
|
backToStartKey: number
|
|
174
229
|
relationList?: boolean
|
|
@@ -185,6 +240,10 @@ export function Calendar({
|
|
|
185
240
|
unsubscribe,
|
|
186
241
|
setOptimisticList,
|
|
187
242
|
canAddRecords,
|
|
243
|
+
disableCreate,
|
|
244
|
+
creatableCalendarCollections,
|
|
245
|
+
additionalOfflineUpdateDisabled,
|
|
246
|
+
additionalTitles,
|
|
188
247
|
onDateSelection,
|
|
189
248
|
backToStartKey,
|
|
190
249
|
relationList,
|
|
@@ -222,10 +281,12 @@ export function Calendar({
|
|
|
222
281
|
const { optimisticUpdates, removeOptimisticUpdate, setOptimisticUpdate, removeCacheOptimistic } = useOptimistic()
|
|
223
282
|
const { isGlobalLoading, setGlobalLoading } = useGlobalLoading()
|
|
224
283
|
const [isInitialized, setIsInitialized] = useState(false)
|
|
284
|
+
const [mainCollectionLoaded, setMainCollectionLoaded] = useState(false)
|
|
285
|
+
const [additionalConfigLoaded, setAdditionalConfigLoaded] = useState(false)
|
|
225
286
|
|
|
226
287
|
const [hasStartUpdateAccess, setHasStartUpdateAccess] = useState<boolean>(false)
|
|
227
288
|
const [hasEndUpdateAccess, setHasEndUpdateAccess] = useState<boolean>(false)
|
|
228
|
-
const [
|
|
289
|
+
const [hasResourceUpdateAccess, setHasResourceUpdateAccess] = useState<boolean>(false)
|
|
229
290
|
|
|
230
291
|
const [currentViewLarge, setCurrentViewLarge] = useState<string | undefined>(undefined)
|
|
231
292
|
const [currentViewSmall, setCurrentViewSmall] = useState<string | undefined>(undefined)
|
|
@@ -234,6 +295,10 @@ export function Calendar({
|
|
|
234
295
|
const [resources, setResources] = useState<Set<{ id: string; title: string; Collection_Path?: string }>>(new Set())
|
|
235
296
|
const [unscheduledRecords, setUnscheduledRecords] = useState<StokerRecord[]>([])
|
|
236
297
|
const [unscheduledLoading, setUnscheduledLoading] = useState<boolean>(true)
|
|
298
|
+
const [additionalLists, setAdditionalLists] = useState<Record<string, StokerRecord[]>>({})
|
|
299
|
+
const [additionalConfig, setAdditionalConfig] = useState<
|
|
300
|
+
Record<string, { config: CalendarConfig; recordTitleField: string }>
|
|
301
|
+
>({})
|
|
237
302
|
|
|
238
303
|
const { filters, getFilterConstraints } = useFilters()
|
|
239
304
|
const [rangeFilter, setRangeFilter] = useState<RangeFilter | undefined>(undefined)
|
|
@@ -465,8 +530,8 @@ export function Calendar({
|
|
|
465
530
|
setHasEndUpdateAccess(hasEndUpdateAccess && (!allDayField || hasAllDayUpdateAccess))
|
|
466
531
|
}
|
|
467
532
|
if (resourceField) {
|
|
468
|
-
const
|
|
469
|
-
|
|
533
|
+
const hasResourceUpdateAccess = !!canUpdateField(collection, resourceFieldSchema, permissions)
|
|
534
|
+
setHasResourceUpdateAccess(hasResourceUpdateAccess)
|
|
470
535
|
}
|
|
471
536
|
|
|
472
537
|
setList({})
|
|
@@ -493,12 +558,21 @@ export function Calendar({
|
|
|
493
558
|
},
|
|
494
559
|
],
|
|
495
560
|
}).then(() => {
|
|
496
|
-
|
|
561
|
+
setMainCollectionLoaded(true)
|
|
497
562
|
})
|
|
498
563
|
}
|
|
499
564
|
initialize()
|
|
500
565
|
}, [])
|
|
501
566
|
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
if (!mainCollectionLoaded || !additionalConfigLoaded) return
|
|
569
|
+
if (list === undefined) return
|
|
570
|
+
const expectedAdditional = Object.keys(additionalConfig)
|
|
571
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
572
|
+
if (expectedAdditional.some((collectionName) => additionalLists[collectionName] === undefined)) return
|
|
573
|
+
setIsInitialized(true)
|
|
574
|
+
}, [mainCollectionLoaded, additionalConfigLoaded, list, additionalLists, additionalConfig])
|
|
575
|
+
|
|
502
576
|
useEffect(() => {
|
|
503
577
|
let unscheduledListener: (() => void) | undefined
|
|
504
578
|
if (!permissions?.Role) throw new Error("PERMISSION_DENIED")
|
|
@@ -550,6 +624,85 @@ export function Calendar({
|
|
|
550
624
|
}
|
|
551
625
|
}, [calendarConfig, filters])
|
|
552
626
|
|
|
627
|
+
useEffect(() => {
|
|
628
|
+
if (!calendarConfig || !permissions?.Role) return
|
|
629
|
+
|
|
630
|
+
if (relationList || !calendarConfig.additionalCollections?.length) {
|
|
631
|
+
setAdditionalConfigLoaded(true)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let cancelled = false
|
|
636
|
+
const listeners: (() => void)[] = []
|
|
637
|
+
|
|
638
|
+
const subscribeToAdditional = async () => {
|
|
639
|
+
const collectionsConfig: Record<string, { config: CalendarConfig; recordTitleField: string }> = {}
|
|
640
|
+
|
|
641
|
+
for (const collectionName of calendarConfig.additionalCollections || []) {
|
|
642
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
643
|
+
const additionalSchema = schema.collections[collectionName]
|
|
644
|
+
const isPreloadCacheEnabledAdditional = preloadCacheEnabled(additionalSchema)
|
|
645
|
+
if (!additionalSchema || !isPreloadCacheEnabledAdditional) continue
|
|
646
|
+
|
|
647
|
+
const additionalCustomization = getCollectionConfigModule(collectionName)
|
|
648
|
+
const additionalConfig = (await getCachedConfigValue(additionalCustomization, [
|
|
649
|
+
"collections",
|
|
650
|
+
collectionName,
|
|
651
|
+
"admin",
|
|
652
|
+
"calendar",
|
|
653
|
+
])) as CalendarConfig | undefined
|
|
654
|
+
if (!additionalConfig) continue
|
|
655
|
+
|
|
656
|
+
const mergedConfig = mergeCalendarConfig(calendarConfig, additionalConfig)
|
|
657
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
658
|
+
collectionsConfig[collectionName] = {
|
|
659
|
+
config: mergedConfig,
|
|
660
|
+
recordTitleField: additionalSchema.recordTitleField,
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const rangeConstraints: QueryConstraint[] = [
|
|
664
|
+
where(mergedConfig.startField, ">=", Timestamp.fromDate(getMinDate())),
|
|
665
|
+
where(mergedConfig.startField, "<=", Timestamp.fromDate(getMaxDate())),
|
|
666
|
+
]
|
|
667
|
+
|
|
668
|
+
if (additionalSchema.softDelete) {
|
|
669
|
+
rangeConstraints.push(where(additionalSchema.softDelete.archivedField, "==", false))
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const result = await subscribeMany(
|
|
673
|
+
[collectionName],
|
|
674
|
+
(docs) => {
|
|
675
|
+
setAdditionalLists((prev) => ({ ...prev, [collectionName]: docs }))
|
|
676
|
+
},
|
|
677
|
+
(error) => {
|
|
678
|
+
console.error(error)
|
|
679
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
680
|
+
setAdditionalLists((prev) => ({ ...prev, [collectionName]: prev[collectionName] || [] }))
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
constraints: rangeConstraints,
|
|
684
|
+
},
|
|
685
|
+
)
|
|
686
|
+
if (cancelled) {
|
|
687
|
+
result.unsubscribe()
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
listeners.push(result.unsubscribe)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (cancelled) return
|
|
694
|
+
setAdditionalConfig(collectionsConfig)
|
|
695
|
+
setAdditionalConfigLoaded(true)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
subscribeToAdditional()
|
|
699
|
+
|
|
700
|
+
return () => {
|
|
701
|
+
cancelled = true
|
|
702
|
+
listeners.forEach((unsubscribe) => unsubscribe())
|
|
703
|
+
}
|
|
704
|
+
}, [calendarConfig, rangeFilter, isPreloadCacheEnabled, permissions?.Role])
|
|
705
|
+
|
|
553
706
|
const plugins = useMemo(
|
|
554
707
|
() => [
|
|
555
708
|
interactionPlugin,
|
|
@@ -638,17 +791,7 @@ export function Calendar({
|
|
|
638
791
|
|
|
639
792
|
const events: EventInput[] = useMemo(() => {
|
|
640
793
|
if (!calendarConfig || !recordTitleField || !permissions || !list) return []
|
|
641
|
-
const
|
|
642
|
-
const updatedList = cloneDeep(list)
|
|
643
|
-
collectionOptimisticUpdates?.forEach((optimisticRecord) => {
|
|
644
|
-
const index = updatedList.findIndex((record) => record.id === optimisticRecord.id)
|
|
645
|
-
if (index !== -1) {
|
|
646
|
-
// eslint-disable-next-line security/detect-object-injection
|
|
647
|
-
updatedList[index] = optimisticRecord
|
|
648
|
-
} else {
|
|
649
|
-
updatedList.push(optimisticRecord)
|
|
650
|
-
}
|
|
651
|
-
})
|
|
794
|
+
const updatedList = applyOptimisticUpdates(list, optimisticUpdates?.get(labels.collection))
|
|
652
795
|
const mainEvents = updatedList
|
|
653
796
|
.filter(
|
|
654
797
|
(record) =>
|
|
@@ -667,7 +810,7 @@ export function Calendar({
|
|
|
667
810
|
start: record[calendarConfig.startField].toDate(),
|
|
668
811
|
startEditable: !isPendingServer && !isUpdateDisabled && hasStartUpdateAccess,
|
|
669
812
|
durationEditable: !isPendingServer && !isUpdateDisabled && hasEndUpdateAccess,
|
|
670
|
-
resourceEditable: !isPendingServer && !isUpdateDisabled &&
|
|
813
|
+
resourceEditable: !isPendingServer && !isUpdateDisabled && hasResourceUpdateAccess,
|
|
671
814
|
}
|
|
672
815
|
if (calendarConfig.endField && record[calendarConfig.endField]) {
|
|
673
816
|
const rawEnd = record[calendarConfig.endField].toDate()
|
|
@@ -695,6 +838,10 @@ export function Calendar({
|
|
|
695
838
|
if (color) {
|
|
696
839
|
event.color = color
|
|
697
840
|
}
|
|
841
|
+
event.extendedProps = {
|
|
842
|
+
collection: labels.collection,
|
|
843
|
+
recordId: record.id,
|
|
844
|
+
}
|
|
698
845
|
return event
|
|
699
846
|
})
|
|
700
847
|
|
|
@@ -724,11 +871,101 @@ export function Calendar({
|
|
|
724
871
|
editable: false,
|
|
725
872
|
allDay: true,
|
|
726
873
|
color: "#6b7280",
|
|
874
|
+
extendedProps: {
|
|
875
|
+
collection: labels.collection,
|
|
876
|
+
recordId: record.id,
|
|
877
|
+
},
|
|
727
878
|
}
|
|
728
879
|
additionalEvents.push(event)
|
|
729
880
|
})
|
|
730
881
|
})
|
|
731
882
|
}
|
|
883
|
+
|
|
884
|
+
Object.entries(additionalConfig).forEach(([collectionName, collectionData]) => {
|
|
885
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
886
|
+
const additionalSchema = schema.collections[collectionName]
|
|
887
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
888
|
+
const additionalList = additionalLists[collectionName]
|
|
889
|
+
const isPreloadCacheEnabledAdditional = preloadCacheEnabled(additionalSchema)
|
|
890
|
+
if (relationList || !additionalSchema || !additionalList || !isPreloadCacheEnabledAdditional) return
|
|
891
|
+
|
|
892
|
+
const { config: mergedConfig, recordTitleField: additionalRecordTitleField } = collectionData
|
|
893
|
+
const updatedAdditionalList = applyOptimisticUpdates(additionalList, optimisticUpdates?.get(collectionName))
|
|
894
|
+
|
|
895
|
+
updatedAdditionalList
|
|
896
|
+
.filter(
|
|
897
|
+
(record) =>
|
|
898
|
+
record[mergedConfig.startField] &&
|
|
899
|
+
(!mergedConfig.filterRecords || mergedConfig.filterRecords(record)),
|
|
900
|
+
)
|
|
901
|
+
.forEach((record) => {
|
|
902
|
+
const title =
|
|
903
|
+
tryFunction(mergedConfig.eventTitle, [record]) ||
|
|
904
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
905
|
+
record[additionalRecordTitleField] ||
|
|
906
|
+
record.id
|
|
907
|
+
|
|
908
|
+
const isPendingServerAdditional = isGlobalLoading.get(record.id)?.server
|
|
909
|
+
const isUpdateDisabledAdditional =
|
|
910
|
+
connectionStatus === "offline" &&
|
|
911
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
912
|
+
(additionalOfflineUpdateDisabled?.[collectionName] || serverWriteOnly)
|
|
913
|
+
|
|
914
|
+
const startField = collectionData.config.startField
|
|
915
|
+
const startFieldSchema = getField(additionalSchema.fields, startField)
|
|
916
|
+
const endField = collectionData.config?.endField
|
|
917
|
+
const endFieldSchema = getField(additionalSchema.fields, endField)
|
|
918
|
+
const allDayField = collectionData.config?.allDayField
|
|
919
|
+
const allDayFieldSchema = getField(additionalSchema.fields, allDayField)
|
|
920
|
+
const systemFields = getSystemFieldsSchema()
|
|
921
|
+
|
|
922
|
+
const hasAllDayUpdateAccess =
|
|
923
|
+
allDayFieldSchema && !!canUpdateField(additionalSchema, allDayFieldSchema, permissions)
|
|
924
|
+
const hasStartUpdateAccessAdditional =
|
|
925
|
+
!!(
|
|
926
|
+
canUpdateField(additionalSchema, startFieldSchema, permissions) &&
|
|
927
|
+
!systemFields.map((field) => field.name).includes(startField)
|
|
928
|
+
) &&
|
|
929
|
+
(!allDayField || hasAllDayUpdateAccess)
|
|
930
|
+
let hasEndUpdateAccessAdditional = false
|
|
931
|
+
if (endField) {
|
|
932
|
+
hasEndUpdateAccessAdditional = !!(
|
|
933
|
+
canUpdateField(additionalSchema, endFieldSchema, permissions) &&
|
|
934
|
+
!systemFields.map((field) => field.name).includes(endField) &&
|
|
935
|
+
(!allDayField || hasAllDayUpdateAccess)
|
|
936
|
+
)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const event: EventInput = {
|
|
940
|
+
id: `${collectionName}:${record.id}`,
|
|
941
|
+
title,
|
|
942
|
+
start: record[mergedConfig.startField].toDate(),
|
|
943
|
+
startEditable:
|
|
944
|
+
!isPendingServerAdditional && !isUpdateDisabledAdditional && hasStartUpdateAccessAdditional,
|
|
945
|
+
durationEditable:
|
|
946
|
+
!isPendingServerAdditional && !isUpdateDisabledAdditional && hasEndUpdateAccessAdditional,
|
|
947
|
+
extendedProps: {
|
|
948
|
+
collection: collectionName,
|
|
949
|
+
recordId: record.id,
|
|
950
|
+
},
|
|
951
|
+
}
|
|
952
|
+
if (mergedConfig.endField && record[mergedConfig.endField]) {
|
|
953
|
+
const rawEnd = record[mergedConfig.endField].toDate()
|
|
954
|
+
event.end = isAllDayEvent(record, mergedConfig) ? getExclusiveEnd(rawEnd, timezone) : rawEnd
|
|
955
|
+
}
|
|
956
|
+
if (mergedConfig.allDayField !== undefined) {
|
|
957
|
+
event.allDay = record[mergedConfig.allDayField]
|
|
958
|
+
} else if (isAllDayEvent(record, mergedConfig)) {
|
|
959
|
+
event.allDay = true
|
|
960
|
+
}
|
|
961
|
+
const color = tryFunction(mergedConfig.color, [record])
|
|
962
|
+
if (color) {
|
|
963
|
+
event.color = color
|
|
964
|
+
}
|
|
965
|
+
additionalEvents.push(event)
|
|
966
|
+
})
|
|
967
|
+
})
|
|
968
|
+
|
|
732
969
|
return mainEvents.concat(additionalEvents)
|
|
733
970
|
}, [
|
|
734
971
|
calendarConfig,
|
|
@@ -738,26 +975,59 @@ export function Calendar({
|
|
|
738
975
|
serverWriteOnly,
|
|
739
976
|
hasStartUpdateAccess,
|
|
740
977
|
hasEndUpdateAccess,
|
|
741
|
-
|
|
978
|
+
hasResourceUpdateAccess,
|
|
742
979
|
isGlobalLoading,
|
|
743
980
|
timezone,
|
|
981
|
+
additionalLists,
|
|
982
|
+
additionalConfig,
|
|
983
|
+
optimisticUpdates,
|
|
984
|
+
additionalOfflineUpdateDisabled,
|
|
985
|
+
connectionStatus,
|
|
744
986
|
])
|
|
745
987
|
|
|
746
988
|
const updateEvent = useCallback(
|
|
747
989
|
async (info: EventDropArg | EventResizeDoneArg | EventReceiveArg) => {
|
|
748
990
|
if (!calendarConfig) return
|
|
749
|
-
const record = list
|
|
750
|
-
?.concat(unscheduledRecords)
|
|
751
|
-
?.find((record) => record.id === info.event.id) as StokerRecord
|
|
752
991
|
|
|
992
|
+
const eventCollectionName = (info.event.extendedProps.collection as string | undefined) || labels.collection
|
|
993
|
+
const recordId = (info.event.extendedProps.recordId as string | undefined) || info.event.id
|
|
994
|
+
const isAdditionalCollection = eventCollectionName !== labels.collection
|
|
995
|
+
|
|
996
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
997
|
+
const eventCollectionSchema = isAdditionalCollection ? schema.collections[eventCollectionName] : collection
|
|
998
|
+
if (!eventCollectionSchema) return
|
|
999
|
+
|
|
1000
|
+
const eventCalendarConfig = isAdditionalCollection
|
|
1001
|
+
? // eslint-disable-next-line security/detect-object-injection
|
|
1002
|
+
additionalConfig[eventCollectionName]?.config
|
|
1003
|
+
: calendarConfig
|
|
1004
|
+
if (!eventCalendarConfig) return
|
|
1005
|
+
|
|
1006
|
+
const eventRecordTitleField = isAdditionalCollection
|
|
1007
|
+
? // eslint-disable-next-line security/detect-object-injection
|
|
1008
|
+
additionalConfig[eventCollectionName]?.recordTitleField
|
|
1009
|
+
: recordTitleField
|
|
1010
|
+
const eventRecordTitle = isAdditionalCollection
|
|
1011
|
+
? // eslint-disable-next-line security/detect-object-injection
|
|
1012
|
+
additionalTitles?.[eventCollectionName]?.record || eventCollectionSchema.labels.record
|
|
1013
|
+
: recordTitle
|
|
1014
|
+
|
|
1015
|
+
const eventList = isAdditionalCollection
|
|
1016
|
+
? // eslint-disable-next-line security/detect-object-injection
|
|
1017
|
+
additionalLists[eventCollectionName]
|
|
1018
|
+
: list?.concat(unscheduledRecords)
|
|
1019
|
+
const record = eventList?.find((record) => record.id === recordId) as StokerRecord | undefined
|
|
1020
|
+
if (!record) return
|
|
1021
|
+
|
|
1022
|
+
const originalRecord = cloneDeep(record)
|
|
753
1023
|
const updatedFields: Partial<StokerRecord> = {}
|
|
754
|
-
if (
|
|
1024
|
+
if (eventCalendarConfig.startField && info.event.start) {
|
|
755
1025
|
const startDate = info.event.allDay
|
|
756
1026
|
? DateTime.fromJSDate(info.event.start).setZone(timezone).startOf("day").toJSDate()
|
|
757
1027
|
: info.event.start
|
|
758
|
-
updatedFields[
|
|
1028
|
+
updatedFields[eventCalendarConfig.startField] = Timestamp.fromDate(startDate)
|
|
759
1029
|
}
|
|
760
|
-
if (
|
|
1030
|
+
if (eventCalendarConfig.endField && info.event.start) {
|
|
761
1031
|
let endDate: Date
|
|
762
1032
|
if (info.event.allDay) {
|
|
763
1033
|
if (info.event.end) {
|
|
@@ -768,12 +1038,28 @@ export function Calendar({
|
|
|
768
1038
|
} else {
|
|
769
1039
|
endDate = info.event.end ?? info.event.start
|
|
770
1040
|
}
|
|
771
|
-
updatedFields[
|
|
1041
|
+
updatedFields[eventCalendarConfig.endField] = Timestamp.fromDate(endDate)
|
|
1042
|
+
}
|
|
1043
|
+
if (eventCalendarConfig.allDayField !== undefined) {
|
|
1044
|
+
updatedFields[eventCalendarConfig.allDayField] = info.event.allDay
|
|
772
1045
|
}
|
|
773
|
-
|
|
774
|
-
|
|
1046
|
+
|
|
1047
|
+
const optimisticUpdate = { ...record, ...updatedFields }
|
|
1048
|
+
setOptimisticUpdate(eventCollectionName, optimisticUpdate)
|
|
1049
|
+
|
|
1050
|
+
const patchAdditionalList = (value: StokerRecord) => {
|
|
1051
|
+
if (!isAdditionalCollection) return
|
|
1052
|
+
setAdditionalLists((prev) => ({
|
|
1053
|
+
...prev,
|
|
1054
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1055
|
+
[eventCollectionName]: (prev[eventCollectionName] ?? []).map((additionalRecord) =>
|
|
1056
|
+
additionalRecord.id === record.id ? value : additionalRecord,
|
|
1057
|
+
),
|
|
1058
|
+
}))
|
|
775
1059
|
}
|
|
776
|
-
|
|
1060
|
+
patchAdditionalList(optimisticUpdate)
|
|
1061
|
+
|
|
1062
|
+
if (!isAdditionalCollection && calendarConfig.resourceField && "newResource" in info && info.newResource) {
|
|
777
1063
|
const field = getField(fields, calendarConfig.resourceField)
|
|
778
1064
|
if (!isRelationField(field)) {
|
|
779
1065
|
updatedFields[calendarConfig.resourceField] = info.newResource.title
|
|
@@ -796,59 +1082,74 @@ export function Calendar({
|
|
|
796
1082
|
}
|
|
797
1083
|
}
|
|
798
1084
|
}
|
|
1085
|
+
const resourceOptimisticUpdate = { ...record, ...updatedFields }
|
|
1086
|
+
setOptimisticUpdate(eventCollectionName, resourceOptimisticUpdate)
|
|
1087
|
+
patchAdditionalList(resourceOptimisticUpdate)
|
|
799
1088
|
}
|
|
800
1089
|
|
|
801
|
-
const offlineDisabled = await isOfflineDisabledSync("update",
|
|
1090
|
+
const offlineDisabled = await isOfflineDisabledSync("update", eventCollectionSchema, record)
|
|
802
1091
|
if (offlineDisabled) {
|
|
803
1092
|
alert(`You are offline and cannot update this record.`)
|
|
804
|
-
|
|
1093
|
+
info.revert()
|
|
1094
|
+
removeOptimisticUpdate(eventCollectionName, record.id)
|
|
1095
|
+
patchAdditionalList(originalRecord)
|
|
805
1096
|
return
|
|
806
1097
|
}
|
|
807
1098
|
|
|
808
|
-
const serverWrite = isServerUpdate(
|
|
809
|
-
const isServerReadOnly = serverReadOnly(
|
|
810
|
-
|
|
811
|
-
const optimisticUpdate = {
|
|
812
|
-
...record,
|
|
813
|
-
...updatedFields,
|
|
814
|
-
}
|
|
815
|
-
setOptimisticUpdate(labels.collection, optimisticUpdate)
|
|
1099
|
+
const serverWrite = isServerUpdate(eventCollectionSchema, record)
|
|
1100
|
+
const isServerReadOnly = serverReadOnly(eventCollectionSchema)
|
|
816
1101
|
|
|
817
|
-
const originalRecord = cloneDeep(record)
|
|
818
1102
|
setGlobalLoading("+", record.id, serverWrite, !(serverWrite || isServerReadOnly))
|
|
819
1103
|
updateRecord(record.Collection_Path, record.id, updatedFields, { originalRecord })
|
|
820
1104
|
.then(() => {
|
|
821
1105
|
if (serverWrite || isServerReadOnly) {
|
|
822
1106
|
toast({
|
|
823
1107
|
// eslint-disable-next-line security/detect-object-injection
|
|
824
|
-
description: `${
|
|
1108
|
+
description: `${eventRecordTitle} ${eventRecordTitleField ? record[eventRecordTitleField] : record.id} updated successfully.`,
|
|
825
1109
|
})
|
|
1110
|
+
removeOptimisticUpdate(eventCollectionName, record.id)
|
|
826
1111
|
}
|
|
827
|
-
removeOptimisticUpdate(labels.collection, record.id)
|
|
828
1112
|
})
|
|
829
1113
|
.catch((error) => {
|
|
830
1114
|
console.error(error)
|
|
831
1115
|
info.revert()
|
|
832
1116
|
toast({
|
|
833
1117
|
// eslint-disable-next-line security/detect-object-injection
|
|
834
|
-
description: `${
|
|
1118
|
+
description: `${eventRecordTitle} ${eventRecordTitleField ? record[eventRecordTitleField] : record.id} failed to update.`,
|
|
835
1119
|
variant: "destructive",
|
|
836
1120
|
})
|
|
837
|
-
removeOptimisticUpdate(
|
|
838
|
-
|
|
1121
|
+
removeOptimisticUpdate(eventCollectionName, record.id)
|
|
1122
|
+
patchAdditionalList(originalRecord)
|
|
1123
|
+
if (!isAdditionalCollection) {
|
|
1124
|
+
setOptimisticList()
|
|
1125
|
+
}
|
|
839
1126
|
})
|
|
840
1127
|
.finally(() => {
|
|
841
1128
|
setGlobalLoading("-", record.id, undefined, !(serverWrite || isServerReadOnly))
|
|
842
1129
|
})
|
|
843
1130
|
if (!serverWrite && !isServerReadOnly) {
|
|
844
|
-
removeCacheOptimistic(
|
|
1131
|
+
removeCacheOptimistic(eventCollectionSchema, record)
|
|
845
1132
|
toast({
|
|
846
1133
|
// eslint-disable-next-line security/detect-object-injection
|
|
847
|
-
description: `${
|
|
1134
|
+
description: `${eventRecordTitle} ${eventRecordTitleField ? record[eventRecordTitleField] : record.id} updated.`,
|
|
848
1135
|
})
|
|
849
1136
|
}
|
|
850
1137
|
},
|
|
851
|
-
[
|
|
1138
|
+
[
|
|
1139
|
+
calendarConfig,
|
|
1140
|
+
additionalConfig,
|
|
1141
|
+
additionalLists,
|
|
1142
|
+
collection,
|
|
1143
|
+
labels.collection,
|
|
1144
|
+
list,
|
|
1145
|
+
unscheduledRecords,
|
|
1146
|
+
recordTitleField,
|
|
1147
|
+
recordTitle,
|
|
1148
|
+
schema.collections,
|
|
1149
|
+
timezone,
|
|
1150
|
+
fields,
|
|
1151
|
+
setOptimisticList,
|
|
1152
|
+
],
|
|
852
1153
|
)
|
|
853
1154
|
|
|
854
1155
|
const createEvent = useCallback(
|
|
@@ -988,11 +1289,26 @@ export function Calendar({
|
|
|
988
1289
|
newRange.to = preloadCacheRange.end
|
|
989
1290
|
if (!isEqual(newRange, preloadRange)) {
|
|
990
1291
|
preloadCollection(labels.collection, undefined, preloadCacheRange)
|
|
1292
|
+
for (const additionalCollection of calendarConfig?.additionalCollections || []) {
|
|
1293
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1294
|
+
const additionalSchema = schema.collections[additionalCollection]
|
|
1295
|
+
if (!additionalSchema?.preloadCache?.range || !preloadCacheEnabled(additionalSchema))
|
|
1296
|
+
continue
|
|
1297
|
+
const preloadCacheRangeAdditional = cloneDeep(additionalSchema.preloadCache.range)
|
|
1298
|
+
preloadCacheRangeAdditional.start = preloadCacheRange.start
|
|
1299
|
+
preloadCacheRangeAdditional.end = preloadCacheRange.end
|
|
1300
|
+
preloadCollection(additionalCollection, undefined, preloadCacheRangeAdditional)
|
|
1301
|
+
}
|
|
991
1302
|
setPreloadRange((prev) => {
|
|
992
|
-
|
|
1303
|
+
const next = {
|
|
993
1304
|
...prev,
|
|
994
1305
|
[labels.collection]: newRange,
|
|
995
1306
|
}
|
|
1307
|
+
for (const additionalCollection of calendarConfig?.additionalCollections || []) {
|
|
1308
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1309
|
+
next[additionalCollection] = newRange
|
|
1310
|
+
}
|
|
1311
|
+
return next
|
|
996
1312
|
})
|
|
997
1313
|
}
|
|
998
1314
|
}
|
|
@@ -1028,17 +1344,34 @@ export function Calendar({
|
|
|
1028
1344
|
|
|
1029
1345
|
const isCreateDisabled = connectionStatus === "offline" && (isOfflineCreateDisabled || serverWriteOnly)
|
|
1030
1346
|
|
|
1347
|
+
let selectable = canAddRecords && !disableCreate && !isCreateDisabled && !!calendarConfig?.endField
|
|
1348
|
+
for (const creatableCalendarCollection of creatableCalendarCollections) {
|
|
1349
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1350
|
+
if (additionalConfig?.[creatableCalendarCollection]?.config.endField) {
|
|
1351
|
+
selectable = true
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1031
1355
|
const calendarProps: CalendarOptions = {
|
|
1032
1356
|
timeZone: timezone || "UTC",
|
|
1033
1357
|
firstDay: 1,
|
|
1034
1358
|
plugins,
|
|
1035
1359
|
events,
|
|
1036
1360
|
resources: Array.from(resources),
|
|
1037
|
-
selectable
|
|
1361
|
+
selectable,
|
|
1038
1362
|
droppable: hasStartUpdateAccess,
|
|
1039
1363
|
eventClick(info: EventClickArg) {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1364
|
+
const eventCollection = info.event.extendedProps.collection as string | undefined
|
|
1365
|
+
const recordId = (info.event.extendedProps.recordId as string | undefined) || info.event.id.split("-")[0]
|
|
1366
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1367
|
+
const eventCollectionSchema = eventCollection ? schema.collections[eventCollection] : collection
|
|
1368
|
+
const eventList =
|
|
1369
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1370
|
+
eventCollection && eventCollection !== labels.collection ? additionalLists[eventCollection] : list
|
|
1371
|
+
const record = eventList?.find((record) => record.id === recordId) as StokerRecord
|
|
1372
|
+
if (record && eventCollectionSchema) {
|
|
1373
|
+
goToRecord(eventCollectionSchema, record, undefined, true)
|
|
1374
|
+
}
|
|
1042
1375
|
},
|
|
1043
1376
|
eventDrop(info: EventDropArg) {
|
|
1044
1377
|
updateEvent(info)
|
|
@@ -1097,7 +1430,7 @@ export function Calendar({
|
|
|
1097
1430
|
)}
|
|
1098
1431
|
>
|
|
1099
1432
|
<CardContent className="p-4 h-full">
|
|
1100
|
-
{currentViewLarge && (
|
|
1433
|
+
{currentViewLarge && isInitialized && (
|
|
1101
1434
|
<FullCalendar
|
|
1102
1435
|
schedulerLicenseKey={import.meta.env.STOKER_FULLCALENDAR_KEY}
|
|
1103
1436
|
initialDate={currentDateLarge}
|
|
@@ -1129,7 +1462,7 @@ export function Calendar({
|
|
|
1129
1462
|
</ScrollArea>
|
|
1130
1463
|
<ScrollArea className="sm:hidden min-h-screen print:h-full">
|
|
1131
1464
|
<CardContent className="p-4 h-full">
|
|
1132
|
-
{currentViewSmall && (
|
|
1465
|
+
{currentViewSmall && isInitialized && (
|
|
1133
1466
|
<FullCalendar
|
|
1134
1467
|
schedulerLicenseKey={import.meta.env.STOKER_FULLCALENDAR_KEY}
|
|
1135
1468
|
initialDate={currentDateSmall}
|
package/src/Collection.tsx
CHANGED
|
@@ -58,7 +58,7 @@ import { useOptimistic } from "./providers/OptimisticProvider"
|
|
|
58
58
|
import { serverReadOnly } from "./utils/serverReadOnly"
|
|
59
59
|
import cloneDeep from "lodash/cloneDeep.js"
|
|
60
60
|
import { Map as StokerMap } from "./Map"
|
|
61
|
-
import { Calendar } from "./Calendar"
|
|
61
|
+
import { Calendar, mergeCalendarConfig } from "./Calendar"
|
|
62
62
|
import { useStokerState } from "./providers/StateProvider"
|
|
63
63
|
import { loadFilters } from "./utils/relationListFiltersState"
|
|
64
64
|
import { Filters } from "./Filters"
|
|
@@ -235,6 +235,25 @@ function Collection({
|
|
|
235
235
|
const [showImages, setShowImages] = useState(false)
|
|
236
236
|
const [showMap, setShowMap] = useState(false)
|
|
237
237
|
const [showCalendar, setShowCalendar] = useState(false)
|
|
238
|
+
const [additionalTitles, setAdditionalTitles] = useState<
|
|
239
|
+
| Record<
|
|
240
|
+
StokerCollection,
|
|
241
|
+
{
|
|
242
|
+
collection: string
|
|
243
|
+
record: string
|
|
244
|
+
}
|
|
245
|
+
>
|
|
246
|
+
| undefined
|
|
247
|
+
>(undefined)
|
|
248
|
+
const [additionalDisableCreate, setSetAdditionalDisableCreate] = useState<
|
|
249
|
+
Record<StokerCollection, boolean> | undefined
|
|
250
|
+
>(undefined)
|
|
251
|
+
const [additionalOfflineCreateDisabled, setAdditionalOfflineCreateDisabled] = useState<
|
|
252
|
+
Record<StokerCollection, boolean> | undefined
|
|
253
|
+
>(undefined)
|
|
254
|
+
const [additionalOfflineUpdateDisabled, setAdditionalOfflineUpdateDisabled] = useState<
|
|
255
|
+
Record<StokerCollection, boolean> | undefined
|
|
256
|
+
>(undefined)
|
|
238
257
|
|
|
239
258
|
const [search, setSearch] = useState("")
|
|
240
259
|
const isServerFullTextSearchActive = isServerFullTextSearch(
|
|
@@ -956,6 +975,54 @@ function Collection({
|
|
|
956
975
|
| undefined
|
|
957
976
|
setCalendarConfig(calendarConfig)
|
|
958
977
|
|
|
978
|
+
if (calendarConfig?.additionalCollections) {
|
|
979
|
+
for (const additionalCollection of calendarConfig.additionalCollections) {
|
|
980
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
981
|
+
const additionalSchema = schema.collections[additionalCollection]
|
|
982
|
+
const additionalCustomization = getCollectionConfigModule(additionalCollection)
|
|
983
|
+
const additionalTitles = await getCachedConfigValue(
|
|
984
|
+
additionalCustomization,
|
|
985
|
+
["collections", additionalCollection, "admin", "titles"],
|
|
986
|
+
[relationList ? "relation-list" : undefined, relationCollection, relationParent],
|
|
987
|
+
true,
|
|
988
|
+
)
|
|
989
|
+
setAdditionalTitles((prev) => ({
|
|
990
|
+
...prev,
|
|
991
|
+
[additionalCollection]: additionalTitles || additionalSchema.labels,
|
|
992
|
+
}))
|
|
993
|
+
const additionalDisableCreate = await getCachedConfigValue(
|
|
994
|
+
additionalCustomization,
|
|
995
|
+
["collections", additionalCollection, "admin", "hideCreate"],
|
|
996
|
+
[relationCollection?.labels.collection],
|
|
997
|
+
true,
|
|
998
|
+
)
|
|
999
|
+
setSetAdditionalDisableCreate((prev) => ({
|
|
1000
|
+
...prev,
|
|
1001
|
+
[additionalCollection]: !!additionalDisableCreate,
|
|
1002
|
+
}))
|
|
1003
|
+
const additionalOfflineCreateDisabled = await getCachedConfigValue(additionalCustomization, [
|
|
1004
|
+
"collections",
|
|
1005
|
+
additionalCollection,
|
|
1006
|
+
"custom",
|
|
1007
|
+
"disableOfflineCreate",
|
|
1008
|
+
])
|
|
1009
|
+
setAdditionalOfflineCreateDisabled((prev) => ({
|
|
1010
|
+
...prev,
|
|
1011
|
+
[additionalCollection]: additionalOfflineCreateDisabled,
|
|
1012
|
+
}))
|
|
1013
|
+
const additionalOfflineUpdateDisabled = await getCachedConfigValue(additionalCustomization, [
|
|
1014
|
+
"collections",
|
|
1015
|
+
additionalCollection,
|
|
1016
|
+
"custom",
|
|
1017
|
+
"disableOfflineUpdate",
|
|
1018
|
+
])
|
|
1019
|
+
setAdditionalOfflineUpdateDisabled((prev) => ({
|
|
1020
|
+
...prev,
|
|
1021
|
+
[additionalCollection]: additionalOfflineUpdateDisabled,
|
|
1022
|
+
}))
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
959
1026
|
const showListConfig =
|
|
960
1027
|
!!permissions.Role && (!listConfig?.roles || listConfig.roles.includes(permissions.Role))
|
|
961
1028
|
setShowList(showListConfig)
|
|
@@ -1662,36 +1729,123 @@ function Collection({
|
|
|
1662
1729
|
}, [rangeSelector])
|
|
1663
1730
|
|
|
1664
1731
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
|
1732
|
+
const [isCollectionPickerOpen, setIsCollectionPickerOpen] = useState(false)
|
|
1733
|
+
const [selectedCreateCollection, setSelectedCreateCollection] = useState<StokerCollection | undefined>(undefined)
|
|
1734
|
+
const [createCalendarConfig, setCreateCalendarConfig] = useState<CalendarConfig | undefined>(undefined)
|
|
1665
1735
|
const [selectedDateRange, setSelectedDateRange] = useState<{ startDate: Date; endDate?: Date } | null>(null)
|
|
1666
1736
|
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1737
|
+
const creatableCalendarCollections = useMemo(() => {
|
|
1738
|
+
const collections = [labels.collection, ...(calendarConfig?.additionalCollections || [])]
|
|
1739
|
+
return collections.filter((collectionName, index) => {
|
|
1740
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1741
|
+
const collectionSchema = schema.collections[collectionName]
|
|
1742
|
+
const hasEntityRestrictions = getEntityRestrictions(collectionSchema, permissions)
|
|
1743
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1744
|
+
const isDisableCreate = (index === 0 && disableCreate) || additionalDisableCreate?.[collectionName]
|
|
1745
|
+
const isOfflineCreateDisabled =
|
|
1746
|
+
connectionStatus === "offline" &&
|
|
1747
|
+
(index === 0
|
|
1748
|
+
? isCreateDisabled
|
|
1749
|
+
: // eslint-disable-next-line security/detect-object-injection
|
|
1750
|
+
additionalOfflineCreateDisabled?.[collectionName] || collectionSchema.access.serverWriteOnly)
|
|
1751
|
+
return (
|
|
1752
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1753
|
+
permissions.collections?.[collectionName]?.operations.includes("Create") &&
|
|
1754
|
+
!hasEntityRestrictions.some((entityRestriction) => entityRestriction.type === "Individual") &&
|
|
1755
|
+
!isOfflineCreateDisabled &&
|
|
1756
|
+
!isDisableCreate
|
|
1757
|
+
)
|
|
1758
|
+
})
|
|
1759
|
+
}, [
|
|
1760
|
+
labels.collection,
|
|
1761
|
+
calendarConfig?.additionalCollections,
|
|
1762
|
+
permissions,
|
|
1763
|
+
schema,
|
|
1764
|
+
disableCreate,
|
|
1765
|
+
additionalDisableCreate,
|
|
1766
|
+
additionalOfflineCreateDisabled,
|
|
1767
|
+
connectionStatus,
|
|
1768
|
+
isCreateDisabled,
|
|
1769
|
+
])
|
|
1770
|
+
|
|
1771
|
+
const openCreateDialogForCollection = useCallback(
|
|
1772
|
+
async (collectionName: StokerCollection) => {
|
|
1773
|
+
setSelectedCreateCollection(collectionName)
|
|
1774
|
+
if (collectionName === labels.collection) {
|
|
1775
|
+
setCreateCalendarConfig(calendarConfig)
|
|
1776
|
+
} else if (calendarConfig) {
|
|
1777
|
+
const additionalCustomization = getCollectionConfigModule(collectionName)
|
|
1778
|
+
const additionalConfig = (await getCachedConfigValue(additionalCustomization, [
|
|
1779
|
+
"collections",
|
|
1780
|
+
collectionName,
|
|
1781
|
+
"admin",
|
|
1782
|
+
"calendar",
|
|
1783
|
+
])) as CalendarConfig | undefined
|
|
1784
|
+
if (additionalConfig) {
|
|
1785
|
+
setCreateCalendarConfig(mergeCalendarConfig(calendarConfig, additionalConfig))
|
|
1786
|
+
} else {
|
|
1787
|
+
setCreateCalendarConfig(calendarConfig)
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
setIsCreateDialogOpen(true)
|
|
1791
|
+
},
|
|
1792
|
+
[calendarConfig, labels.collection],
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
const handleCalendarDateSelection = useCallback(
|
|
1796
|
+
(dateSelectionData: { startDate: Date; endDate?: Date }) => {
|
|
1797
|
+
if (creatableCalendarCollections.length === 0) return
|
|
1798
|
+
setSelectedDateRange(dateSelectionData)
|
|
1799
|
+
if (
|
|
1800
|
+
!relationList &&
|
|
1801
|
+
calendarConfig?.additionalCollections?.length &&
|
|
1802
|
+
creatableCalendarCollections.length > 1
|
|
1803
|
+
) {
|
|
1804
|
+
setIsCollectionPickerOpen(true)
|
|
1805
|
+
} else {
|
|
1806
|
+
openCreateDialogForCollection(creatableCalendarCollections[0] || labels.collection)
|
|
1807
|
+
}
|
|
1808
|
+
},
|
|
1809
|
+
[calendarConfig, creatableCalendarCollections, labels.collection, openCreateDialogForCollection],
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
const createCollectionSchema = useMemo(() => {
|
|
1813
|
+
const collectionName = selectedCreateCollection || labels.collection
|
|
1814
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1815
|
+
return schema.collections[collectionName] || collection
|
|
1816
|
+
}, [selectedCreateCollection, labels.collection, schema, collection])
|
|
1817
|
+
|
|
1818
|
+
const createRecordTitle = useMemo(() => {
|
|
1819
|
+
return (
|
|
1820
|
+
additionalTitles?.[selectedCreateCollection || labels.collection]?.record ||
|
|
1821
|
+
createCollectionSchema.labels.record
|
|
1822
|
+
)
|
|
1823
|
+
}, [createCollectionSchema, additionalTitles, selectedCreateCollection, labels.collection])
|
|
1671
1824
|
|
|
1672
1825
|
const createPrePopulatedRecord = useCallback(() => {
|
|
1673
1826
|
const prePopulatedRecord: Partial<StokerRecord> = {}
|
|
1827
|
+
const activeCalendarConfig = createCalendarConfig || calendarConfig
|
|
1674
1828
|
|
|
1675
|
-
if (selectedDateRange &&
|
|
1676
|
-
if (
|
|
1829
|
+
if (selectedDateRange && activeCalendarConfig) {
|
|
1830
|
+
if (activeCalendarConfig.startField) {
|
|
1677
1831
|
const startDate = keepTimezone(
|
|
1678
1832
|
DateTime.fromJSDate(selectedDateRange.startDate).setZone(timezone).toJSDate(),
|
|
1679
1833
|
timezone,
|
|
1680
1834
|
)
|
|
1681
|
-
prePopulatedRecord[
|
|
1835
|
+
prePopulatedRecord[activeCalendarConfig.startField] = Timestamp.fromDate(startDate)
|
|
1682
1836
|
}
|
|
1683
1837
|
|
|
1684
|
-
if (
|
|
1838
|
+
if (activeCalendarConfig.endField && selectedDateRange.endDate) {
|
|
1685
1839
|
const endDate = keepTimezone(
|
|
1686
1840
|
DateTime.fromJSDate(selectedDateRange.endDate).setZone(timezone).toJSDate(),
|
|
1687
1841
|
timezone,
|
|
1688
1842
|
)
|
|
1689
|
-
prePopulatedRecord[
|
|
1843
|
+
prePopulatedRecord[activeCalendarConfig.endField] = Timestamp.fromDate(endDate)
|
|
1690
1844
|
}
|
|
1691
1845
|
}
|
|
1692
1846
|
|
|
1693
1847
|
if (relationList && relationParent) {
|
|
1694
|
-
const relationFieldSchema = getField(fields, relationList.field)
|
|
1848
|
+
const relationFieldSchema = getField(createCollectionSchema.fields, relationList.field)
|
|
1695
1849
|
if (relationFieldSchema && isRelationField(relationFieldSchema)) {
|
|
1696
1850
|
const value: Record<string, StokerRecord> = {}
|
|
1697
1851
|
value[relationParent.id] = relationParent
|
|
@@ -1702,7 +1856,18 @@ function Collection({
|
|
|
1702
1856
|
|
|
1703
1857
|
if (Object.keys(prePopulatedRecord).length === 0) return
|
|
1704
1858
|
return prePopulatedRecord as StokerRecord
|
|
1705
|
-
}, [
|
|
1859
|
+
}, [
|
|
1860
|
+
selectedDateRange,
|
|
1861
|
+
createCalendarConfig,
|
|
1862
|
+
createCollectionSchema,
|
|
1863
|
+
calendarConfig,
|
|
1864
|
+
relationList,
|
|
1865
|
+
relationParent,
|
|
1866
|
+
selectedCreateCollection,
|
|
1867
|
+
labels.collection,
|
|
1868
|
+
fields,
|
|
1869
|
+
timezone,
|
|
1870
|
+
])
|
|
1706
1871
|
|
|
1707
1872
|
const mainContentRef = useRef<HTMLDivElement>(null)
|
|
1708
1873
|
const addButtonRef = useRef<HTMLButtonElement>(null)
|
|
@@ -2529,6 +2694,63 @@ function Collection({
|
|
|
2529
2694
|
</Button>
|
|
2530
2695
|
)
|
|
2531
2696
|
})()}
|
|
2697
|
+
{isCollectionPickerOpen &&
|
|
2698
|
+
createPortal(
|
|
2699
|
+
<div
|
|
2700
|
+
id="collection-picker-modal"
|
|
2701
|
+
className="fixed inset-0 z-50 flex items-center justify-center animate-in fade-in slide-in-from-top-4 duration-300"
|
|
2702
|
+
aria-modal="true"
|
|
2703
|
+
aria-live="polite"
|
|
2704
|
+
role="dialog"
|
|
2705
|
+
>
|
|
2706
|
+
<div className="fixed inset-0 bg-black/50" />
|
|
2707
|
+
<div className="relative bg-background rounded-lg w-full max-w-md overflow-hidden border border-border p-6">
|
|
2708
|
+
<div className="flex justify-end items-center mb-4">
|
|
2709
|
+
<Button
|
|
2710
|
+
type="button"
|
|
2711
|
+
variant="ghost"
|
|
2712
|
+
size="icon"
|
|
2713
|
+
onClick={() => {
|
|
2714
|
+
setIsCollectionPickerOpen(false)
|
|
2715
|
+
setSelectedDateRange(null)
|
|
2716
|
+
}}
|
|
2717
|
+
>
|
|
2718
|
+
<X className="h-4 w-4" />
|
|
2719
|
+
<span className="sr-only">Close</span>
|
|
2720
|
+
</Button>
|
|
2721
|
+
</div>
|
|
2722
|
+
<div className="flex flex-col gap-2">
|
|
2723
|
+
{creatableCalendarCollections.map(
|
|
2724
|
+
(collectionName) => {
|
|
2725
|
+
return (
|
|
2726
|
+
<Button
|
|
2727
|
+
key={collectionName}
|
|
2728
|
+
type="button"
|
|
2729
|
+
variant="outline"
|
|
2730
|
+
className="justify-start"
|
|
2731
|
+
onClick={() => {
|
|
2732
|
+
setIsCollectionPickerOpen(
|
|
2733
|
+
false,
|
|
2734
|
+
)
|
|
2735
|
+
openCreateDialogForCollection(
|
|
2736
|
+
collectionName,
|
|
2737
|
+
)
|
|
2738
|
+
}}
|
|
2739
|
+
>
|
|
2740
|
+
Add{" "}
|
|
2741
|
+
{/* eslint-disable-next-line security/detect-object-injection */}
|
|
2742
|
+
{additionalTitles?.[
|
|
2743
|
+
collectionName
|
|
2744
|
+
]?.record || recordTitle}
|
|
2745
|
+
</Button>
|
|
2746
|
+
)
|
|
2747
|
+
},
|
|
2748
|
+
)}
|
|
2749
|
+
</div>
|
|
2750
|
+
</div>
|
|
2751
|
+
</div>,
|
|
2752
|
+
document.body,
|
|
2753
|
+
)}
|
|
2532
2754
|
{isCreateDialogOpen &&
|
|
2533
2755
|
createPortal(
|
|
2534
2756
|
<div
|
|
@@ -2550,7 +2772,9 @@ function Collection({
|
|
|
2550
2772
|
id="dialog-title"
|
|
2551
2773
|
className="font-medium leading-none"
|
|
2552
2774
|
>
|
|
2553
|
-
Add
|
|
2775
|
+
Add{" "}
|
|
2776
|
+
{createRecordTitle ||
|
|
2777
|
+
recordTitle}
|
|
2554
2778
|
</h4>
|
|
2555
2779
|
<Button
|
|
2556
2780
|
type="button"
|
|
@@ -2560,12 +2784,18 @@ function Collection({
|
|
|
2560
2784
|
onClick={() => {
|
|
2561
2785
|
setIsCreateDialogOpen(false)
|
|
2562
2786
|
setSelectedDateRange(null)
|
|
2787
|
+
setSelectedCreateCollection(
|
|
2788
|
+
undefined,
|
|
2789
|
+
)
|
|
2790
|
+
setCreateCalendarConfig(
|
|
2791
|
+
undefined,
|
|
2792
|
+
)
|
|
2563
2793
|
setTimeout(() => {
|
|
2564
2794
|
addButtonRef.current?.focus()
|
|
2565
2795
|
}, 0)
|
|
2566
2796
|
|
|
2567
2797
|
localStorage.removeItem(
|
|
2568
|
-
`stoker-draft-${labels.collection}`,
|
|
2798
|
+
`stoker-draft-${selectedCreateCollection || labels.collection}`,
|
|
2569
2799
|
)
|
|
2570
2800
|
}}
|
|
2571
2801
|
>
|
|
@@ -2576,11 +2806,15 @@ function Collection({
|
|
|
2576
2806
|
</Button>
|
|
2577
2807
|
</div>
|
|
2578
2808
|
<RecordForm
|
|
2579
|
-
collection={
|
|
2809
|
+
collection={createCollectionSchema}
|
|
2580
2810
|
operation="create"
|
|
2581
|
-
path={[
|
|
2811
|
+
path={[
|
|
2812
|
+
selectedCreateCollection ||
|
|
2813
|
+
labels.collection,
|
|
2814
|
+
]}
|
|
2582
2815
|
record={createPrePopulatedRecord()}
|
|
2583
2816
|
draft={true}
|
|
2817
|
+
fromCalendar={true}
|
|
2584
2818
|
parentCollection={
|
|
2585
2819
|
relationCollection?.labels
|
|
2586
2820
|
.collection
|
|
@@ -2589,6 +2823,12 @@ function Collection({
|
|
|
2589
2823
|
onSuccess={() => {
|
|
2590
2824
|
setIsCreateDialogOpen(false)
|
|
2591
2825
|
setSelectedDateRange(null)
|
|
2826
|
+
setSelectedCreateCollection(
|
|
2827
|
+
undefined,
|
|
2828
|
+
)
|
|
2829
|
+
setCreateCalendarConfig(
|
|
2830
|
+
undefined,
|
|
2831
|
+
)
|
|
2592
2832
|
setTimeout(() => {
|
|
2593
2833
|
addButtonRef.current?.focus()
|
|
2594
2834
|
}, 0)
|
|
@@ -2823,6 +3063,10 @@ function Collection({
|
|
|
2823
3063
|
unsubscribe={unsubscribe}
|
|
2824
3064
|
setOptimisticList={setOptimisticList}
|
|
2825
3065
|
canAddRecords={!!(canAddRecords && !isCreateDisabled)}
|
|
3066
|
+
disableCreate={disableCreate}
|
|
3067
|
+
creatableCalendarCollections={creatableCalendarCollections}
|
|
3068
|
+
additionalOfflineUpdateDisabled={additionalOfflineUpdateDisabled}
|
|
3069
|
+
additionalTitles={additionalTitles}
|
|
2826
3070
|
onDateSelection={handleCalendarDateSelection}
|
|
2827
3071
|
backToStartKey={backToStartKey}
|
|
2828
3072
|
relationList={!!relationList}
|
package/src/Form.tsx
CHANGED
|
@@ -238,6 +238,7 @@ interface FormProps {
|
|
|
238
238
|
onSaveRecord?: () => void
|
|
239
239
|
rowSelection?: StokerRecord[]
|
|
240
240
|
fromRelationList?: string
|
|
241
|
+
fromCalendar?: boolean
|
|
241
242
|
parentCollection?: string
|
|
242
243
|
parentRecord?: StokerRecord
|
|
243
244
|
}
|
|
@@ -2408,6 +2409,7 @@ function RecordForm({
|
|
|
2408
2409
|
onSaveRecord,
|
|
2409
2410
|
rowSelection,
|
|
2410
2411
|
fromRelationList,
|
|
2412
|
+
fromCalendar,
|
|
2411
2413
|
parentCollection,
|
|
2412
2414
|
parentRecord,
|
|
2413
2415
|
}: FormProps) {
|
|
@@ -4480,10 +4482,12 @@ function RecordForm({
|
|
|
4480
4482
|
description: `${recordTitle} ${recordTitleField ? originalRecord?.[recordTitleField] : id} deleted.`,
|
|
4481
4483
|
})
|
|
4482
4484
|
}
|
|
4483
|
-
if (!(hidden && !fromRelationList)) {
|
|
4485
|
+
if (!(hidden && !fromRelationList) && !fromCalendar) {
|
|
4484
4486
|
navigate(fromRelationList ? fromRelationList : `/${labels.collection?.toLowerCase()}`)
|
|
4487
|
+
} else if (fromCalendar) {
|
|
4488
|
+
navigate(-1)
|
|
4485
4489
|
}
|
|
4486
|
-
}, [formValues, originalRecord, navigate, hidden])
|
|
4490
|
+
}, [formValues, originalRecord, navigate, hidden, fromCalendar])
|
|
4487
4491
|
|
|
4488
4492
|
const handleRestore = useCallback(async () => {
|
|
4489
4493
|
if (!formValues) return
|
|
@@ -4536,11 +4540,13 @@ function RecordForm({
|
|
|
4536
4540
|
})
|
|
4537
4541
|
}
|
|
4538
4542
|
if (!isServerReadOnly) {
|
|
4539
|
-
if (!(hidden && !fromRelationList)) {
|
|
4543
|
+
if (!(hidden && !fromRelationList) && !fromCalendar) {
|
|
4540
4544
|
navigate(fromRelationList ? fromRelationList : `/${labels.collection?.toLowerCase()}`)
|
|
4545
|
+
} else if (fromCalendar) {
|
|
4546
|
+
navigate(-1)
|
|
4541
4547
|
}
|
|
4542
4548
|
}
|
|
4543
|
-
}, [formValues, originalRecord, navigate, hidden])
|
|
4549
|
+
}, [formValues, originalRecord, navigate, hidden, fromCalendar])
|
|
4544
4550
|
|
|
4545
4551
|
const revert = useCallback(() => {
|
|
4546
4552
|
resetPermissions()
|
package/src/Record.tsx
CHANGED
|
@@ -61,6 +61,7 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
61
61
|
window.history.replaceState(null, "", location.pathname)
|
|
62
62
|
|
|
63
63
|
const fromRelationList = useRef(location.state?.relationList)
|
|
64
|
+
const fromCalendar = useRef(location.state?.fromCalendar)
|
|
64
65
|
|
|
65
66
|
if (!pathString) {
|
|
66
67
|
throw new Error("Path param is required")
|
|
@@ -243,6 +244,7 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
243
244
|
record={record}
|
|
244
245
|
isLoading={isLoading}
|
|
245
246
|
fromRelationList={fromRelationList.current}
|
|
247
|
+
fromCalendar={fromCalendar.current}
|
|
246
248
|
/>
|
|
247
249
|
</main>
|
|
248
250
|
}
|
|
@@ -373,6 +375,7 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
|
|
|
373
375
|
record={record}
|
|
374
376
|
isLoading={isLoading}
|
|
375
377
|
fromRelationList={fromRelationList.current}
|
|
378
|
+
fromCalendar={fromCalendar.current}
|
|
376
379
|
/>
|
|
377
380
|
</main>
|
|
378
381
|
}
|
package/src/utils/goToRecord.ts
CHANGED
|
@@ -9,7 +9,12 @@ export const useGoToRecord = () => {
|
|
|
9
9
|
const params = useParams()
|
|
10
10
|
const location = useLocation()
|
|
11
11
|
|
|
12
|
-
const goToRecord = (
|
|
12
|
+
const goToRecord = (
|
|
13
|
+
collection: CollectionSchema,
|
|
14
|
+
record: StokerRecord,
|
|
15
|
+
relationField?: RelationField,
|
|
16
|
+
fromCalendar?: boolean,
|
|
17
|
+
) => {
|
|
13
18
|
const customization = getCollectionConfigModule(collection.labels.collection)
|
|
14
19
|
if (!customization) return
|
|
15
20
|
let route = "edit"
|
|
@@ -25,6 +30,7 @@ export const useGoToRecord = () => {
|
|
|
25
30
|
record,
|
|
26
31
|
relationList: params.id ? location.pathname : undefined,
|
|
27
32
|
relationField,
|
|
33
|
+
fromCalendar,
|
|
28
34
|
},
|
|
29
35
|
},
|
|
30
36
|
),
|