@firecms/core 3.0.0-canary.84 → 3.0.0-canary.86

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.
@@ -8,7 +8,7 @@ import { StorageSource } from "./storage";
8
8
  * to do it as the result of a hook.
9
9
  * @group Hooks and utilities
10
10
  */
11
- export type AuthController<UserType extends User = any, ExtraData extends any = any> = {
11
+ export type AuthController<UserType extends User = any, ExtraData = any> = {
12
12
  /**
13
13
  * The user currently logged in
14
14
  * The values can be: the user object, null if they skipped login
@@ -55,9 +55,8 @@ export interface DeleteEntityProps<M extends Record<string, any> = any> {
55
55
  entity: Entity<M>;
56
56
  }
57
57
  /**
58
- * Implement this interface and pass it to a {@link FireCMS}
59
- * to connect it to your data source.
60
- * A Firestore implementation of this interface can be found in {@link useFirestoreDataSource}
58
+ * Component in charge of communicating with the data source.
59
+ * Usually you won't need to implement this interface, but a {@link DataSourceDelegate} instead.
61
60
  * @group Datasource
62
61
  */
63
62
  export interface DataSource {
@@ -172,6 +171,14 @@ export type ListenCollectionDelegateProps<M extends Record<string, any> = any> =
172
171
  isCollectionGroup?: boolean;
173
172
  };
174
173
  export interface DataSourceDelegate {
174
+ /**
175
+ * Key that identifies this data source delegate
176
+ */
177
+ key: string;
178
+ /**
179
+ * If the data source has been initialised
180
+ */
181
+ initialised?: boolean;
175
182
  /**
176
183
  * Fetch data from a collection
177
184
  * @param path
@@ -220,8 +227,8 @@ export interface DataSourceDelegate {
220
227
  /**
221
228
  * Save entity to the specified path
222
229
  * @param path
223
- * @param id
224
- * @param collection
230
+ * @param entityId
231
+ * @param values
225
232
  * @param status
226
233
  */
227
234
  saveEntity<M extends Record<string, any> = any>({ path, entityId, values, status }: SaveEntityDelegateProps<M>): Promise<Entity<M>>;
@@ -256,7 +263,7 @@ export interface DataSourceDelegate {
256
263
  /**
257
264
  * Get the object to generate the current time in the datasource
258
265
  */
259
- currentTime(): any;
266
+ currentTime?: () => any;
260
267
  delegateToCMSModel: (data: any) => any;
261
268
  cmsToDelegateModel: (data: any) => any;
262
269
  setDateToMidnight: (input?: any) => any;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.84",
4
+ "version": "3.0.0-canary.86",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -46,8 +46,8 @@
46
46
  "./package.json": "./package.json"
47
47
  },
48
48
  "dependencies": {
49
- "@firecms/formex": "^3.0.0-canary.84",
50
- "@firecms/ui": "^3.0.0-canary.84",
49
+ "@firecms/formex": "^3.0.0-canary.86",
50
+ "@firecms/ui": "^3.0.0-canary.86",
51
51
  "@fontsource/jetbrains-mono": "^5.0.20",
52
52
  "@hello-pangea/dnd": "^16.6.0",
53
53
  "@radix-ui/react-portal": "^1.1.1",
@@ -72,37 +72,37 @@
72
72
  "firebase": "^10.5.2",
73
73
  "react": "^18.3.1",
74
74
  "react-dom": "^18.3.1",
75
- "react-router": "^6.22.0",
76
- "react-router-dom": "^6.22.0"
75
+ "react-router": "^6.25.1",
76
+ "react-router-dom": "^6.25.1"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@jest/globals": "^29.7.0",
80
80
  "@testing-library/react": "^15.0.7",
81
81
  "@testing-library/user-event": "^14.5.2",
82
82
  "@types/jest": "^29.5.12",
83
- "@types/node": "^20.14.9",
83
+ "@types/node": "^20.14.12",
84
84
  "@types/object-hash": "^3.0.6",
85
85
  "@types/react": "^18.3.3",
86
86
  "@types/react-dom": "^18.3.0",
87
87
  "@types/react-measure": "^2.0.12",
88
88
  "@vitejs/plugin-react": "^4.3.1",
89
89
  "cross-env": "^7.0.3",
90
- "firebase": "^10.12.2",
90
+ "firebase": "^10.12.4",
91
91
  "jest": "^29.7.0",
92
92
  "npm-run-all": "^4.1.5",
93
- "react-router": "^6.24.0",
94
- "react-router-dom": "^6.24.0",
95
- "ts-jest": "^29.1.5",
93
+ "react-router": "^6.25.1",
94
+ "react-router-dom": "^6.25.1",
95
+ "ts-jest": "^29.2.3",
96
96
  "ts-node": "^10.9.2",
97
97
  "tsd": "^0.31.1",
98
- "typescript": "^5.5.3",
99
- "vite": "^5.3.2"
98
+ "typescript": "^5.5.4",
99
+ "vite": "^5.3.4"
100
100
  },
101
101
  "files": [
102
102
  "dist",
103
103
  "src"
104
104
  ],
105
- "gitHead": "f286f012efde63060e83eeac5da67a866f1026e6",
105
+ "gitHead": "33cba1151b6e92dd0dd3b8ac36d428fed4f1f155",
106
106
  "publishConfig": {
107
107
  "access": "public"
108
108
  },
@@ -67,6 +67,7 @@ export const TableReferenceFieldSuccess = React.memo(
67
67
  }, [updateValue]);
68
68
 
69
69
  const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
70
+ console.log("onMultipleEntitiesSelected", entities);
70
71
  updateValue(entities.map((e) => getReferenceFrom(e)));
71
72
  }, [updateValue]);
