@firecms/core 3.4.0-canary.8635253 → 3.4.0-canary.d20cc85

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.
Files changed (87) hide show
  1. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  2. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +7 -0
  5. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  6. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  7. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  8. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  9. package/dist/components/common/table_url_params.d.ts +24 -0
  10. package/dist/components/common/useDataSourceTableController.d.ts +7 -1
  11. package/dist/components/common/useTableSearchHelper.d.ts +7 -1
  12. package/dist/core/EntityEditView.d.ts +7 -1
  13. package/dist/form/EntityForm.d.ts +6 -1
  14. package/dist/hooks/data/save.d.ts +1 -1
  15. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  16. package/dist/hooks/data/useEntityFetch.d.ts +6 -1
  17. package/dist/index.es.js +949 -622
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/index.umd.js +948 -621
  20. package/dist/index.umd.js.map +1 -1
  21. package/dist/types/collections.d.ts +7 -0
  22. package/dist/types/datasource.d.ts +59 -4
  23. package/dist/types/entities.d.ts +21 -1
  24. package/dist/types/properties.d.ts +17 -1
  25. package/dist/types/side_entity_controller.d.ts +6 -0
  26. package/dist/types/translations.d.ts +6 -0
  27. package/dist/util/__tests__/collections.test.d.ts +1 -0
  28. package/dist/util/__tests__/urls.test.d.ts +1 -0
  29. package/dist/util/collections.d.ts +1 -1
  30. package/dist/util/index.d.ts +1 -0
  31. package/dist/util/navigation_from_path.d.ts +7 -0
  32. package/dist/util/navigation_utils.d.ts +3 -1
  33. package/dist/util/parent_references_from_path.d.ts +1 -0
  34. package/dist/util/urls.d.ts +22 -0
  35. package/package.json +4 -4
  36. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
  37. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  38. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  39. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +4 -0
  40. package/src/components/EntityCollectionView/EntityCollectionView.tsx +29 -3
  41. package/src/components/EntityCollectionView/useBoardDataController.tsx +11 -0
  42. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +11 -0
  43. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  44. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  45. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  46. package/src/components/common/table_url_params.ts +172 -0
  47. package/src/components/common/useDataSourceTableController.tsx +64 -175
  48. package/src/components/common/useTableSearchHelper.ts +21 -2
  49. package/src/contexts/SnackbarProvider.tsx +14 -1
  50. package/src/core/EntityEditView.tsx +21 -0
  51. package/src/core/EntitySidePanel.tsx +2 -0
  52. package/src/form/EntityForm.tsx +9 -3
  53. package/src/hooks/data/delete.ts +2 -0
  54. package/src/hooks/data/save.ts +2 -0
  55. package/src/hooks/data/useCollectionFetch.tsx +13 -0
  56. package/src/hooks/data/useEntityFetch.tsx +18 -0
  57. package/src/hooks/useResolvedNavigationFrom.tsx +2 -0
  58. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  59. package/src/internal/useBuildDataSource.ts +25 -4
  60. package/src/internal/useBuildSideEntityController.tsx +6 -0
  61. package/src/locales/de.ts +5 -0
  62. package/src/locales/en.ts +5 -0
  63. package/src/locales/es.ts +5 -0
  64. package/src/locales/fr.ts +5 -0
  65. package/src/locales/hi.ts +5 -0
  66. package/src/locales/it.ts +5 -0
  67. package/src/locales/pl.ts +5 -0
  68. package/src/locales/pt.ts +5 -0
  69. package/src/preview/components/StorageThumbnail.tsx +24 -2
  70. package/src/routes/FireCMSRoute.tsx +3 -0
  71. package/src/types/collections.ts +8 -0
  72. package/src/types/datasource.ts +59 -4
  73. package/src/types/entities.ts +24 -1
  74. package/src/types/properties.ts +17 -0
  75. package/src/types/side_entity_controller.tsx +6 -0
  76. package/src/types/translations.ts +6 -0
  77. package/src/util/__tests__/collections.test.ts +32 -0
  78. package/src/util/__tests__/urls.test.ts +131 -0
  79. package/src/util/collections.ts +1 -1
  80. package/src/util/entities.ts +3 -1
  81. package/src/util/index.ts +1 -0
  82. package/src/util/navigation_from_path.ts +19 -1
  83. package/src/util/navigation_utils.ts +4 -0
  84. package/src/util/parent_references_from_path.ts +10 -2
  85. package/src/util/previews.ts +8 -1
  86. package/src/util/resolutions.ts +8 -0
  87. package/src/util/urls.ts +52 -0
@@ -74,6 +74,10 @@ export function useValidateAuthenticator<USER extends User = any>
74
74
 
