@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 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.217",
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.84",
55
- "@stoker-platform/utils": "0.5.75",
56
- "@stoker-platform/web-client": "0.5.88",
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 [hasReourceUpdateAccess, setHasReourceUpdateAccess] = useState<boolean>(false)
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 hasReourceUpdateAccess = !!canUpdateField(collection, resourceFieldSchema, permissions)
469
- setHasReourceUpdateAccess(hasReourceUpdateAccess)
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
- setIsInitialized(true)
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 collectionOptimisticUpdates = optimisticUpdates?.get(labels.collection)
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 && hasReourceUpdateAccess,
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
- hasReourceUpdateAccess,
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 (calendarConfig.startField && info.event.start) {
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[calendarConfig.startField] = Timestamp.fromDate(startDate)
1028
+ updatedFields[eventCalendarConfig.startField] = Timestamp.fromDate(startDate)
759
1029
  }
760
- if (calendarConfig.endField && info.event.start) {
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[calendarConfig.endField] = Timestamp.fromDate(endDate)
1041
+ updatedFields[eventCalendarConfig.endField] = Timestamp.fromDate(endDate)
1042
+ }
1043
+ if (eventCalendarConfig.allDayField !== undefined) {
1044
+ updatedFields[eventCalendarConfig.allDayField] = info.event.allDay
772
1045
  }
773
- if (calendarConfig.allDayField !== undefined) {
774
- updatedFields[calendarConfig.allDayField] = info.event.allDay
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
- if (calendarConfig.resourceField && "newResource" in info && info.newResource) {
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", collection, record)
1090
+ const offlineDisabled = await isOfflineDisabledSync("update", eventCollectionSchema, record)
802
1091
  if (offlineDisabled) {
803
1092
  alert(`You are offline and cannot update this record.`)
804
- removeOptimisticUpdate(labels.collection, record.id)
1093
+ info.revert()
1094
+ removeOptimisticUpdate(eventCollectionName, record.id)
1095
+ patchAdditionalList(originalRecord)
805
1096
  return
806
1097
  }
807
1098
 
808
- const serverWrite = isServerUpdate(collection, record)
809
- const isServerReadOnly = serverReadOnly(collection)
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: `${recordTitle} ${recordTitleField ? record[recordTitleField] : record.id} updated successfully.`,
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: `${recordTitle} ${recordTitleField ? record[recordTitleField] : record.id} failed to update.`,
1118
+ description: `${eventRecordTitle} ${eventRecordTitleField ? record[eventRecordTitleField] : record.id} failed to update.`,
835
1119
  variant: "destructive",
836
1120
  })
837
- removeOptimisticUpdate(labels.collection, record.id)
838
- setOptimisticList()
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(collection, record)
1131
+ removeCacheOptimistic(eventCollectionSchema, record)
845
1132
  toast({
846
1133
  // eslint-disable-next-line security/detect-object-injection
847
- description: `${recordTitle} ${recordTitleField ? record[recordTitleField] : record.id} updated.`,
1134
+ description: `${eventRecordTitle} ${eventRecordTitleField ? record[eventRecordTitleField] : record.id} updated.`,
848
1135
  })
849
1136
  }
850
1137
  },
851
- [calendarConfig, list, unscheduledRecords, recordTitleField, recordTitle, timezone],
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
- return {
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: canAddRecords && !isCreateDisabled && !!calendarConfig?.endField,
1361
+ selectable,
1038
1362
  droppable: hasStartUpdateAccess,
1039
1363
  eventClick(info: EventClickArg) {
1040
- const record = list?.find((record) => record.id === info.event.id.split("-")[0]) as StokerRecord
1041
- goToRecord(collection, record)
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}
@@ -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 handleCalendarDateSelection = useCallback((dateSelectionData: { startDate: Date; endDate?: Date }) => {
1668
- setSelectedDateRange(dateSelectionData)
1669
- setIsCreateDialogOpen(true)
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 && calendarConfig) {
1676
- if (calendarConfig.startField) {
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[calendarConfig.startField] = Timestamp.fromDate(startDate)
1835
+ prePopulatedRecord[activeCalendarConfig.startField] = Timestamp.fromDate(startDate)
1682
1836
  }
1683
1837
 
1684
- if (calendarConfig.endField && selectedDateRange.endDate) {
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[calendarConfig.endField] = Timestamp.fromDate(endDate)
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
- }, [selectedDateRange, calendarConfig])
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 {recordTitle}
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={collection}
2809
+ collection={createCollectionSchema}
2580
2810
  operation="create"
2581
- path={[labels.collection]}
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
  }
@@ -9,7 +9,12 @@ export const useGoToRecord = () => {
9
9
  const params = useParams()
10
10
  const location = useLocation()
11
11
 
12
- const goToRecord = (collection: CollectionSchema, record: StokerRecord, relationField?: RelationField) => {
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
  ),