@firecms/core 3.3.0-canary.d3242eb → 3.4.0-canary.00f9be5

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 (221) hide show
  1. package/README.md +4 -0
  2. package/dist/app/useApp.d.ts +1 -0
  3. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  4. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  5. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  8. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  9. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  10. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  11. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  12. package/dist/components/EntityPreview.d.ts +3 -1
  13. package/dist/components/ErrorBoundary.d.ts +1 -3
  14. package/dist/components/LicenseBanner.d.ts +23 -0
  15. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  16. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  17. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  18. package/dist/components/common/table_url_params.d.ts +24 -0
  19. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  20. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  23. package/dist/core/DefaultDrawer.d.ts +10 -3
  24. package/dist/core/EntityEditView.d.ts +9 -1
  25. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  26. package/dist/core/field_configs.d.ts +1 -1
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/plugin_translations.d.ts +38 -0
  29. package/dist/core/pro_plugins.d.ts +40 -0
  30. package/dist/de-BF5vcQPY.js +703 -0
  31. package/dist/de-BF5vcQPY.js.map +1 -0
  32. package/dist/editor/extensions/Image.d.ts +11 -0
  33. package/dist/editor/index.d.ts +2 -1
  34. package/dist/editor/lazy_editor.d.ts +6 -0
  35. package/dist/editor-BSo_aS1I.js +4563 -0
  36. package/dist/editor-BSo_aS1I.js.map +1 -0
  37. package/dist/form/EntityForm.d.ts +8 -1
  38. package/dist/form/EntityFormActions.d.ts +2 -0
  39. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  40. package/dist/form/index.d.ts +1 -0
  41. package/dist/fr-DH9_wyFI.js +703 -0
  42. package/dist/fr-DH9_wyFI.js.map +1 -0
  43. package/dist/hi-BM8dm1Lu.js +703 -0
  44. package/dist/hi-BM8dm1Lu.js.map +1 -0
  45. package/dist/hooks/data/delete.d.ts +1 -1
  46. package/dist/hooks/data/save.d.ts +1 -1
  47. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  48. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  49. package/dist/hooks/index.d.ts +2 -0
  50. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  51. package/dist/hooks/useProjectLog.d.ts +11 -6
  52. package/dist/hooks/useTranslation.d.ts +1 -1
  53. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  54. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  55. package/dist/index.es.js +4429 -10479
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/index.umd.js +9313 -6582
  58. package/dist/index.umd.js.map +1 -1
  59. package/dist/it-D3K1gfe4.js +703 -0
  60. package/dist/it-D3K1gfe4.js.map +1 -0
  61. package/dist/locales/pl.d.ts +2 -0
  62. package/dist/pl-CP1zeLzn.js +705 -0
  63. package/dist/pl-CP1zeLzn.js.map +1 -0
  64. package/dist/preview/index.d.ts +1 -0
  65. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  66. package/dist/pt-Cus7eKQR.js +705 -0
  67. package/dist/pt-Cus7eKQR.js.map +1 -0
  68. package/dist/types/analytics.d.ts +1 -1
  69. package/dist/types/collections.d.ts +31 -0
  70. package/dist/types/datasource.d.ts +67 -4
  71. package/dist/types/entities.d.ts +21 -1
  72. package/dist/types/entity_actions.d.ts +6 -0
  73. package/dist/types/entity_callbacks.d.ts +21 -0
  74. package/dist/types/firecms.d.ts +18 -1
  75. package/dist/types/firecms_context.d.ts +8 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/license.d.ts +55 -0
  78. package/dist/types/navigation.d.ts +34 -5
  79. package/dist/types/plugins.d.ts +22 -1
  80. package/dist/types/properties.d.ts +17 -1
  81. package/dist/types/side_entity_controller.d.ts +7 -0
  82. package/dist/types/translations.d.ts +54 -5
  83. package/dist/util/__tests__/collections.test.d.ts +1 -0
  84. package/dist/util/__tests__/urls.test.d.ts +1 -0
  85. package/dist/util/collections.d.ts +1 -1
  86. package/dist/util/date_fns_locales.d.ts +18 -0
  87. package/dist/util/entities.d.ts +1 -0
  88. package/dist/util/entity_cache.d.ts +11 -0
  89. package/dist/util/geopoint.d.ts +22 -0
  90. package/dist/util/index.d.ts +3 -0
  91. package/dist/util/lazy_eager.d.ts +3 -3
  92. package/dist/util/navigation_blocking.d.ts +22 -0
  93. package/dist/util/navigation_from_path.d.ts +17 -0
  94. package/dist/util/navigation_utils.d.ts +107 -3
  95. package/dist/util/parent_references_from_path.d.ts +16 -0
  96. package/dist/util/paths.d.ts +28 -0
  97. package/dist/util/permissions.d.ts +10 -4
  98. package/dist/util/urls.d.ts +22 -0
  99. package/package.json +17 -11
  100. package/src/app/Scaffold.tsx +54 -45
  101. package/src/app/useApp.tsx +1 -0
  102. package/src/components/DeleteEntityDialog.tsx +2 -0
  103. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  104. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  105. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  106. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  107. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  108. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  109. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  110. package/src/components/EntityCollectionView/EntityCollectionView.tsx +71 -17
  111. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  112. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
  113. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  114. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  115. package/src/components/EntityPreview.tsx +45 -41
  116. package/src/components/ErrorBoundary.tsx +12 -18
  117. package/src/components/HomePage/NavigationCardBinding.tsx +6 -3
  118. package/src/components/LicenseBanner.tsx +207 -0
  119. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  120. package/src/components/ReferenceWidget.tsx +1 -1
  121. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  122. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  123. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  124. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  125. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  126. package/src/components/common/default_entity_actions.tsx +22 -2
  127. package/src/components/common/table_url_params.ts +172 -0
  128. package/src/components/common/useDataSourceTableController.tsx +126 -163
  129. package/src/components/common/useTableSearchHelper.ts +26 -2
  130. package/src/components/index.tsx +1 -0
  131. package/src/contexts/LicenseStatusContext.tsx +50 -0
  132. package/src/contexts/SnackbarProvider.tsx +14 -1
  133. package/src/core/DefaultAppBar.tsx +28 -16
  134. package/src/core/DefaultDrawer.tsx +151 -65
  135. package/src/core/DrawerNavigationGroup.tsx +27 -29
  136. package/src/core/DrawerNavigationItem.tsx +10 -12
  137. package/src/core/EntityEditView.tsx +77 -35
  138. package/src/core/EntityEditViewFormActions.tsx +3 -2
  139. package/src/core/EntitySidePanel.tsx +8 -4
  140. package/src/core/FireCMS.tsx +73 -47
  141. package/src/core/SideDialogs.tsx +44 -1
  142. package/src/core/field_configs.tsx +15 -0
  143. package/src/core/index.tsx +1 -0
  144. package/src/core/plugin_translations.ts +87 -0
  145. package/src/core/pro_plugins.ts +79 -0
  146. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  147. package/src/editor/extensions/Image.ts +17 -1
  148. package/src/editor/hooks/useProseMirror.ts +5 -0
  149. package/src/editor/index.ts +11 -1
  150. package/src/editor/lazy_editor.tsx +33 -0
  151. package/src/form/EntityForm.tsx +31 -10
  152. package/src/form/EntityFormActions.tsx +2 -0
  153. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  154. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  155. package/src/form/index.tsx +1 -0
  156. package/src/hooks/data/delete.ts +8 -0
  157. package/src/hooks/data/save.ts +6 -1
  158. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  159. package/src/hooks/data/useEntityFetch.tsx +35 -4
  160. package/src/hooks/index.tsx +2 -0
  161. package/src/hooks/useBuildNavigationController.tsx +75 -16
  162. package/src/hooks/useFireCMSContext.tsx +7 -3
  163. package/src/hooks/useLicenseStatus.tsx +32 -0
  164. package/src/hooks/useProjectLog.tsx +27 -9
  165. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  166. package/src/hooks/useTranslation.ts +3 -1
  167. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  168. package/src/i18n/FireCMSi18nProvider.tsx +199 -23
  169. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  170. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  171. package/src/internal/useBuildDataSource.ts +37 -15
  172. package/src/internal/useBuildSideEntityController.tsx +20 -5
  173. package/src/locales/de.ts +37 -7
  174. package/src/locales/en.ts +41 -9
  175. package/src/locales/es.ts +37 -7
  176. package/src/locales/fr.ts +37 -7
  177. package/src/locales/hi.ts +37 -7
  178. package/src/locales/it.ts +37 -7
  179. package/src/locales/pl.ts +752 -0
  180. package/src/locales/pt.ts +37 -7
  181. package/src/preview/PropertyPreview.tsx +12 -0
  182. package/src/preview/components/DatePreview.tsx +2 -4
  183. package/src/preview/components/ReferencePreview.tsx +7 -3
  184. package/src/preview/components/StorageThumbnail.tsx +24 -2
  185. package/src/preview/index.ts +1 -0
  186. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  187. package/src/routes/FireCMSRoute.tsx +54 -27
  188. package/src/types/analytics.ts +3 -0
  189. package/src/types/collections.ts +34 -0
  190. package/src/types/datasource.ts +67 -4
  191. package/src/types/entities.ts +24 -1
  192. package/src/types/entity_actions.tsx +6 -0
  193. package/src/types/entity_callbacks.ts +24 -0
  194. package/src/types/firecms.tsx +19 -1
  195. package/src/types/firecms_context.tsx +9 -0
  196. package/src/types/index.ts +1 -0
  197. package/src/types/license.ts +63 -0
  198. package/src/types/navigation.ts +35 -5
  199. package/src/types/plugins.tsx +23 -1
  200. package/src/types/properties.ts +17 -0
  201. package/src/types/side_entity_controller.tsx +7 -0
  202. package/src/types/translations.ts +54 -5
  203. package/src/util/__tests__/collections.test.ts +32 -0
  204. package/src/util/__tests__/urls.test.ts +131 -0
  205. package/src/util/collections.ts +1 -1
  206. package/src/util/date_fns_locales.ts +168 -0
  207. package/src/util/entities.ts +16 -1
  208. package/src/util/entity_cache.ts +18 -0
  209. package/src/util/geopoint.ts +81 -0
  210. package/src/util/index.ts +3 -0
  211. package/src/util/lazy_eager.tsx +30 -8
  212. package/src/util/navigation_blocking.ts +45 -0
  213. package/src/util/navigation_from_path.ts +47 -7
  214. package/src/util/navigation_utils.ts +250 -5
  215. package/src/util/parent_references_from_path.ts +45 -4
  216. package/src/util/paths.ts +40 -3
  217. package/src/util/permissions.ts +22 -10
  218. package/src/util/previews.ts +8 -1
  219. package/src/util/resolutions.ts +11 -0
  220. package/src/util/urls.ts +52 -0
  221. package/src/util/useStorageUploadController.tsx +6 -1