75
75
  if (authenticator instanceof Function && delegateUser && !equal(checkedUserRef.current?.uid, delegateUser.uid)) {
76
76
  setAuthLoading(true);
77
+ // Reset the error of any previous authentication attempt. Otherwise, a
78
+ // rejected user would keep `canAccessMainView` to false for every user
79
+ // logging in afterwards, locking them out of the CMS until a page reload.
80
+ setNotAllowedError(false);
77
81
  try {
78
82
  const allowed = await authenticator({
79
83
  user: delegateUser,
@@ -55,6 +55,7 @@ export function useBuildDataSource({
55
55
  */
56
56
  fetchCollection: useCallback(<M extends Record<string, any>>({
57
57
  path,
58
+ pathSegments,
58
59
  collection,
59
60
  filter,
60
61
  limit,
@@ -67,6 +68,7 @@ export function useBuildDataSource({
67
68
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
68
69
  return usedDelegate.fetchCollection<M>({
69
70
  path,
71
+ pathSegments,
70
72
  filter,
71
73
  limit,
72
74
  startAfter,
@@ -97,6 +99,7 @@ export function useBuildDataSource({
97
99
  ? useCallback(<M extends Record<string, any>>(
98
100
  {
99
101
  path,
102
+ pathSegments,
100
103
  collection: collectionProp,
101
104
  filter,
102
105
  limit,
@@ -117,6 +120,7 @@ export function useBuildDataSource({
117
120
 
118
121
  return usedDelegate.listenCollection<M>({
119
122
  path,
123
+ pathSegments,
120
124
  filter,
121
125
  limit,
122
126
  startAfter,
@@ -139,6 +143,7 @@ export function useBuildDataSource({
139
143
  */
140
144
  fetchEntity: useCallback(<M extends Record<string, any>>({
141
145
  path,
146
+ pathSegments,
142
147
  entityId,
143
148
  collection
144
149
  }: FetchEntityProps<M>
@@ -146,6 +151,7 @@ export function useBuildDataSource({
146
151
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
147
152
  return usedDelegate.fetchEntity({
148
153
  path,
154
+ pathSegments,
149
155
  entityId,
150
156
  collection
151
157
  });
@@ -165,6 +171,7 @@ export function useBuildDataSource({
165
171
  ? useCallback(<M extends Record<string, any>>(
166
172
  {
167
173
  path,
174
+ pathSegments,
168
175
  entityId,
169
176
  collection,
170
177
  onUpdate,
@@ -177,6 +184,7 @@ export function useBuildDataSource({
177
184
 
178
185
  return usedDelegate.listenEntity<M>({
179
186
  path,
187
+ pathSegments,
180
188
  entityId,
181
189
  onUpdate,
182
190
  onError,
@@ -198,6 +206,7 @@ export function useBuildDataSource({
198
206
  saveEntity: useCallback(async <M extends Record<string, any>>(
199
207
  {
200
208
  path,
209
+ pathSegments,
201
210
  entityId,
202
211
  values,
203
212
  collection: collectionProp,
@@ -244,6 +253,7 @@ export function useBuildDataSource({
244
253
  try {
245
254
  const entities = await usedDelegate.fetchCollection({
246
255
  path,
256
+ pathSegments,
247
257
  orderBy: orderProperty,
248
258
  order: "asc",
249
259
  limit: 1,
@@ -270,6 +280,7 @@ export function useBuildDataSource({
270
280
 
271
281
  return usedDelegate.saveEntity({
272
282
  path,
283
+ pathSegments,
273
284
  collection,
274
285
  entityId,
275
286
  values: finalValues,
@@ -292,12 +303,15 @@ export function useBuildDataSource({
292
303
  deleteEntity: useCallback(<M extends Record<string, any>>(
293
304
  {
294
305
  entity,
306
+ pathSegments,
295
307
  collection
296
308
  }: DeleteEntityProps<M>
297
309
  ): Promise<void> => {
298
310
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
299
311
  return usedDelegate.deleteEntity({
300
312
  entity,
313
+ // Falls back to what the entity carries from the fetch that produced it.
314
+ pathSegments: pathSegments ?? entity.pathSegments,
301
315
  collection
302
316
  });
303
317
  }, [delegate.deleteEntity]),
@@ -317,25 +331,28 @@ export function useBuildDataSource({
317
331
  name: string,
318
332
  value: any,
319
333
  entityId?: string,
320
- collection?: EntityCollection
334
+ collection?: EntityCollection,
335
+ pathSegments?: string[]
321
336
  ): Promise<boolean> => {
322
337
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
323
- return usedDelegate.checkUniqueField(path, name, value, entityId, collection);
338
+ return usedDelegate.checkUniqueField(path, name, value, entityId, collection, pathSegments);
324
339
  }, [delegate.checkUniqueField]),
325
340
 
326
- generateEntityId: useCallback((path: string, collection: EntityCollection): string => {
341
+ generateEntityId: useCallback((path: string, collection: EntityCollection, pathSegments?: string[]): string => {
327
342
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
328
- return usedDelegate.generateEntityId(path, collection);
343
+ return usedDelegate.generateEntityId(path, collection, pathSegments);
329
344
  }, [delegate.generateEntityId]),
330
345
 
331
346
  countEntities: delegate.countEntities ? async ({
332
347
  path,
348
+ pathSegments,
333
349
  collection,
334
350
  filter,
335
351
  order,
336
352
  orderBy
337
353
  }: {
338
354
  path: string,
355
+ pathSegments?: string[],
339
356
  collection: EntityCollection<any>,
340
357
  filter?: FilterValues<Extract<keyof any, string>>,
341
358
  orderBy?: string,
@@ -344,6 +361,7 @@ export function useBuildDataSource({
344
361
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
345
362
  return usedDelegate.countEntities!({
346
363
  path,
364
+ pathSegments,
347
365
  filter,
348
366
  orderBy,
349
367
  order,
@@ -353,11 +371,13 @@ export function useBuildDataSource({
353
371
 
354
372
  isFilterCombinationValid: useCallback(({
355
373
  path,
374
+ pathSegments,
356
375
  databaseId,
357
376
  filterValues,
358
377
  sortBy
359
378
  }: {
360
379
  path: string,
380
+ pathSegments?: string[],
361
381
  databaseId?: string,
362
382
  filterValues: FilterValues<any>,
363
383
  sortBy?: [string, "asc" | "desc"]
@@ -367,6 +387,7 @@ export function useBuildDataSource({
367
387
  return delegate.isFilterCombinationValid(
368
388
  {
369
389
  path,
390
+ pathSegments,
370
391
  databaseId,
371
392
  filterValues,
372
393
  sortBy
@@ -224,12 +224,14 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
224
224
  let sidePanel: EntitySidePanelProps<any> | undefined = undefined;
225
225
  let lastCollectionPath = "";
226
226
  let lastCollectionId: string | undefined = undefined;
227
+ let lastCollectionSegments: string[] | undefined = undefined;
227
228
  for (let i = 0; i < navigationViewsForPath.length; i++) {
228
229
  const navigationEntry = navigationViewsForPath[i];
229
230
 
230
231
  if (navigationEntry.type === "collection") {
231
232
  lastCollectionPath = navigationEntry.path;
232
233
  lastCollectionId = navigationEntry.collection.id;
234
+ lastCollectionSegments = navigationEntry.pathSegments;
233
235
  }
234
236
 
235
237
  const previousEntry = navigationViewsForPath[i - 1];
@@ -237,6 +239,7 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
237
239
  sidePanel = {
238
240
  path: navigationEntry.path,
239
241
  fullIdPath: navigationEntry.fullIdPath,
242
+ pathSegments: navigationEntry.pathSegments,
240
243
  entityId: navigationEntry.entityId,
241
244
  copy: false,
242
245
  width: navigationEntry.parentCollection?.sideDialogWidth
@@ -259,6 +262,9 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
259
262
  sidePanel = {
260
263
  path: lastCollectionPath,
261
264
  fullIdPath: lastCollectionId,
265
+ // A new entity still needs unambiguous segments: it may be created inside a
266
+ // subcollection whose parent id contains "/".
267
+ pathSegments: lastCollectionSegments,
262
268
  copy: false
263
269
  }
264
270
  }
package/src/locales/de.ts CHANGED
@@ -389,6 +389,7 @@ export const de: FireCMSTranslations = {
389
389
  download: "Herunterladen",
390
390
  large_number_of_documents: "Diese Sammlung hat eine große Anzahl von Dokumenten ({{count}}).",
391
391
  include_undefined_values: "Undefinierte Werte einschließen",
392
+ export_apply_filter_sort: "Nur Ergebnisse exportieren, die dem aktuellen Filter/der aktuellen Sortierung entsprechen",
392
393
  submit: "Einreichen",
393
394
 
394
395
  no_filterable_properties: "Keine filterbaren Eigenschaften verfügbar",
@@ -586,6 +587,10 @@ export const de: FireCMSTranslations = {
586
587
  order_property_not_found: "Eigenschaft \"{{property}}\" existiert nicht oder ist keine Zahlen-Eigenschaft. Bitte wählen Sie eine gültige Eigenschaft oder löschen Sie die Auswahl.",
587
588
  no_number_properties: "Keine Zahlen-Eigenschaften gefunden. Fügen Sie eine Zahlen-Eigenschaft hinzu, um die Sortierung zu aktivieren.",
588
589
  order_property_description: "Wählen Sie eine Zahlen-Eigenschaft, um die Reihenfolge der Elemente beizubehalten",
590
+ image_property: "Bild-Eigenschaft",
591
+ image_property_not_found: "Eigenschaft \"{{property}}\" existiert nicht oder ist keine Bild-Eigenschaft. Bitte wählen Sie eine gültige Eigenschaft oder löschen Sie die Auswahl.",
592
+ no_image_properties: "Keine Bild-Eigenschaften gefunden. Fügen Sie eine Speicher- oder Bild-URL-Eigenschaft hinzu, um sie als Vorschaubild zu verwenden.",
593
+ image_property_description: "Wählen Sie die Eigenschaft, die als Vorschaubild in den Ansichten Karten und Kanban verwendet wird. Wenn nicht festgelegt, wird automatisch die erste passende Bild-Eigenschaft verwendet.",
589
594
  display_settings: "Anzeigeeinstellungen",
590
595
  default_row_size: "Standard-Zeilengröße",
591
596
  side_dialog_width: "Seitendialog-Breite",
package/src/locales/en.ts CHANGED
@@ -397,6 +397,7 @@ export const en: FireCMSTranslations = {
397
397
  download: "Download",
398
398
  large_number_of_documents: "This collections has a large number of documents ({{count}}).",
399
399
  include_undefined_values: "Include undefined values",
400
+ export_apply_filter_sort: "Only export results matching the current filter/sort",
400
401
  submit: "Submit",
401
402
 
402
403
  no_filterable_properties: "No filterable properties available",
@@ -598,6 +599,10 @@ export const en: FireCMSTranslations = {
598
599
  order_property_not_found: "Property \"{{property}}\" does not exist or is not a number property. Please select a valid property or clear the selection.",
599
600
  no_number_properties: "No number properties found. Add a number property to enable ordering.",
600
601
  order_property_description: "Select a number property to persist the order of items",
602
+ image_property: "Image Property",
603
+ image_property_not_found: "Property \"{{property}}\" does not exist or is not an image property. Please select a valid property or clear the selection.",
604
+ no_image_properties: "No image properties found. Add a storage or URL image property to use as a thumbnail.",
605
+ image_property_description: "Select the property used as the thumbnail image in Cards and Kanban view modes. If not set, the first matching image property will be used automatically.",
601
606
  display_settings: "Display settings",
602
607
  default_row_size: "Default row size",
603
608
  side_dialog_width: "Side dialog width",
package/src/locales/es.ts CHANGED
@@ -397,6 +397,7 @@ export const es: FireCMSTranslations = {
397
397
  download: "Descargar",
398
398
  large_number_of_documents: "Esta colección posee un gran número de documentos ({{count}}).",
399
399
  include_undefined_values: "Incluir valores omitidos (undefined)",
400
+ export_apply_filter_sort: "Exportar solo los resultados que coinciden con el filtro/orden actual",
400
401
  submit: "Enviar",
401
402
 
402
403
  no_filterable_properties: "No hay propiedades filtrables disponibles",
@@ -598,6 +599,10 @@ export const es: FireCMSTranslations = {
598
599
  order_property_not_found: "La propiedad \"{{property}}\" no existe o no es una propiedad numérica. Selecciona una propiedad válida o borra la selección.",
599
600
  no_number_properties: "No se encontraron propiedades numéricas. Agrega una propiedad numérica para habilitar el ordenamiento.",
600
601
  order_property_description: "Selecciona una propiedad numérica para mantener el orden de los elementos",
602
+ image_property: "Propiedad de imagen",
603
+ image_property_not_found: "La propiedad \"{{property}}\" no existe o no es una propiedad de imagen. Selecciona una propiedad válida o borra la selección.",
604
+ no_image_properties: "No se encontraron propiedades de imagen. Agrega una propiedad de almacenamiento o URL de imagen para usarla como miniatura.",
605
+ image_property_description: "Selecciona la propiedad usada como miniatura en las vistas de Tarjetas y Kanban. Si no se establece, se usará automáticamente la primera propiedad de imagen que coincida.",
601
606
  display_settings: "Configuración de visualización",
602
607
  default_row_size: "Tamaño de fila predeterminado",
603
608
  side_dialog_width: "Ancho del diálogo lateral",
package/src/locales/fr.ts CHANGED
@@ -389,6 +389,7 @@ export const fr: FireCMSTranslations = {
389
389
  download: "Télécharger",
390
390
  large_number_of_documents: "Cette collection contient un grand nombre de documents ({{count}}).",
391
391
  include_undefined_values: "Inclure les valeurs non définies",
392
+ export_apply_filter_sort: "N'exporter que les résultats correspondant au filtre/tri actuel",
392
393
  submit: "Soumettre",
393
394
 
394
395
  no_filterable_properties: "Aucune propriété filtrable disponible",
@@ -586,6 +587,10 @@ export const fr: FireCMSTranslations = {
586
587
  order_property_not_found: "La propriété \"{{property}}\" n'existe pas ou n'est pas une propriété numérique. Veuillez sélectionner une propriété valide ou effacer la sélection.",
587
588
  no_number_properties: "Aucune propriété numérique trouvée. Ajoutez une propriété numérique pour activer le tri.",
588
589
  order_property_description: "Sélectionnez une propriété numérique pour conserver l'ordre des éléments",
590
+ image_property: "Propriété d'image",
591
+ image_property_not_found: "La propriété \"{{property}}\" n'existe pas ou n'est pas une propriété d'image. Veuillez sélectionner une propriété valide ou effacer la sélection.",
592
+ no_image_properties: "Aucune propriété d'image trouvée. Ajoutez une propriété de stockage ou d'URL d'image à utiliser comme miniature.",
593
+ image_property_description: "Sélectionnez la propriété utilisée comme miniature dans les vues Cartes et Kanban. Si elle n'est pas définie, la première propriété d'image correspondante sera utilisée automatiquement.",
589
594
  display_settings: "Paramètres d'affichage",
590
595
  default_row_size: "Taille de ligne par défaut",
591
596
  side_dialog_width: "Largeur du dialogue latéral",
package/src/locales/hi.ts CHANGED
@@ -389,6 +389,7 @@ export const hi: FireCMSTranslations = {
389
389
  download: "डाउनलोड",
390
390
  large_number_of_documents: "इस संग्रह में बड़ी संख्या में दस्तावेज़ हैं ({{count}})।",
391
391
  include_undefined_values: "अपरिभाषित (undefined) मान शामिल करें",
392
+ export_apply_filter_sort: "केवल वर्तमान फ़िल्टर/सॉर्ट से मेल खाने वाले परिणाम निर्यात करें",
392
393
  submit: "सबमिट करें",
393
394
 
394
395
  no_filterable_properties: "कोई फ़िल्टर करने योग्य गुण उपलब्ध नहीं हैं",
@@ -586,6 +587,10 @@ export const hi: FireCMSTranslations = {
586
587
  order_property_not_found: "प्रॉपर्टी \"{{property}}\" मौजूद नहीं है या एक संख्या प्रॉपर्टी नहीं है। कृपया एक वैध प्रॉपर्टी चुनें या चयन साफ़ करें।",
587
588
  no_number_properties: "कोई संख्या प्रॉपर्टी नहीं मिली। क्रम सक्षम करने के लिए एक संख्या प्रॉपर्टी जोड़ें।",
588
589
  order_property_description: "आइटम का क्रम बनाए रखने के लिए एक संख्या प्रॉपर्टी चुनें",
590
+ image_property: "इमेज प्रॉपर्टी",
591
+ image_property_not_found: "प्रॉपर्टी \"{{property}}\" मौजूद नहीं है या एक इमेज प्रॉपर्टी नहीं है। कृपया एक वैध प्रॉपर्टी चुनें या चयन साफ़ करें।",
592
+ no_image_properties: "कोई इमेज प्रॉपर्टी नहीं मिली। थंबनेल के रूप में उपयोग करने के लिए एक स्टोरेज या URL इमेज प्रॉपर्टी जोड़ें।",
593
+ image_property_description: "कार्ड्स और कानबन व्यू मोड में थंबनेल इमेज के रूप में उपयोग की जाने वाली प्रॉपर्टी चुनें। यदि सेट नहीं किया गया है, तो पहली मिलान वाली इमेज प्रॉपर्टी स्वचालित रूप से उपयोग की जाएगी।",
589
594
  display_settings: "प्रदर्शन सेटिंग्स",
590
595
  default_row_size: "डिफ़ॉल्ट पंक्ति आकार",
591
596
  side_dialog_width: "साइड डायलॉग चौड़ाई",
package/src/locales/it.ts CHANGED
@@ -389,6 +389,7 @@ export const it: FireCMSTranslations = {
389
389
  download: "Scarica",
390
390
  large_number_of_documents: "Questa collezione contiene un numero elevato di documenti ({{count}})",
391
391
  include_undefined_values: "Includi valori non definiti",
392
+ export_apply_filter_sort: "Esporta solo i risultati che corrispondono al filtro/ordinamento attuale",
392
393
  submit: "Invia",
393
394
 
394
395
  no_filterable_properties: "Nessuna proprietà filtrabile disponibile",
@@ -586,6 +587,10 @@ export const it: FireCMSTranslations = {
586
587
  order_property_not_found: "La proprietà \"{{property}}\" non esiste o non è una proprietà numerica. Seleziona una proprietà valida o cancella la selezione.",
587
588
  no_number_properties: "Nessuna proprietà numerica trovata. Aggiungi una proprietà numerica per abilitare l'ordinamento.",
588
589
  order_property_description: "Seleziona una proprietà numerica per mantenere l'ordine degli elementi",
590
+ image_property: "Proprietà immagine",
591
+ image_property_not_found: "La proprietà \"{{property}}\" non esiste o non è una proprietà immagine. Seleziona una proprietà valida o cancella la selezione.",
592
+ no_image_properties: "Nessuna proprietà immagine trovata. Aggiungi una proprietà di archiviazione o URL immagine da usare come miniatura.",
593
+ image_property_description: "Seleziona la proprietà utilizzata come miniatura nelle viste Schede e Kanban. Se non impostata, verrà utilizzata automaticamente la prima proprietà immagine corrispondente.",
589
594
  display_settings: "Impostazioni di visualizzazione",
590
595
  default_row_size: "Dimensione riga predefinita",
591
596
  side_dialog_width: "Larghezza del dialogo laterale",
package/src/locales/pl.ts CHANGED
@@ -389,6 +389,7 @@ export const pl: FireCMSTranslations = {
389
389
  download: "Pobierz",
390
390
  large_number_of_documents: "Ta kolekcja zawiera dużą liczbę dokumentów ({{count}}).",
391
391
  include_undefined_values: "Uwzględnij niezdefiniowane wartości",
392
+ export_apply_filter_sort: "Eksportuj tylko wyniki zgodne z aktualnym filtrem/sortowaniem",
392
393
  submit: "Wyślij",
393
394
 
394
395
  no_filterable_properties: "Brak dostępnych właściwości do filtrowania",
@@ -590,6 +591,10 @@ export const pl: FireCMSTranslations = {
590
591
  order_property_not_found: "Właściwość „{{property}}” nie istnieje lub nie jest właściwością liczbową. Wybierz prawidłową właściwość lub wyczyść wybór.",
591
592
  no_number_properties: "Nie znaleziono właściwości liczbowych. Dodaj właściwość liczbową, aby włączyć sortowanie.",
592
593
  order_property_description: "Wybierz właściwość liczbową, aby zachować kolejność elementów",
594
+ image_property: "Właściwość obrazu",
595
+ image_property_not_found: "Właściwość „{{property}}” nie istnieje lub nie jest właściwością obrazu. Wybierz prawidłową właściwość lub wyczyść wybór.",
596
+ no_image_properties: "Nie znaleziono właściwości obrazu. Dodaj właściwość przechowywania lub adresu URL obrazu, aby użyć jej jako miniatury.",
597
+ image_property_description: "Wybierz właściwość używaną jako miniatura w widokach Karty i Kanban. Jeśli nie zostanie ustawiona, automatycznie zostanie użyta pierwsza pasująca właściwość obrazu.",
593
598
  display_settings: "Ustawienia wyświetlania",
594
599
  default_row_size: "Domyślny rozmiar wiersza",
595
600
  side_dialog_width: "Szerokość okna bocznego",
package/src/locales/pt.ts CHANGED
@@ -394,6 +394,7 @@ export const pt: FireCMSTranslations = {
394
394
  download: "Descarregar",
395
395
  large_number_of_documents: "Esta coleção tem um grande número de documentos ({{count}}).",
396
396
  include_undefined_values: "Incluir valores indefinidos",
397
+ export_apply_filter_sort: "Exportar apenas os resultados que correspondem ao filtro/ordenação atual",
397
398
  submit: "Submeter",
398
399
 
399
400
  no_filterable_properties: "Não há propriedades filtráveis disponíveis",
@@ -595,6 +596,10 @@ export const pt: FireCMSTranslations = {
595
596
  order_property_not_found: "A propriedade \"{{property}}\" não existe ou não é uma propriedade numérica. Selecione uma propriedade válida ou limpe a seleção.",
596
597
  no_number_properties: "Nenhuma propriedade numérica encontrada. Adicione uma propriedade numérica para habilitar a ordenação.",
597
598
  order_property_description: "Selecione uma propriedade numérica para manter a ordem dos itens",
599
+ image_property: "Propriedade de imagem",
600
+ image_property_not_found: "A propriedade \"{{property}}\" não existe ou não é uma propriedade de imagem. Selecione uma propriedade válida ou limpe a seleção.",
601
+ no_image_properties: "Nenhuma propriedade de imagem encontrada. Adicione uma propriedade de armazenamento ou URL de imagem para usar como miniatura.",
602
+ image_property_description: "Selecione a propriedade usada como miniatura nas visualizações de Cartões e Kanban. Se não for definida, a primeira propriedade de imagem correspondente será usada automaticamente.",
598
603
  display_settings: "Configurações de exibição",
599
604
  default_row_size: "Tamanho de linha padrão",
600
605
  side_dialog_width: "Largura do diálogo lateral",
@@ -6,6 +6,7 @@ import { useStorageSource } from "../../hooks";
6
6
  import { DownloadConfig, FileType } from "../../types";
7
7
  import { PreviewSize } from "../PropertyPreviewProps";
8
8
  import { ErrorView } from "../../components";
9
+ import { getPreviewTypeFromUrl, isAbsoluteHttpUrl } from "../../util/urls";
9
10
 
10
11
  type StorageThumbnailProps = {
11
12
  storagePathOrDownloadUrl: string;
@@ -41,10 +42,20 @@ export function StorageThumbnailInternal({
41
42
  const [error, setError] = React.useState<Error | undefined>(undefined);
42
43
  const storage = useStorageSource();
43
44
 
45
+ /**
46
+ * The saved value may already be a resolved download URL instead of a
47
+ * storage path: either because the property is configured with
48
+ * `storeUrl: true`, or because the value was written directly in the
49
+ * datasource pointing to an externally hosted file. Such values can be
50
+ * previewed as they are, and must not be handed over to the storage source,
51
+ * which can only resolve paths of its own bucket.
52
+ */
53
+ const valueIsUrl = storeUrl || isAbsoluteHttpUrl(storagePathOrDownloadUrl);
54
+
44
55
  const [downloadConfig, setDownloadConfig] = React.useState<DownloadConfig>(URL_CACHE[storagePathOrDownloadUrl]);
45
56
 
46
57
  useEffect(() => {
47
- if (!storagePathOrDownloadUrl)
58
+ if (!storagePathOrDownloadUrl || valueIsUrl)
48
59
  return;
49
60
  let unmounted = false;
50
61
  storage.getDownloadURL(storagePathOrDownloadUrl)
@@ -57,10 +68,21 @@ export function StorageThumbnailInternal({
57
68
  return () => {
58
69
  unmounted = true;
59
70
  };
60
- }, [storagePathOrDownloadUrl]);
71
+ }, [storagePathOrDownloadUrl, valueIsUrl]);
61
72
 
62
73
  if (!storagePathOrDownloadUrl) return null;
63
74
 
75
+ if (valueIsUrl) {
76
+ // There is no storage metadata for a value that is already a URL, so the
77
+ // preview type is inferred from its file extension when possible.
78
+ return <UrlComponentPreview previewType={getPreviewTypeFromUrl(storagePathOrDownloadUrl) ?? "file"}
79
+ url={storagePathOrDownloadUrl}
80
+ interactive={interactive}
81
+ size={size}
82
+ fill={fill}
83
+ hint={storagePathOrDownloadUrl} />;
84
+ }
85
+
64
86
  const filetype = downloadConfig?.metadata ? getFiletype(downloadConfig?.metadata.contentType) : undefined;
65
87
  const previewType = filetype?.startsWith("image")
66
88
  ? "image"
@@ -99,6 +99,7 @@ export function FireCMSRoute() {
99
99
  isSubCollection={false}
100
100
  parentCollectionIds={[]}
101
101
  fullPath={collection.path}
102
+ pathSegments={navigationEntries[0].pathSegments}
102
103
  fullIdPath={collection.id}
103
104
  updateUrl={true}
104
105
  {...collection}
@@ -123,6 +124,7 @@ export function FireCMSRoute() {
123
124
  isSubCollection={false}
124
125
  parentCollectionIds={[]}
125
126
  fullPath={collection.path}
127
+ pathSegments={firstEntry.pathSegments}
126
128
  updateUrl={true}
127
129
  {...collection}
128
130
  Actions={toArray(collection.Actions)} />
@@ -255,6 +257,7 @@ function EntityFullScreenRoute({
255
257
  key={collection.id + "_" + (isNew ? "new" : (isCopy ? entityId + "_copy" : entityId))}
256
258
  entityId={isNew ? undefined : entityId}
257
259
  fullIdPath={fullIdPath}
260
+ pathSegments={isNew ? lastCollectionEntry!.pathSegments : lastEntityEntry!.pathSegments}
258
261
  collection={collection}
259
262
  layout={"full_screen"}
260
263
  path={collectionPath}
@@ -101,6 +101,14 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
101
101
  */
102
102
  titleProperty?: keyof M;
103
103
 
104
+ /**
105
+ * Property used as the image thumbnail in Cards and Kanban view modes.
106
+ * Must reference a storage/image property (or an array of them), or a
107
+ * string property with `url: "image"`.
108
+ * If not specified, the first matching image property will be used.
109
+ */
110
+ imageProperty?: keyof M;
111
+
104
112
  /**
105
113
  * When editing an entity, you can choose to open the entity in a side dialog
106
114
  * or in a full screen dialog. Defaults to `full_screen`.
@@ -8,6 +8,20 @@ import { FireCMSContext } from "./firecms_context";
8
8
  */
9
9
  export interface FetchEntityProps<M extends Record<string, any> = any> {
10
10
  path: string;
11
+ /**
12
+ * The path split at its real segment boundaries, e.g.
13
+ * `["nodes", "node/42", "edges"]`.
14
+ *
15
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
16
+ * recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
17
+ * nesting. This is the unambiguous form: one array element per segment, entity ids
18
+ * kept whole however many slashes they contain.
19
+ *
20
+ * For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
21
+ * delegates that do not care can keep using `path`. Delegates that do should prefer
22
+ * `pathSegments ?? path.split("/")`.
23
+ */
24
+ pathSegments?: string[];
11
25
  entityId: string;
12
26
  databaseId?: string;
13
27
  collection?: EntityCollection<M, any>
@@ -28,6 +42,20 @@ export type ListenEntityProps<M extends Record<string, any> = any> =
28
42
  */
29
43
  export interface FetchCollectionProps<M extends Record<string, any> = any> {
30
44
  path: string;
45
+ /**
46
+ * The path split at its real segment boundaries, e.g.
47
+ * `["nodes", "node/42", "edges"]`.
48
+ *
49
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
50
+ * recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
51
+ * nesting. This is the unambiguous form: one array element per segment, entity ids
52
+ * kept whole however many slashes they contain.
53
+ *
54
+ * For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
55
+ * delegates that do not care can keep using `path`. Delegates that do should prefer
56
+ * `pathSegments ?? path.split("/")`.
57
+ */
58
+ pathSegments?: string[];
31
59
  collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
32
60
  filter?: FilterValues<Extract<keyof M, string>>,
33
61
  limit?: number;
@@ -52,6 +80,20 @@ export type ListenCollectionProps<M extends Record<string, any> = any> =
52
80
  */
53
81
  export interface SaveEntityProps<M extends Record<string, any> = any> {
54
82
  path: string;
83
+ /**
84
+ * The path split at its real segment boundaries, e.g.
85
+ * `["nodes", "node/42", "edges"]`.
86
+ *
87
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
88
+ * recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
89
+ * nesting. This is the unambiguous form: one array element per segment, entity ids
90
+ * kept whole however many slashes they contain.
91
+ *
92
+ * For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
93
+ * delegates that do not care can keep using `path`. Delegates that do should prefer
94
+ * `pathSegments ?? path.split("/")`.
95
+ */
96
+ pathSegments?: string[];
55
97
  values: Partial<EntityValues<M>>;
56
98
  entityId?: string; // can be empty for new entities
57
99
  previousValues?: Partial<EntityValues<M>>;
@@ -64,6 +106,11 @@ export interface SaveEntityProps<M extends Record<string, any> = any> {
64
106
  */
65
107
  export interface DeleteEntityProps<M extends Record<string, any> = any> {
66
108
  entity: Entity<M>;
109
+ /**
110
+ * `entity.path` split at its real segment boundaries, when the caller knows them.
111
+ * Never derived from `entity.path` — see `pathSegments` on FetchCollectionProps.
112
+ */
113
+ pathSegments?: string[];
67
114
  collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
68
115
  }
69
116
 
@@ -203,13 +250,19 @@ export interface DataSource {
203
250
  name: string,
204
251
  value: any,
205
252
  entityId?: string,
206
- collection?: EntityCollection
253
+ collection?: EntityCollection,
254
+ /**
255
+ * Appended last, and optional, so existing implementations and callers keep
256
+ * working unchanged — a function declaring fewer parameters stays assignable.
257
+ * See `pathSegments` on FetchCollectionProps.
258
+ */
259
+ pathSegments?: string[]
207
260
  ): Promise<boolean>;
208
261
 
209
262
  /**
210
263
  * Generate an id for a new entity
211
264
  */
212
- generateEntityId(path: string, collection: EntityCollection): string;
265
+ generateEntityId(path: string, collection: EntityCollection, pathSegments?: string[]): string;
213
266
 
214
267
  /**
215
268
  * Count the number of entities in a collection
@@ -237,6 +290,8 @@ export interface DataSource {
237
290
  }
238
291
 
239
292
  export type FilterCombinationValidProps = {
293
+ /** `path` split at its real segment boundaries — see `pathSegments` on FetchCollectionProps. */
294
+ pathSegments?: string[];
240
295
  path: string;
241
296
  collection: EntityCollection<any>;
242
297
  filterValues: FilterValues<any>;
@@ -366,12 +421,12 @@ export interface DataSourceDelegate {
366
421
  * @param collection
367
422
  * @return `true` if there are no other fields besides the given entity
368
423
  */
369
- checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
424
+ checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection, pathSegments?: string[]): Promise<boolean>;
370
425
 
371
426
  /**
372
427
  * Generate an id for a new entity
373
428
  */
374
- generateEntityId(path: string, collection?: EntityCollection): string;
429
+ generateEntityId(path: string, collection?: EntityCollection, pathSegments?: string[]): string;
375
430
 
376
431
  /**
377
432
  * Count the number of entities in a collection
@@ -21,6 +21,17 @@ export interface Entity<M extends object = any> {
21
21
  */
22
22
  path: string;
23
23
 
24
+ /**
25
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
26
+ *
27
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
28
+ * recovered from it. This keeps each segment whole.
29
+ *
30
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
31
+ * the case the field exists for. Absent means "not known here", not "no slashes".
32
+ */
33
+ pathSegments?: string[];
34
+
24
35
  /**
25
36
  * Current values
26
37
  */
@@ -55,10 +66,22 @@ export class EntityReference {
55
66
  */
56
67
  readonly databaseId?: string;
57
68
 
58
- constructor(id: string, path: string, databaseId?: string) {
69
+ /**
70
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
71
+ *
72
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
73
+ * recovered from it. This keeps each segment whole.
74
+ *
75
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
76
+ * the case the field exists for. Absent means "not known here", not "no slashes".
77
+ */
78
+ readonly pathSegments?: string[];
79
+
80
+ constructor(id: string, path: string, databaseId?: string, pathSegments?: string[]) {
59
81
  this.id = id;
60
82
  this.path = path;
61
83
  this.databaseId = databaseId;
84
+ this.pathSegments = pathSegments;
62
85
  }
63
86
 
64
87
  get pathWithId() {
@@ -562,6 +562,22 @@ export type PropertyBuilderProps<M extends Record<string, any> = any> =
562
562
  * Current value of this property
563
563
  */
564
564
  propertyValue?: any;
565
+ /**
566
+ * Path of this property within the entity, in dot notation, e.g.
567
+ * `my_map.my_field` or `my_array.0.my_field`.
568
+ * Useful to look up sibling values in `values` with `getValueInPath`.
569
+ *
570
+ * When a property builder is used as the `of` prop of an array property,
571
+ * it is resolved twice: once per existing element, with an index
572
+ * qualified key (`my_array.0`), and once as the template for the element
573
+ * shape, with the array's own key (`my_array`). In the latter case
574
+ * `propertyValue` is the whole array, since the template is not bound to
575
+ * any element.
576
+ *
577
+ * It may be undefined if the property is resolved outside the context of
578
+ * an entity.
579
+ */
580
+ propertyKey?: string;
565
581
  /**
566
582
  * Index of this property (only for arrays)
567
583
  */
@@ -592,6 +608,7 @@ export type PropertyBuilder<T extends CMSType = any, M extends Record<string, an
592
608
  values,
593
609
  previousValues,
594
610
  propertyValue,
611
+ propertyKey,
595
612
  index,
596
613
  path,
597
614
  entityId,