@firecms/core 3.4.0-canary.f6a889a → 3.4.0

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 (123) 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 +8 -0
  5. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  8. package/dist/components/ErrorBoundary.d.ts +1 -3
  9. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  10. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  11. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  12. package/dist/components/common/table_url_params.d.ts +24 -0
  13. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  14. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  15. package/dist/core/EntityEditView.d.ts +9 -1
  16. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  17. package/dist/form/EntityForm.d.ts +8 -1
  18. package/dist/form/EntityFormActions.d.ts +2 -0
  19. package/dist/hooks/data/delete.d.ts +1 -1
  20. package/dist/hooks/data/save.d.ts +1 -1
  21. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  22. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  23. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  24. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  25. package/dist/index.es.js +1753 -1170
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/index.umd.js +1751 -1168
  28. package/dist/index.umd.js.map +1 -1
  29. package/dist/types/collections.d.ts +14 -0
  30. package/dist/types/datasource.d.ts +67 -4
  31. package/dist/types/entities.d.ts +21 -1
  32. package/dist/types/entity_actions.d.ts +6 -0
  33. package/dist/types/entity_callbacks.d.ts +21 -0
  34. package/dist/types/navigation.d.ts +34 -5
  35. package/dist/types/plugins.d.ts +13 -0
  36. package/dist/types/properties.d.ts +17 -1
  37. package/dist/types/side_entity_controller.d.ts +7 -0
  38. package/dist/types/translations.d.ts +6 -0
  39. package/dist/util/__tests__/collections.test.d.ts +1 -0
  40. package/dist/util/__tests__/urls.test.d.ts +1 -0
  41. package/dist/util/collections.d.ts +1 -1
  42. package/dist/util/entity_cache.d.ts +11 -0
  43. package/dist/util/index.d.ts +1 -0
  44. package/dist/util/navigation_from_path.d.ts +7 -0
  45. package/dist/util/navigation_utils.d.ts +87 -3
  46. package/dist/util/parent_references_from_path.d.ts +16 -0
  47. package/dist/util/paths.d.ts +28 -0
  48. package/dist/util/permissions.d.ts +10 -4
  49. package/dist/util/urls.d.ts +22 -0
  50. package/package.json +4 -4
  51. package/src/components/DeleteEntityDialog.tsx +2 -0
  52. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  53. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
  54. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  55. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  56. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  57. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  58. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  59. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  60. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  61. package/src/components/EntityPreview.tsx +4 -1
  62. package/src/components/ErrorBoundary.tsx +12 -18
  63. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  64. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  65. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  66. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  67. package/src/components/common/default_entity_actions.tsx +22 -2
  68. package/src/components/common/table_url_params.ts +172 -0
  69. package/src/components/common/useDataSourceTableController.tsx +86 -179
  70. package/src/components/common/useTableSearchHelper.ts +26 -2
  71. package/src/contexts/SnackbarProvider.tsx +14 -1
  72. package/src/core/EntityEditView.tsx +20 -3
  73. package/src/core/EntityEditViewFormActions.tsx +3 -2
  74. package/src/core/EntitySidePanel.tsx +8 -4
  75. package/src/form/EntityForm.tsx +31 -10
  76. package/src/form/EntityFormActions.tsx +2 -0
  77. package/src/hooks/data/delete.ts +8 -0
  78. package/src/hooks/data/save.ts +6 -1
  79. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  80. package/src/hooks/data/useEntityFetch.tsx +35 -4
  81. package/src/hooks/useBuildNavigationController.tsx +23 -8
  82. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  83. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  84. package/src/i18n/FireCMSi18nProvider.tsx +48 -4
  85. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  86. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  87. package/src/internal/useBuildDataSource.ts +37 -15
  88. package/src/internal/useBuildSideEntityController.tsx +19 -5
  89. package/src/locales/de.ts +5 -0
  90. package/src/locales/en.ts +5 -0
  91. package/src/locales/es.ts +5 -0
  92. package/src/locales/fr.ts +5 -0
  93. package/src/locales/hi.ts +5 -0
  94. package/src/locales/it.ts +5 -0
  95. package/src/locales/pl.ts +5 -0
  96. package/src/locales/pt.ts +5 -0
  97. package/src/preview/components/ReferencePreview.tsx +7 -3
  98. package/src/preview/components/StorageThumbnail.tsx +24 -2
  99. package/src/routes/FireCMSRoute.tsx +11 -3
  100. package/src/types/collections.ts +16 -0
  101. package/src/types/datasource.ts +67 -4
  102. package/src/types/entities.ts +24 -1
  103. package/src/types/entity_actions.tsx +6 -0
  104. package/src/types/entity_callbacks.ts +24 -0
  105. package/src/types/navigation.ts +35 -5
  106. package/src/types/plugins.tsx +13 -0
  107. package/src/types/properties.ts +17 -0
  108. package/src/types/side_entity_controller.tsx +7 -0
  109. package/src/types/translations.ts +6 -0
  110. package/src/util/__tests__/collections.test.ts +32 -0
  111. package/src/util/__tests__/urls.test.ts +131 -0
  112. package/src/util/collections.ts +1 -1
  113. package/src/util/entities.ts +3 -1
  114. package/src/util/entity_cache.ts +18 -0
  115. package/src/util/index.ts +1 -0
  116. package/src/util/navigation_from_path.ts +25 -2
  117. package/src/util/navigation_utils.ts +208 -2
  118. package/src/util/parent_references_from_path.ts +42 -3
  119. package/src/util/paths.ts +40 -3
  120. package/src/util/permissions.ts +22 -10
  121. package/src/util/previews.ts +8 -1
  122. package/src/util/resolutions.ts +8 -0
  123. package/src/util/urls.ts +52 -0
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",
@@ -7,6 +7,7 @@ import { Skeleton } from "@firecms/ui";
7
7
  import { ErrorBoundary, ErrorView } from "../../components";