package/src/locales/pt.ts CHANGED
@@ -70,8 +70,19 @@ export const pt: FireCMSTranslations = {
70
70
  select_from: "Selecionar de {{name}}",
71
71
  done: "Concluído",
72
72
  log_out: "Terminar sessão",
73
- license_needed: "Licença necessária",
74
- license_description: "Precisa de uma licença válida para usar o FireCMS PRO. Por favor contacte {{email}} para mais informações.",
73
+ license_trial_banner: "Avaliação do FireCMS PRO: faltam {{days}} dias em produção.",
74
+ license_trial_banner_one: "Avaliação do FireCMS PRO: falta {{days}} dia em produção.",
75
+ license_expired_banner: "A sua avaliação PRO terminou a {{date}}. As funcionalidades PRO, como o editor de esquemas, a importação/exportação e o histórico, estão em pausa; os seus dados e coleções continuam a funcionar.",
76
+ license_expired_banner_undated: "A sua avaliação PRO terminou. As funcionalidades PRO, como o editor de esquemas, a importação/exportação e o histórico, estão em pausa; os seus dados e coleções continuam a funcionar.",
77
+ license_invalid_project_banner: "Esta chave de licença não está associada ao projeto {{projectId}}. As funcionalidades PRO ficam em pausa até que esteja.",
78
+ license_invalid_project_banner_unnamed: "Esta chave de licença não está associada a este projeto. As funcionalidades PRO ficam em pausa até que esteja.",
79
+ license_over_quota_banner: "A licença deste projeto cobre {{licensed}} projetos em produção e este não é um deles. As funcionalidades PRO, como o editor de esquemas, a importação/exportação e o histórico, estão em pausa; os seus dados e coleções continuam a funcionar.",
80
+ license_over_quota_banner_one: "A licença deste projeto cobre {{licensed}} projeto em produção e é outro. As funcionalidades PRO, como o editor de esquemas, a importação/exportação e o histórico, estão em pausa; os seus dados e coleções continuam a funcionar.",
81
+ license_get_license: "Obter uma licença",
82
+ license_update_license: "Atualizar a sua licença",
83
+ license_add_project_to_license: "Adicioná-lo à sua licença",
84
+ pro_features_paused: "Funcionalidades PRO em pausa",
85
+ user_management_paused_description: "A gestão de utilizadores e funções faz parte do FireCMS PRO. Os seus utilizadores e funções atuais continuam a decidir quem pode iniciar sessão e o que pode fazer, e poderá voltar a editá-los assim que este projeto tiver uma licença.",
75
86
  column_cannot_be_edited: "Esta coluna não pode ser editada diretamente",
76
87
  close: "Fechar",
77
88
  unsaved_local_changes: "Alterações locais não guardadas",
@@ -151,8 +162,8 @@ export const pt: FireCMSTranslations = {
151
162
  // ─── Error states ─────────────────────────────────────────────
152
163
  error: "Erro",
153
164
  error_uploading_file: "Erro ao carregar ficheiro",
154
- error_deleting: "Erro ao eliminar",
155
- error_before_delete: "Erro antes de eliminar",
165
+ error_deleting: "Erro ao eliminar: {{message}}",
166
+ error_before_delete: "Erro antes de eliminar: {{message}}",
156
167
  error_updating_asset: "Erro ao atualizar recurso",
157
168
  error_deleting_asset: "Erro ao eliminar recurso",
158
169
  error_firestore_index: "É necessário um índice Firestore para esta consulta.",
@@ -394,6 +405,10 @@ export const pt: FireCMSTranslations = {
394
405
  download: "Descarregar",
395
406
  large_number_of_documents: "Esta coleção tem um grande número de documentos ({{count}}).",
396
407
  include_undefined_values: "Incluir valores indefinidos",
408
+ export_apply_filter_sort: "Exportar apenas os resultados que correspondem ao filtro/ordenação atual",
409
+ export_all_entities: "Todas as entidades",
410
+ export_selected_entities: "Entidades selecionadas ({{count}})",
411
+ export_error: "Não foi possível exportar os dados: {{message}}",
397
412
  submit: "Submeter",
398
413
 
399
414
  no_filterable_properties: "Não há propriedades filtráveis disponíveis",
@@ -442,8 +457,9 @@ export const pt: FireCMSTranslations = {
442
457
  cms_users: "Utilizadores do CMS",
443
458
  roles_menu: "Funções",
444
459
  project_settings: "Definições do projeto",
445
- firestore_explorer: "Explorador do Firestore",
446
- explore_your_firestore_data: "Explore os seus dados do Firestore",
460
+ firestore_manager: "Gerenciador do Firestore",
461
+ datatalk: "DataTalk",
462
+ manage_your_firestore_data: "Gerencie os seus dados do Firestore",
447
463
 
448
464
  // ─── FireCMS Cloud Login ──────────────────────────────────────
449
465
  build_admin_panel_in_minutes: "Construa o Seu Painel de Administração Firebase em Minutos",
@@ -459,6 +475,7 @@ export const pt: FireCMSTranslations = {
459
475
  not_the_admin_panel: "não no painel de administração.",
460
476
  join_our_newsletter: "Subscreva a nossa newsletter. Sem spam, apenas atualizações importantes!",
461
477
  by_signing_in_you_agree_to_our: "Ao iniciar sessão, concorda com os nossos",
478
+ please_accept_the_terms_to_continue: "Aceite os termos e condições para continuar",
462
479
  terms_and_conditions: "Termos e Condições",
463
480
  and_our: "e a nossa",
464
481
  privacy_policy: "Política de privacidade",
@@ -501,6 +518,7 @@ export const pt: FireCMSTranslations = {
501
518
  licensed_projects_lowercase: "projetos licenciados",
502
519
  manage: "Gerir",
503
520
  update_payment_method: "Atualizar método de pagamento",
521
+ stripe_portal_error: "Não foi possível abrir o Stripe: {{message}}",
504
522
  your_firecms_cloud_projects: "Os seus projetos FireCMS Cloud",
505
523
  status_active: "Ativo",
506
524
  status_trialing: "Em avaliação",
@@ -519,10 +537,18 @@ export const pt: FireCMSTranslations = {
519
537
  auto_setup_collections_button: "Configurar coleções automaticamente",
520
538
  auto_setup_collections_title: "Configurar coleções automaticamente?",
521
539
  auto_setup_collections_desc: "Isto criará automaticamente configurações de coleção para coleções que <b>NÃO</b> estão já mapeadas",
522
- this_can_take_a_minute: "Isto pode demorar um ou dois minutos",
540
+ setting_up_collections: "Configurando coleções",
541
+ setting_up_collection: "Configurando {{name}}",
523
542
  no_collections_found_to_setup: "Nenhuma coleção encontrada para configurar.",
524
543
  collections_have_been_setup: "As coleções foram configuradas automaticamente.",
525
544
  error_setting_up_collections: "Erro ao configurar coleções automaticamente",
545
+ setup_collections_title: "Set up collections",
546
+ setup_collections_select_desc: "Select which collections to automatically set up:",
547
+ select_all: "Select all",
548
+ deselect_all: "Deselect all",
549
+ setup_collections_confirm: "Set up ({{count}})",
550
+ collection_setup_success: "{{name}} has been set up",
551
+ go_to_collection: "Go to collection",
526
552
 
527
553
  // --- Home Suggestions ---
528
554
  add_your: "Adicione as suas",
@@ -587,6 +613,10 @@ export const pt: FireCMSTranslations = {
587
613
  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.",
588
614
  no_number_properties: "Nenhuma propriedade numérica encontrada. Adicione uma propriedade numérica para habilitar a ordenação.",
589
615
  order_property_description: "Selecione uma propriedade numérica para manter a ordem dos itens",
616
+ image_property: "Propriedade de imagem",
617
+ 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.",
618
+ no_image_properties: "Nenhuma propriedade de imagem encontrada. Adicione uma propriedade de armazenamento ou URL de imagem para usar como miniatura.",
619
+ 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.",
590
620
  display_settings: "Configurações de exibição",
591
621
  default_row_size: "Tamanho de linha padrão",
592
622
  side_dialog_width: "Largura do diálogo lateral",
@@ -4,6 +4,7 @@ import equal from "react-fast-compare"
4
4
  import {
5
5
  CMSType,
6
6
  EntityReference,
7
+ GeoPoint,
7
8
  ResolvedArrayProperty,
8
9
  ResolvedMapProperty,
9
10
  ResolvedNumberProperty,
@@ -27,12 +28,14 @@ import { ArrayPropertyEnumPreview } from "./property_previews/ArrayPropertyEnumP
27
28
  import { ArrayOfStringsPreview } from "./property_previews/ArrayOfStringsPreview";
28
29
  import { ArrayOneOfPreview } from "./property_previews/ArrayOneOfPreview";
29
30
  import { MapPropertyPreview } from "./property_previews/MapPropertyPreview";
31
+ import { GeopointPropertyPreview } from "./property_previews/GeopointPropertyPreview";
30
32
  import { ReferencePreview } from "./components/ReferencePreview";
31
33
  import { DatePreview } from "./components/DatePreview";
32
34
  import { BooleanPreview } from "./components/BooleanPreview";
33
35
  import { NumberPropertyPreview } from "./property_previews/NumberPropertyPreview";
34
36
  import { ErrorView } from "../components";
35
37
  import { UserPreview } from "./components/UserPreview";
38
+ import { getGeoPointCoordinates } from "../util";
36
39
 
37
40
  /**
38
41
  * @group Preview components
@@ -202,6 +205,15 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
202
205
  } else {
203
206
  content = buildWrongValueType(propertyKey, property.dataType, value);
204
207
  }
208
+ } else if (property.dataType === "geopoint") {
209
+ const coordinates = getGeoPointCoordinates(value as GeoPoint);
210
+ if (coordinates) {
211
+ content = <GeopointPropertyPreview {...props}
212
+ property={property}
213
+ value={value as GeoPoint}/>;
214
+ } else {
215
+ content = buildWrongValueType(propertyKey, property.dataType, value);
216
+ }
205
217
  } else if (property.dataType === "reference") {
206
218
  if (typeof property.path === "string") {
207
219
  if (typeof value === "object" && "isEntityReference" in value && value.isEntityReference()) {
@@ -1,9 +1,8 @@
1
1
  import React from "react";
2
2
 
3
3
  import { format } from "date-fns";
4
- import * as locales from "date-fns/locale";
5
4
  import { useCustomizationController } from "../../hooks";
6
- import { defaultDateFormat } from "../../util";
5
+ import { defaultDateFormat, useDateFnsLocale } from "../../util";
7
6
 
8
7
  export interface DatePreviewProps {
9
8
  date: Date;
@@ -28,8 +27,7 @@ export function DatePreview({
28
27
  }: DatePreviewProps): React.ReactElement {
29
28
 
30
29
  const customizationController = useCustomizationController();
31
- // @ts-ignore
32
- const dateUtilsLocale = customizationController?.locale ? locales[customizationController?.locale] : undefined;
30
+ const dateUtilsLocale = useDateFnsLocale(customizationController?.locale);
33
31
 
34
32
  if (!date) {
35
33
  return <></>;
@@ -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"
@@ -7,6 +7,7 @@ export * from "./property_previews/ArrayOfStringsPreview";
7
7
  export * from "./property_previews/ArrayPropertyEnumPreview";
8
8
  export * from "./property_previews/ArrayOfMapsPreview";
9
9
  export * from "./property_previews/NumberPropertyPreview";
10
+ export * from "./property_previews/GeopointPropertyPreview";
10
11
  export * from "./property_previews/StringPropertyPreview";
11
12
  export * from "./property_previews/ArrayOfStorageComponentsPreview";
12
13
  export * from "./property_previews/ArrayOfReferencesPreview";
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+
3
+ import { GeoPoint } from "../../types";
4
+ import { PropertyPreviewProps } from "../PropertyPreviewProps";
5
+ import { formatGeoPoint, getGeoPointCoordinates } from "../../util";
6
+
7
+ export function GeopointPropertyPreview({
8
+ value,
9
+ size
10
+ }: PropertyPreviewProps<GeoPoint>): React.ReactElement {
11
+
12
+ const coordinates = getGeoPointCoordinates(value);
13
+
14
+ if (!coordinates) {
15
+ return <span className={size === "small" ? "text-sm text-text-secondary dark:text-text-secondary-dark" : "text-text-secondary dark:text-text-secondary-dark"}>—</span>;
16
+ }
17
+
18
+ return (
19
+ <span className={size === "small" ? "text-sm font-mono" : "font-mono"}>
20
+ {formatGeoPoint(coordinates)}
21
+ </span>
22
+ );
23
+ }
@@ -10,6 +10,8 @@ import {
10
10
  } from "../util/navigation_from_path";
11
11
  import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
12
12
  import { toArray } from "../util/arrays";
13
+ import { addInitialSlash, encodeEntityId } from "../util/navigation_utils";
14
+ import { shouldBlockEntityNavigation } from "../util/navigation_blocking";
13
15
  import { NotFoundPage } from "../components";
14
16
  import { lazyEager } from "../util/lazy_eager";
15
17
 
@@ -88,7 +90,7 @@ export function FireCMSRoute() {
88
90
  let collection: EntityCollection<any> | undefined;
89
91
  collection = navigation.getCollectionById(navigationEntries[0].id);
90
92
  if (!collection)
91
- collection = navigation.getCollection(navigationEntries[0].path);
93
+ collection = navigation.getCollection(navigationEntries[0].path, false, navigationEntries[0].pathSegments);
92
94
  if (!collection)
93
95
  return null;
94
96
  return <React.Suspense fallback={null}>
@@ -97,6 +99,7 @@ export function FireCMSRoute() {
97
99
  isSubCollection={false}
98
100
  parentCollectionIds={[]}
99
101
  fullPath={collection.path}
102
+ pathSegments={navigationEntries[0].pathSegments}
100
103
  fullIdPath={collection.id}
101
104
  updateUrl={true}
102
105
  {...collection}
@@ -111,7 +114,7 @@ export function FireCMSRoute() {
111
114
  const firstEntry = navigationEntries[0] as NavigationViewCollectionInternal<any>;
112
115
  collection = navigation.getCollectionById(firstEntry.id);
113
116
  if (!collection)
114
- collection = navigation.getCollection(firstEntry.path);
117
+ collection = navigation.getCollection(firstEntry.path, false, firstEntry.pathSegments);
115
118
  if (!collection)
116
119
  return null;
117
120
  return <React.Suspense fallback={null}>
@@ -121,6 +124,7 @@ export function FireCMSRoute() {
121
124
  isSubCollection={false}
122
125
  parentCollectionIds={[]}
123
126
  fullPath={collection.path}
127
+ pathSegments={firstEntry.pathSegments}
124
128
  updateUrl={true}
125
129
  {...collection}
126
130
  Actions={toArray(collection.Actions)} />
@@ -182,6 +186,8 @@ function EntityFullScreenRoute({
182
186
  const urlTab = getSelectedTabFromUrl(isNew, lastCustomView);
183
187
  const [selectedTab, setSelectedTab] = useState<string | undefined>(urlTab);
184
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.
185
191
  const parentCollectionIds = navigation.getParentCollectionIds(navigationPath);
186
192
  useEffect(() => {
187
193
  if (urlTab !== selectedTab) {
@@ -189,27 +195,51 @@ function EntityFullScreenRoute({
189
195
  }
190
196
  }, [urlTab]);
191
197
 
192
- const basePath = !entityId || isNew
198
+ const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
199
+
200
+ // The entity id reaches the URL escaped AND percent-encoded, so it cannot be located by
201
+ // searching `pathname` for the raw id — that misses for any id needing encoding (a "/",
202
+ // but equally a space or a non-ASCII character). Rebuild the URLs from the navigation
203
+ // entries instead, which already carry the escaped chain in `fullPath`.
204
+ const entityEntryIndex = lastEntityEntry ? navigationEntries.indexOf(lastEntityEntry) : -1;
205
+ const parentCollectionEntry = entityEntryIndex > 0
206
+ ? navigationEntries[entityEntryIndex - 1] as NavigationViewCollectionInternal<any>
207
+ : undefined;
208
+
209
+ const buildUrl = (escapedPath: string) => addInitialSlash(navigation.buildUrlCollectionPath(escapedPath));
210
+
211
+ const basePath = !entityId || isNew || !parentCollectionEntry
193
212
  ? pathname
194
- : pathname.substring(0, pathname.lastIndexOf(`/${entityId}`));
213
+ : buildUrl(parentCollectionEntry.fullPath);
214
+
215
+ const entityPath = lastEntityEntry ? buildUrl(lastEntityEntry.fullPath) : basePath;
195
216
 
196
- const entityPath = basePath + `/${entityId}`;
217
+ /**
218
+ * Build the URL of an entity in the current collection. `id` is raw, so it is escaped
219
+ * before being joined; `buildUrlCollectionPath` then percent-encodes the whole path.
220
+ */
221
+ const buildEntityUrl = (id: string, tab?: string) => {
222
+ const parentPath = parentCollectionEntry?.fullPath ?? lastCollectionEntry?.fullPath;
223
+ if (!parentPath) return pathname;
224
+ return buildUrl(`${parentPath}/${encodeEntityId(id)}${tab ? "/" + tab : ""}`);
225
+ };
197
226
 
198
227
  let blocker: Blocker | undefined = undefined;
199
228
  try {
200
229
  blocker = useBlocker(({
230
+ currentLocation,
201
231
  nextLocation
202
- }) => {
203
- if (nextLocation.pathname.startsWith(entityPath))
204
- return false;
205
- return blocked.current;
206
- });
232
+ }) => shouldBlockEntityNavigation({
233
+ currentLocation,
234
+ nextLocation,
235
+ entityPath,
236
+ basePath,
237
+ blocked: blocked.current
238
+ }));
207
239
  } catch (e) {
208
240
  // console.warn("Blocker not available, navigation will not be blocked");
209
241
  }
210
242
 
211
- const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
212
-
213
243
  if (isNew && !lastCollectionEntry) {
214
244
  throw new Error("INTERNAL: No collection found in the navigation");
215
245
  }
@@ -219,14 +249,20 @@ function EntityFullScreenRoute({
219
249
  }
220
250
 
221
251
  const collection = isNew ? lastCollectionEntry!.collection : lastEntityEntry!.parentCollection;
222
- const fullIdPath = isNew ? lastCollectionEntry!.path : lastEntityEntry!.path;
223
- const collectionPath = navigation.resolveIdsFrom(fullIdPath);
252
+ // `fullIdPath` is used downstream to build URLs, so it carries the escaped chain;
253
+ // `collectionPath` addresses the datasource, so it is resolved from the raw one.
254
+ const fullIdPath = isNew ? lastCollectionEntry!.fullPath : (parentCollectionEntry?.fullPath ?? lastEntityEntry!.fullPath);
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);
224
259
  return <>
225
260
  <React.Suspense fallback={null}>
226
261
  <EntityEditView
227
262
  key={collection.id + "_" + (isNew ? "new" : (isCopy ? entityId + "_copy" : entityId))}
228
263
  entityId={isNew ? undefined : entityId}
229
264
  fullIdPath={fullIdPath}
265
+ pathSegments={pathSegments}
230
266
  collection={collection}
231
267
  layout={"full_screen"}
232
268
  path={collectionPath}
@@ -234,25 +270,16 @@ function EntityFullScreenRoute({
234
270
  selectedTab={selectedTab ?? undefined}
235
271
  onValuesModified={(modified) => blocked.current = modified}
236
272
  onSaved={(params) => {
237
- const newSelectedTab = params.selectedTab;
238
273
  const newEntityId = params.entityId;
239
- if (newSelectedTab) {
240
- navigate(`${basePath}/${newEntityId}/${newSelectedTab}`, { replace: true });
241
- } else {
242
- navigate(`${basePath}/${newEntityId}`, { replace: true });
243
- }
274
+ if (!newEntityId) return;
275
+ navigate(buildEntityUrl(newEntityId, params.selectedTab), { replace: true });
244
276
  }}
245
277
  onTabChange={(params) => {
246
278
  setSelectedTab(params.selectedTab);
247
- if (isNew) {
279
+ if (isNew || !entityId) {
248
280
  return;
249
281
  }
250
- const newSelectedTab = params.selectedTab;
251
- if (newSelectedTab) {
252
- navigate(`${basePath}/${entityId}/${newSelectedTab}`, { replace: true });
253
- } else {
254
- navigate(`${basePath}/${entityId}`, { replace: true });
255
- }
282
+ navigate(buildEntityUrl(entityId, params.selectedTab), { replace: true });
256
283
  }}
257
284
  parentCollectionIds={parentCollectionIds}
258
285
  />
@@ -44,5 +44,8 @@ export type CMSAnalyticsEvent =
44
44
 
45
45
  | "card_view_entity_click"
46
46
 
47
+ | "entity_history_opened"
48
+ | "entity_history_reverted"
49
+
47
50
  | "unmapped_event"
48
51
  ;
@@ -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`.
@@ -261,6 +269,22 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
261
269
  */
262
270
  initialFilter?: FilterValues<Extract<keyof M, string>>; // setting FilterValues<M> can break defining collections by code
263
271
 
272
+ /**
273
+ * Array of property keys that are allowed for filtering. Allowed filters will be displayed in the collection view, table row headers
274
+ * and will be used to filter data.
275
+ *
276
+ * If not specified, all properties that are filterable will be allowed.
277
+ *
278
+ * If you specify this prop, the filters will be displayed in the collection view if they are filterable.
279
+ *
280
+ * If you set it as an empty array, no filters will be displayed.
281
+ *
282
+ * e.g. `allowedFilters: ["name", "category"]`
283
+ *
284
+ * e.g. `allowedFilters: []`
285
+ */
286
+ allowedFilters?: (keyof M)[];
287
+
264
288
  /**
265
289
  * Default sort applied to this collection.
266
290
  * When setting this prop, entities will have a default order
@@ -473,6 +497,14 @@ export interface CollectionActionsProps<M extends Record<string, any> = any, USE
473
497
  */
474
498
  path: string;
475
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
+
476
508
  /**
477
509
  * Path of the last collection, like `addresses`
478
510
  */
@@ -725,6 +757,8 @@ export type EntityTableController<M extends Record<string, any> = any> = {
725
757
  dataLoadingError?: Error;
726
758
  filterValues?: FilterValues<Extract<keyof M, string>>;
727
759
  setFilterValues?: (filterValues: FilterValues<Extract<keyof M, string>>) => void;
760
+ allowedFilters?: (keyof M)[];
761
+ forcedFilters?: (keyof M)[];
728
762
  sortBy?: [Extract<keyof M, string>, "asc" | "desc"];
729
763
  setSortBy?: (sortBy?: [Extract<keyof M, string>, "asc" | "desc"]) => void;
730
764
  searchString?: string;