72
73
 
@@ -199,6 +199,7 @@ const EntityPreviewContainerInner = React.forwardRef<HTMLDivElement, EntityPrevi
199
199
  }}
200
200
  className={cls(
201
201
  "bg-white dark:bg-gray-900",
202
+ "min-h-[42px]",
202
203
  fullwidth ? "w-full" : "",
203
204
  "items-center",
204
205
  hover ? "hover:bg-slate-50 dark:hover:bg-gray-800 group-hover:bg-slate-50 dark:group-hover:bg-gray-800" : "",
@@ -221,7 +221,6 @@ export const SelectableTable = React.memo<SelectableTableProps<any>>(
221
221
  onEndReached={loadNextPage}
222
222
  onResetPagination={resetPagination}
223
223
  error={dataLoadingError}
224
- paginationEnabled={paginationEnabled}
225
224
  onColumnResize={onColumnResize}
226
225
  rowHeight={getRowHeight(size)}
227
226
  loading={dataLoading}
@@ -95,7 +95,7 @@ export function DateTimeFilterField({
95
95
  size={"medium"}
96
96
  locale={locale}
97
97
  value={internalValue ?? undefined}
98
- onChange={(dateValue: Date | undefined) => {
98
+ onChange={(dateValue: Date | null) => {
99
99
  updateFilter(operation, dateValue === null ? undefined : dateValue);
100
100
  }}
101
101
  clearable={true}
@@ -91,6 +91,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
91
91
  data,
92
92
  onResetPagination,
93
93
  onEndReached,
94
+ endOffset = 600,
94
95
  rowHeight = 54,
95
96
  columns: columnsProp,
96
97
  onRowClick,
@@ -110,7 +111,8 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
110
111
  style,
111
112
  className,
112
113
  endAdornment,
113
- AddColumnComponent
114
+ AddColumnComponent,
115
+ debug
114
116
  }: VirtualTableProps<T>) {
115
117
 
116
118
  const sortByProperty: string | undefined = sortBy ? sortBy[0] : undefined;
@@ -128,10 +130,14 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
128
130
  const [measureRef, bounds] = useMeasure();
129
131
 
130
132
  const onColumnResizeInternal = useCallback((params: OnVirtualTableColumnResizeParams) => {
133
+ if (debug)
134
+ console.log("onColumnResizeInternal", params);
131
135
  setColumns(columns.map((column) => column.key === params.column.key ? params.column : column));
132
136
  }, [columns]);
133
137
 
134
138
  const onColumnResizeEndInternal = useCallback((params: OnVirtualTableColumnResizeParams) => {
139
+ if (debug)
140
+ console.log("onColumnResizeEndInternal", params);
135
141
  setColumns(columns.map((column) => column.key === params.column.key ? params.column : column));
136
142
  if (onColumnResize) {
137
143
  onColumnResize(params);
@@ -142,10 +148,14 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
142
148
  const filterRef = useRef<VirtualTableFilterValues<any> | undefined>();
143
149
 
144
150
  useEffect(() => {
151
+ if (debug)
152
+ console.log("Filter updated", filterInput);
145
153
  filterRef.current = filterInput;
146
154
  }, [filterInput]);
147
155
 
148
156
  const scrollToTop = useCallback(() => {
157
+ if (debug)
158
+ console.log("scrollToTop");
149
159
  endReachCallbackThreshold.current = 0;
150
160
  if (tableRef.current) {
151
161
  // scrollRef.current = [scrollRef.current[0], 0];
@@ -155,6 +165,9 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
155
165
 
156
166
  const onColumnSort = useCallback((key: string) => {
157
167
 
168
+ if (debug)
169
+ console.log("onColumnSort", key);
170
+
158
171
  const isDesc = sortByProperty === key && currentSort === "desc";
159
172
  const isAsc = sortByProperty === key && currentSort === "asc";
160
173
  const newSort = isAsc ? "desc" : (isDesc ? undefined : "asc");
@@ -181,21 +194,21 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
181
194
  scrollToTop();
182
195
  }, [checkFilterCombination, currentSort, onFilterUpdate, onResetPagination, onSortByUpdate, scrollToTop, sortByProperty]);
183
196
 
184
- const resetSort = useCallback(() => {
185
- endReachCallbackThreshold.current = 0;
186
- if (onSortByUpdate)
187
- onSortByUpdate(undefined);
188
- }, [onSortByUpdate]);
189
-
190
197
  const maxScroll = Math.max((data?.length ?? 0) * rowHeight - bounds.height, 0);
198
+ if (debug)
199
+ console.log("maxScroll", maxScroll);
200
+
191
201
  const onEndReachedInternal = useCallback((scrollOffset: number) => {
192
- if (onEndReached && (data?.length ?? 0) > 0 && scrollOffset > endReachCallbackThreshold.current + 600) {
202
+ if (debug)
203
+ console.log("onEndReachedInternal", scrollOffset, endReachCallbackThreshold.current + endOffset);
204
+ if (onEndReached && (data?.length ?? 0) > 0 && scrollOffset > endReachCallbackThreshold.current + endOffset) {
193
205
  endReachCallbackThreshold.current = scrollOffset;
194
206
  onEndReached();
195
207
  }
196
208
  }, [data?.length, onEndReached]);
197
209
 
198
210
  const onScroll = useCallback(({
211
+ scrollDirection,
199
212
  scrollOffset,
200
213
  scrollUpdateWasRequested
201
214
  }: {
@@ -203,11 +216,19 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
203
216
  scrollOffset: number,
204
217
  scrollUpdateWasRequested: boolean;
205
218
  }) => {
206
- if (!scrollUpdateWasRequested && (scrollOffset >= maxScroll - 600))
219
+ if(debug)
220
+ console.log("onScroll", {
221
+ scrollDirection,
222
+ scrollOffset,
223
+ scrollUpdateWasRequested
224
+ });
225
+ if (!scrollUpdateWasRequested && (scrollOffset >= maxScroll - endOffset))
207
226
  onEndReachedInternal(scrollOffset);
208
227
  }, [maxScroll, onEndReachedInternal]);
209
228
 
210
229
  const onFilterUpdateInternal = useCallback((column: VirtualTableColumn, filterForProperty?: [VirtualTableWhereFilterOp, any]) => {
230
+ if(debug)
231
+ console.log("onFilterUpdateInternal", column, filterForProperty);
211
232
 
212
233
  endReachCallbackThreshold.current = 0;
213
234
  const filter = filterRef.current;
@@ -272,6 +293,9 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
272
293
  AddColumnComponent
273
294
  };
274
295
 
296
+ if(debug)
297
+ console.log("VirtualTable render", virtualListController);
298
+
275
299
  // useTraceUpdate(virtualListController);
276
300
  return (
277
301
  <div
@@ -31,11 +31,6 @@ export interface VirtualTableProps<T extends Record<string, any>> {
31
31
  */
32
32
  cellRenderer: React.ComponentType<CellRendererParams<T>>;
33
33
 
34
- /**
35
- * If enabled, content is loaded in batch
36
- */
37
- paginationEnabled?: boolean;
38
-
39
34
  /**
40
35
  * Set this callback if you want to support some combinations
41
36
  * of filter combinations only.
@@ -50,6 +45,11 @@ export interface VirtualTableProps<T extends Record<string, any>> {
50
45
  */
51
46
  onEndReached?: () => void;
52
47
 
48
+ /**
49
+ * Offset in pixels where the onEndReached callback is triggered
50
+ */
51
+ endOffset?: number;
52
+
53
53
  /**
54
54
  * When the pagination should be reset. E.g. the filters or sorting
55
55
  * has been reset.
@@ -147,9 +147,14 @@ export interface VirtualTableProps<T extends Record<string, any>> {
147
147
  */
148
148
  AddColumnComponent?: React.ComponentType;
149
149
 
150
+ /**
151
+ * Debug mode
152
+ */
153
+ debug?: boolean;
154
+
150
155
  }
151
156
 
152
- export type CellRendererParams<T extends any = any> = {
157
+ export type CellRendererParams<T = any> = {
153
158
  column: VirtualTableColumn;
154
159
  columns: VirtualTableColumn[];
155
160
  columnIndex: number;
@@ -163,7 +168,7 @@ export type CellRendererParams<T extends any = any> = {
163
168
  * @see Table
164
169
  * @group Components
165
170
  */
166
- export interface VirtualTableColumn<CustomProps extends any = any> {
171
+ export interface VirtualTableColumn<CustomProps = any> {
167
172
 
168
173
  /**
169
174
  * Data key for the cell value, could be "a.b.c"
@@ -237,7 +242,6 @@ export type OnVirtualTableColumnResizeParams = {
237
242
  column: VirtualTableColumn
238
243
  };
239
244
 
240
-
241
245
  /**
242
246
  * @see Table
243
247
  * @group Components
@@ -82,7 +82,7 @@ export function FireCMS<UserType extends User, EC extends EntityCollection>(prop
82
82
  onAnalyticsEvent
83
83
  }), []);
84
84
 
85
- const accessResponse = useProjectLog(authController, plugins);
85
+ const accessResponse = useProjectLog(authController, dataSourceDelegate, plugins);
86
86
 
87
87
  if (navigationController.navigationLoadingError) {
88
88
  return (
@@ -43,7 +43,7 @@ export function DateTimeFieldBinding({
43
43
  <>
44
44
  <DateTimeField
45
45
  value={internalValue}
46
- onChange={(dateValue) => setValue(dateValue ?? null)}
46
+ onChange={(dateValue) => setValue(dateValue)}
47
47
  size={"medium"}
48
48
  mode={property.mode}
49
49
  clearable={property.clearable}
@@ -1,4 +1,4 @@
1
- import { useContext, useMemo } from "react";
1
+ import { useContext } from "react";
2
2
  import { DataSource, EntityCollection } from "../../types";
3
3
  import { DataSourceContext } from "../../contexts/DataSourceContext";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { AuthController, FireCMSPlugin } from "../types";
2
+ import { AuthController, DataSourceDelegate, FireCMSPlugin } from "../types";
3
3
 
4
4
  export const DEFAULT_SERVER_DEV = "https://api-kdoe6pj3qq-ey.a.run.app";
5
5
  export const DEFAULT_SERVER = "https://api-drplyi3b6q-ey.a.run.app";
@@ -9,17 +9,26 @@ export type AccessResponse = {
9
9
  message?: string;
10
10
  }
11
11
 
12
- async function makeRequest(authController: AuthController, pluginKeys: string[] | undefined) {
13
- const firebaseToken = await authController.getAuthToken();
12
+ async function makeRequest(authController: AuthController, dataSourceKey: string, pluginKeys: string[] | undefined) {
13
+ let idToken: string | null;
14
+ try {
15
+ idToken = await authController.getAuthToken();
16
+ } catch (e) {
17
+ idToken = null;
18
+ }
14
19
  return fetch(DEFAULT_SERVER + "/access_log",
15
20
  {
16
21
  // mode: "no-cors",
17
22
  method: "POST",
18
23
  headers: {
19
24
  "Content-Type": "application/json",
20
- Authorization: `Basic ${firebaseToken}`,
25
+ Authorization: `Basic ${idToken}`
21
26
  },
22
- body: JSON.stringify({ plugins: pluginKeys })
27
+ body: JSON.stringify({
28
+ email: authController.user?.email ?? null,
29
+ datasource: dataSourceKey,
30
+ plugins: pluginKeys
31
+ })
23
32
  })
24
33
  .then(async (res) => {
25
34
  return res.json();
@@ -27,15 +36,17 @@ async function makeRequest(authController: AuthController, pluginKeys: string[]
27
36
  }
28
37
 
29
38
  export function useProjectLog(authController: AuthController,
39
+ dataSourceDelegate: DataSourceDelegate,
30
40
  plugins?: FireCMSPlugin<any, any, any>[]): AccessResponse | null {
31
41
  const [accessResponse, setAccessResponse] = useState<AccessResponse | null>(null);
32
42
  const accessedUserRef = useRef<string | null>(null);
43
+ const dataSourceKey = dataSourceDelegate.key;
33
44
  const pluginKeys = plugins?.map(plugin => plugin.key);
34
45
  useEffect(() => {
35
46
  if (authController.user && authController.user.uid !== accessedUserRef.current && !authController.initialLoading) {
36
- makeRequest(authController, pluginKeys).then(setAccessResponse);
47
+ makeRequest(authController, dataSourceKey, pluginKeys).then(setAccessResponse);
37
48
  accessedUserRef.current = authController.user.uid;
38
49
  }
39
- }, [authController, pluginKeys]);
50
+ }, [authController, dataSourceKey, pluginKeys]);
40
51
  return accessResponse;
41
52
  }
@@ -216,7 +216,7 @@ export function useBuildDataSource({
216
216
  inputValues: firestoreValues,
217
217
  properties,
218
218
  status,
219
- timestampNowValue: delegate.currentTime(),
219
+ timestampNowValue: delegate.currentTime?.() ?? new Date(),
220
220
  setDateToMidnight: delegate.setDateToMidnight
221
221
  })
222
222
  : firestoreValues;
@@ -290,7 +290,7 @@ export function useBuildDataSource({
290
290
  filter,
291
291
  orderBy,
292
292
  order,
293
- isCollectionGroup: Boolean(collection.collectionGroup) ?? false
293
+ isCollectionGroup: Boolean(collection.collectionGroup)
294
294
  });
295
295
  } : undefined,
296
296
 
@@ -9,7 +9,7 @@ import { StorageSource } from "./storage";
9
9
  * to do it as the result of a hook.
10
10
  * @group Hooks and utilities
11
11
  */
12
- export type AuthController<UserType extends User = any, ExtraData extends any = any> = {
12
+ export type AuthController<UserType extends User = any, ExtraData = any> = {
13
13
 
14
14
  /**
15
15
  * The user currently logged in
@@ -82,7 +82,7 @@ export interface EntityCollection<M extends Record<string, any> = any, UserType
82
82
  * as the title in entity related views and references.
83
83
  * If not specified, the first property simple text property will be used.
84
84
  */
85
- titleProperty?: keyof M,
85
+ titleProperty?: keyof M;
86
86
 
87
87
  /**
88
88
  * Can this collection be edited by the end user.
@@ -1,4 +1,4 @@
1
- import { Entity, EntityReference, EntityStatus, EntityValues, GeoPoint } from "./entities";
1
+ import { Entity, EntityStatus, EntityValues } from "./entities";
2
2
  import { EntityCollection, FilterValues } from "./collections";
3
3
  import { ResolvedEntityCollection } from "./resolved_entities";
4
4
  import { FireCMSContext } from "./firecms_context";
@@ -66,9 +66,8 @@ export interface DeleteEntityProps<M extends Record<string, any> = any> {
66
66
  }
67
67
 
68
68
  /**
69
- * Implement this interface and pass it to a {@link FireCMS}
70
- * to connect it to your data source.
71
- * A Firestore implementation of this interface can be found in {@link useFirestoreDataSource}
69
+ * Component in charge of communicating with the data source.
70
+ * Usually you won't need to implement this interface, but a {@link DataSourceDelegate} instead.
72
71
  * @group Datasource
73
72
  */
74
73
  export interface DataSource {
@@ -254,6 +253,17 @@ export type ListenCollectionDelegateProps<M extends Record<string, any> = any> =
254
253
  };
255
254
 
256
255
  export interface DataSourceDelegate {
256
+
257
+ /**
258
+ * Key that identifies this data source delegate
259
+ */
260
+ key: string;
261
+
262
+ /**
263
+ * If the data source has been initialised
264
+ */
265
+ initialised?: boolean;
266
+
257
267
  /**
258
268
  * Fetch data from a collection
259
269
  * @param path
@@ -332,8 +342,8 @@ export interface DataSourceDelegate {
332
342
  /**
333
343
  * Save entity to the specified path
334
344
  * @param path
335
- * @param id
336
- * @param collection
345
+ * @param entityId
346
+ * @param values
337
347
  * @param status
338
348
  */
339
349
  saveEntity<M extends Record<string, any> = any>({
@@ -379,7 +389,7 @@ export interface DataSourceDelegate {
379
389
  /**
380
390
  * Get the object to generate the current time in the datasource
381
391
  */
382
- currentTime(): any;
392
+ currentTime?: () => any;
383
393
 
384
394
  delegateToCMSModel: (data: any) => any;
385
395