8
8
  import { EntityPreview, EntityPreviewContainer } from "../../components/EntityPreview";
9
9
  import { jsonStringifyReplacer } from "../../util/objects";
10
+ import { entityCacheKey } from "../../util/entity_cache";
10
11
 
11
12
  export type ReferencePreviewProps = {
12
13
  disabled?: boolean;
@@ -53,7 +54,7 @@ function ReferencePreviewInternal({
53
54
 
54
55
  const navigationController = useNavigationController();
55
56
 
56
- const collection = navigationController.getCollection(reference.path);
57
+ const collection = navigationController.getCollection(reference.path, false, reference.pathSegments);
57
58
  if (!collection) {
58
59
  if (customizationController.components?.missingReference) {
59
60
  return <customizationController.components.missingReference path={reference.path}/>;
@@ -99,16 +100,19 @@ function ReferencePreviewExisting<M extends Record<string, any> = any>({
99
100
  dataLoadingError
100
101
  } = useEntityFetch({
101
102
  path: reference.path,
103
+ // References carry the segments of the collection they point at, when the code that
104
+ // built them knew them (see getReferenceFrom).
105
+ pathSegments: reference.pathSegments,
102
106
  entityId: reference.id,
103
107
  collection,
104
108
  useCache: true
105
109
  });
106
110
 
107
111
  if (entity) {
108
- referencesCache.set(reference.pathWithId, entity);
112
+ referencesCache.set(entityCacheKey(reference.path, reference.id), entity);
109
113
  }
110
114
 
111
- const usedEntity = entity ?? referencesCache.get(reference.pathWithId);
115
+ const usedEntity = entity ?? referencesCache.get(entityCacheKey(reference.path, reference.id));
112
116
 
113
117
  let body: React.ReactNode;
114
118
 
@@ -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"
@@ -90,7 +90,7 @@ export function FireCMSRoute() {
90
90
  let collection: EntityCollection<any> | undefined;
91
91
  collection = navigation.getCollectionById(navigationEntries[0].id);
92
92
  if (!collection)
93
- collection = navigation.getCollection(navigationEntries[0].path);
93
+ collection = navigation.getCollection(navigationEntries[0].path, false, navigationEntries[0].pathSegments);
94
94
  if (!collection)
95
95
  return null;
96
96
  return <React.Suspense fallback={null}>
@@ -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}
@@ -113,7 +114,7 @@ export function FireCMSRoute() {
113
114
  const firstEntry = navigationEntries[0] as NavigationViewCollectionInternal<any>;
114
115
  collection = navigation.getCollectionById(firstEntry.id);
115
116
  if (!collection)
116
- collection = navigation.getCollection(firstEntry.path);
117
+ collection = navigation.getCollection(firstEntry.path, false, firstEntry.pathSegments);
117
118
  if (!collection)
118
119
  return null;
119
120
  return <React.Suspense fallback={null}>
@@ -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)} />
@@ -184,6 +186,8 @@ function EntityFullScreenRoute({
184
186
  const urlTab = getSelectedTabFromUrl(isNew, lastCustomView);
185
187
  const [selectedTab, setSelectedTab] = useState<string | undefined>(urlTab);
186
188
 
189
+ // `navigationPath` comes from the URL, where entity ids are still escaped (see
190
+ // encodeEntityId), so splitting it on "/" inside is correct and no segments are needed.
187
191
  const parentCollectionIds = navigation.getParentCollectionIds(navigationPath);
188
192
  useEffect(() => {
189
193
  if (urlTab !== selectedTab) {
@@ -248,13 +252,17 @@ function EntityFullScreenRoute({
248
252
  // `fullIdPath` is used downstream to build URLs, so it carries the escaped chain;
249
253
  // `collectionPath` addresses the datasource, so it is resolved from the raw one.
250
254
  const fullIdPath = isNew ? lastCollectionEntry!.fullPath : (parentCollectionEntry?.fullPath ?? lastEntityEntry!.fullPath);
251
- const collectionPath = navigation.resolveIdsFrom(isNew ? lastCollectionEntry!.path : lastEntityEntry!.path);
255
+ // The navigation entries know the real segment boundaries, so the path is resolved
256
+ // from them rather than re-parsed out of the flattened string.
257
+ const pathSegments = isNew ? lastCollectionEntry!.pathSegments : lastEntityEntry!.pathSegments;
258
+ const collectionPath = navigation.resolveIdsFrom(isNew ? lastCollectionEntry!.path : lastEntityEntry!.path, pathSegments);
252
259
  return <>
253
260
  <React.Suspense fallback={null}>
254
261
  <EntityEditView
255
262
  key={collection.id + "_" + (isNew ? "new" : (isCopy ? entityId + "_copy" : entityId))}
256
263
  entityId={isNew ? undefined : entityId}
257
264
  fullIdPath={fullIdPath}
265
+ pathSegments={pathSegments}
258
266
  collection={collection}
259
267
  layout={"full_screen"}
260
268
  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`.
@@ -489,6 +497,14 @@ export interface CollectionActionsProps<M extends Record<string, any> = any, USE
489
497
  */
490
498
  path: string;
491
499
 
500
+ /**
501
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
502
+ *
503
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
504
+ * contain "/". Absent means "not known here", not "no slashes".
505
+ */
506
+ pathSegments?: string[];
507
+
492
508
  /**
493
509
  * Path of the last collection, like `addresses`
494
510
  */
@@ -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
@@ -230,6 +283,10 @@ export interface DataSource {
230
283
  initTextSearch?: (props: {
231
284
  context: FireCMSContext,
232
285
  path: string,
286
+ /**
287
+ * `path` split at its real segment boundaries, when known. Never derived from `path`.
288
+ */
289
+ pathSegments?: string[],
233
290
  collection: EntityCollection,
234
291
  parentCollectionIds?: string[]
235
292
  }) => Promise<boolean>;
@@ -237,6 +294,8 @@ export interface DataSource {
237
294
  }
238
295
 
239
296
  export type FilterCombinationValidProps = {
297
+ /** `path` split at its real segment boundaries — see `pathSegments` on FetchCollectionProps. */
298
+ pathSegments?: string[];
240
299
  path: string;
241
300
  collection: EntityCollection<any>;
242
301
  filterValues: FilterValues<any>;
@@ -366,12 +425,12 @@ export interface DataSourceDelegate {
366
425
  * @param collection
367
426
  * @return `true` if there are no other fields besides the given entity
368
427
  */
369
- checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
428
+ checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection, pathSegments?: string[]): Promise<boolean>;
370
429
 
371
430
  /**
372
431
  * Generate an id for a new entity
373
432
  */
374
- generateEntityId(path: string, collection?: EntityCollection): string;
433
+ generateEntityId(path: string, collection?: EntityCollection, pathSegments?: string[]): string;
375
434
 
376
435
  /**
377
436
  * Count the number of entities in a collection
@@ -398,6 +457,10 @@ export interface DataSourceDelegate {
398
457
  initTextSearch?: (props: {
399
458
  context: FireCMSContext,
400
459
  path: string,
460
+ /**
461
+ * `path` split at its real segment boundaries, when known. Never derived from `path`.
462
+ */
463
+ pathSegments?: string[],
401
464
  databaseId?: string,
402
465
  collection: EntityCollection,
403
466
  parentCollectionIds?: string[]
@@ -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() {
@@ -65,6 +65,12 @@ export type EntityActionClickProps<M extends object, USER extends User = User> =
65
65
  context: FireCMSContext<USER>;
66
66
 
67
67
  fullPath?: string;
68
+ /**
69
+ * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`,
70
+ * when the caller knows them. Absent means "not known here" — never derived from
71
+ * `fullPath`, since a parent id may itself contain "/".
72
+ */
73
+ pathSegments?: string[];
68
74
  fullIdPath?: string;
69
75
  collection?: EntityCollection<M>;
70
76
 
@@ -86,6 +86,14 @@ export interface EntityOnFetchProps<M extends Record<string, any> = any, USER ex
86
86
  */
87
87
  path: string;
88
88
 
89
+ /**
90
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
91
+ *
92
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
93
+ * contain "/". Absent means "not known here", not "no slashes".
94
+ */
95
+ pathSegments?: string[];
96
+
89
97
  /**
90
98
  * Fetched entity
91
99
  */
@@ -133,6 +141,14 @@ export interface EntityOnSaveProps<M extends Record<string, any> = any, USER ext
133
141
  */
134
142
  path: string;
135
143
 
144
+ /**
145
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
146
+ *
147
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
148
+ * contain "/". Absent means "not known here", not "no slashes".
149
+ */
150
+ pathSegments?: string[];
151
+
136
152
  /**
137
153
  * Full path where this entity is being saved, with alias resolved
138
154
  */
@@ -180,6 +196,14 @@ export interface EntityOnDeleteProps<M extends Record<string, any> = any, USER e
180
196
  */
181
197
  path: string;
182
198
 
199
+ /**
200
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
201
+ *
202
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
203
+ * contain "/". Absent means "not known here", not "no slashes".
204
+ */
205
+ pathSegments?: string[];
206
+
183
207
  /**
184
208
  * Deleted entity id
185
209
  */
@@ -57,8 +57,12 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
57
57
  /**
58
58
  * Get the collection configuration for a given path.
59
59
  * The collection is resolved from the given path or alias.
60
+ *
61
+ * Pass `pathSegments` when you have them: without them the path is split on "/" and
62
+ * asserted to have an odd number of parts, which a chain containing a slash-bearing
63
+ * entity id fails — it throws instead of resolving.
60
64
  */
61
- getCollection: (pathOrId: string, includeUserOverride?: boolean) => EC | undefined;
65
+ getCollection: (pathOrId: string, includeUserOverride?: boolean, pathSegments?: string[]) => EC | undefined;
62
66
 
63
67
  /**
64
68
  * Get the top level collection configuration for a given id
@@ -118,9 +122,30 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
118
122
  /**
119
123
  * Turn a path with collection ids into a resolved path.
120
124
  * The ids (typically used in urls) will be replaced with relative paths (typically used in database paths)
125
+ *
126
+ * Pass `pathSegments` whenever you have them. Without them this has to find the entity
127
+ * ids inside the flattened string by reading up to the next "/", which is wrong for any
128
+ * id that contains one — the chain then shifts by a segment and resolution fails.
129
+ *
121
130
  * @param pathWithAliases
131
+ * @param pathSegments `pathWithAliases` split at its real segment boundaries, if known
122
132
  */
123
- resolveIdsFrom: (pathWithAliases: string) => string;
133
+ resolveIdsFrom: (pathWithAliases: string, pathSegments?: string[]) => string;
134
+
135
+ /**
136
+ * The segment-wise counterpart of {@link resolveIdsFrom}: replaces collection ids with
137
+ * their real paths while carrying every entity id through whole, slashes included.
138
+ *
139
+ * Idempotent — segments that are already resolved pass through unchanged.
140
+ *
141
+ * Optional so that a `navigationController` built against an earlier version keeps
142
+ * working: callers fall back to using the segments as given, which is what happened
143
+ * before this method existed. Controllers built by `useBuildNavigationController`
144
+ * always provide it.
145
+ *
146
+ * @param pathSegments
147
+ */
148
+ resolveSegmentsFrom?: (pathSegments: string[]) => string[];
124
149
 
125
150
  /**
126
151
  * Call this method to recalculate the navigation
@@ -128,16 +153,21 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
128
153
  refreshNavigation: () => void;
129
154
 
130
155
  /**
131
- * Retrieve all the related parent references for a given path
156
+ * Retrieve all the related parent references for a given path.
157
+ *
158
+ * `path` is expected escaped, as it comes from the URL. Pass `pathSegments` when the
159
+ * path at hand is a raw datasource path, whose ids may contain a bare "/".
160
+ *
132
161
  * @param path
162
+ * @param pathSegments `path` split at its real segment boundaries, with raw ids
133
163
  */
134
- getParentReferencesFromPath: (path: string) => EntityReference[];
164
+ getParentReferencesFromPath: (path: string, pathSegments?: string[]) => EntityReference[];
135
165
 
136
166
  /**
137
167
  * Retrieve all the related parent collection ids for a given path
138
168
  * @param path
139
169
  */
140
- getParentCollectionIds: (path: string) => string[];
170
+ getParentCollectionIds: (path: string, pathSegments?: string[]) => string[];
141
171
 
142
172
  /**
143
173
  * Resolve paths from a list of ids