@firecms/core 3.4.0-canary.cb27f16 → 3.4.0-canary.ce97da7

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 (88) hide show
  1. package/README.md +4 -0
  2. package/dist/components/ErrorBoundary.d.ts +1 -3
  3. package/dist/components/LicenseBanner.d.ts +23 -0
  4. package/dist/components/index.d.ts +1 -0
  5. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/plugin_translations.d.ts +38 -0
  8. package/dist/core/pro_plugins.d.ts +40 -0
  9. package/dist/de-DMp6cIY1.js +702 -0
  10. package/dist/de-DMp6cIY1.js.map +1 -0
  11. package/dist/editor/extensions/Image.d.ts +11 -0
  12. package/dist/editor/index.d.ts +2 -1
  13. package/dist/editor/lazy_editor.d.ts +6 -0
  14. package/dist/editor-BSo_aS1I.js +4563 -0
  15. package/dist/editor-BSo_aS1I.js.map +1 -0
  16. package/dist/fr-Cckz4IkR.js +702 -0
  17. package/dist/fr-Cckz4IkR.js.map +1 -0
  18. package/dist/hi-CDCoIqbu.js +702 -0
  19. package/dist/hi-CDCoIqbu.js.map +1 -0
  20. package/dist/hooks/index.d.ts +2 -0
  21. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  22. package/dist/hooks/useProjectLog.d.ts +11 -6
  23. package/dist/hooks/useTranslation.d.ts +1 -1
  24. package/dist/index.es.js +7938 -15928
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/index.umd.js +7710 -6925
  27. package/dist/index.umd.js.map +1 -1
  28. package/dist/it-BJCUqixj.js +702 -0
  29. package/dist/it-BJCUqixj.js.map +1 -0
  30. package/dist/pl-DgT_joFf.js +704 -0
  31. package/dist/pl-DgT_joFf.js.map +1 -0
  32. package/dist/pt-B3dN3x0X.js +704 -0
  33. package/dist/pt-B3dN3x0X.js.map +1 -0
  34. package/dist/types/analytics.d.ts +1 -1
  35. package/dist/types/firecms.d.ts +18 -1
  36. package/dist/types/firecms_context.d.ts +8 -0
  37. package/dist/types/index.d.ts +1 -0
  38. package/dist/types/license.d.ts +54 -0
  39. package/dist/types/plugins.d.ts +9 -1
  40. package/dist/types/translations.d.ts +35 -2
  41. package/dist/util/date_fns_locales.d.ts +18 -0
  42. package/dist/util/index.d.ts +1 -0
  43. package/dist/util/lazy_eager.d.ts +3 -3
  44. package/package.json +4 -4
  45. package/src/app/Scaffold.tsx +7 -0
  46. package/src/components/ErrorBoundary.tsx +12 -18
  47. package/src/components/LicenseBanner.tsx +209 -0
  48. package/src/components/index.tsx +1 -0
  49. package/src/contexts/LicenseStatusContext.tsx +50 -0
  50. package/src/core/DefaultAppBar.tsx +28 -16
  51. package/src/core/FireCMS.tsx +73 -47
  52. package/src/core/SideDialogs.tsx +44 -1
  53. package/src/core/index.tsx +1 -0
  54. package/src/core/plugin_translations.ts +87 -0
  55. package/src/core/pro_plugins.ts +79 -0
  56. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  57. package/src/editor/extensions/Image.ts +17 -1
  58. package/src/editor/hooks/useProseMirror.ts +5 -0
  59. package/src/editor/index.ts +11 -1
  60. package/src/editor/lazy_editor.tsx +33 -0
  61. package/src/hooks/index.tsx +2 -0
  62. package/src/hooks/useBuildNavigationController.tsx +47 -7
  63. package/src/hooks/useFireCMSContext.tsx +7 -3
  64. package/src/hooks/useLicenseStatus.tsx +32 -0
  65. package/src/hooks/useProjectLog.tsx +27 -9
  66. package/src/hooks/useTranslation.ts +3 -1
  67. package/src/i18n/FireCMSi18nProvider.tsx +164 -34
  68. package/src/locales/de.ts +18 -2
  69. package/src/locales/en.ts +21 -3
  70. package/src/locales/es.ts +18 -2
  71. package/src/locales/fr.ts +18 -2
  72. package/src/locales/hi.ts +18 -2
  73. package/src/locales/it.ts +18 -2
  74. package/src/locales/pl.ts +18 -2
  75. package/src/locales/pt.ts +18 -2
  76. package/src/preview/components/DatePreview.tsx +2 -4
  77. package/src/types/analytics.ts +3 -0
  78. package/src/types/firecms.tsx +19 -1
  79. package/src/types/firecms_context.tsx +9 -0
  80. package/src/types/index.ts +1 -0
  81. package/src/types/license.ts +62 -0
  82. package/src/types/plugins.tsx +10 -1
  83. package/src/types/translations.ts +35 -2
  84. package/src/util/date_fns_locales.ts +168 -0
  85. package/src/util/index.ts +1 -0
  86. package/src/util/lazy_eager.tsx +30 -8
  87. package/src/util/navigation_utils.ts +6 -1
  88. package/src/util/useStorageUploadController.tsx +6 -1
package/src/locales/pl.ts CHANGED
@@ -65,8 +65,19 @@ export const pl: FireCMSTranslations = {
65
65
  select_from: "Wybierz z {{name}}",
66
66
  done: "Gotowe",
67
67
  log_out: "Wyloguj się",
68
- license_needed: "Wymagana licencja",
69
- license_description: "Aby korzystać z FireCMS PRO, potrzebujesz ważnej licencji. Skontaktuj się z nami pod adresem {{email}}, aby uzyskać więcej informacji.",
68
+ license_trial_banner: "Okres próbny FireCMS PRO w produkcji kończy się za {{days}} dni.",
69
+ license_trial_banner_one: "Okres próbny FireCMS PRO w produkcji kończy się za {{days}} dzień.",
70
+ license_expired_banner: "Twój okres próbny PRO zakończył się {{date}}. Funkcje PRO, takie jak edytor schematów, import/eksport i historia, są wstrzymane; Twoje dane i kolekcje nadal działają.",
71
+ license_expired_banner_undated: "Twój okres próbny PRO się zakończył. Funkcje PRO, takie jak edytor schematów, import/eksport i historia, są wstrzymane; Twoje dane i kolekcje nadal działają.",
72
+ license_invalid_project_banner: "Ten klucz licencyjny nie jest powiązany z projektem {{projectId}}. Do tego czasu funkcje PRO są wstrzymane.",
73
+ license_invalid_project_banner_unnamed: "Ten klucz licencyjny nie jest powiązany z tym projektem. Do tego czasu funkcje PRO są wstrzymane.",
74
+ license_over_quota_banner: "Liczba projektów objętych licencją: {{licensed}}, powiązanych z nią: {{actual}}.",
75
+ license_over_quota_banner_one: "Ta licencja obejmuje {{licensed}} projekt, a powiązanych z nią projektów jest: {{actual}}.",
76
+ license_get_license: "Kup licencję",
77
+ license_update_license: "Zaktualizuj licencję",
78
+ license_add_project_to_license: "Dodaj go do licencji",
79
+ pro_features_paused: "Funkcje PRO wstrzymane",
80
+ user_management_paused_description: "Zarządzanie użytkownikami i rolami jest częścią FireCMS PRO. Dotychczasowi użytkownicy i role nadal decydują o tym, kto może się zalogować i co może robić. Edytować je będzie można ponownie, gdy ten projekt otrzyma licencję.",
70
81
  column_cannot_be_edited: "Ta kolumna nie może być edytowana bezpośrednio",
71
82
  close: "Zamknij",
72
83
  unsaved_local_changes: "Niezapisane zmiany lokalne",
@@ -390,6 +401,8 @@ export const pl: FireCMSTranslations = {
390
401
  large_number_of_documents: "Ta kolekcja zawiera dużą liczbę dokumentów ({{count}}).",
391
402
  include_undefined_values: "Uwzględnij niezdefiniowane wartości",
392
403
  export_apply_filter_sort: "Eksportuj tylko wyniki zgodne z aktualnym filtrem/sortowaniem",
404
+ export_all_entities: "Wszystkie encje",
405
+ export_selected_entities: "Zaznaczone encje ({{count}})",
393
406
  submit: "Wyślij",
394
407
 
395
408
  no_filterable_properties: "Brak dostępnych właściwości do filtrowania",
@@ -439,6 +452,7 @@ export const pl: FireCMSTranslations = {
439
452
  roles_menu: "Role",
440
453
  project_settings: "Ustawienia projektu",
441
454
  firestore_manager: "Menedżer Firestore",
455
+ datatalk: "DataTalk",
442
456
  manage_your_firestore_data: "Zarządzaj danymi w Firestore",
443
457
 
444
458
  // ─── FireCMS Cloud Login ──────────────────────────────────────
@@ -455,6 +469,7 @@ export const pl: FireCMSTranslations = {
455
469
  not_the_admin_panel: "a nie na panelu administracyjnym.",
456
470
  join_our_newsletter: "Dołącz do naszego newslettera. Bez spamu, tylko ważne aktualizacje!",
457
471
  by_signing_in_you_agree_to_our: "Logując się, akceptujesz nasz",
472
+ please_accept_the_terms_to_continue: "Zaakceptuj regulamin, aby kontynuować",
458
473
  terms_and_conditions: "Regulamin",
459
474
  and_our: "oraz naszą",
460
475
  privacy_policy: "Politykę prywatności",
@@ -497,6 +512,7 @@ export const pl: FireCMSTranslations = {
497
512
  licensed_projects_lowercase: "licencjonowanych projektów",
498
513
  manage: "Zarządzaj",
499
514
  update_payment_method: "Zaktualizuj metodę płatności",
515
+ stripe_portal_error: "Nie udało się otworzyć Stripe: {{message}}",
500
516
  your_firecms_cloud_projects: "Twoje projekty FireCMS Cloud",
501
517
  status_active: "Aktywna",
502
518
  status_trialing: "Okres próbny",
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: "Esta licença cobre {{licensed}} projetos e tem {{actual}} associados.",
80
+ license_over_quota_banner_one: "Esta licença cobre {{licensed}} projeto e tem {{actual}} associados.",
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",
@@ -395,6 +406,8 @@ export const pt: FireCMSTranslations = {
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",
397
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}})",
398
411
  submit: "Submeter",
399
412
 
400
413
  no_filterable_properties: "Não há propriedades filtráveis disponíveis",
@@ -444,6 +457,7 @@ export const pt: FireCMSTranslations = {
444
457
  roles_menu: "Funções",
445
458
  project_settings: "Definições do projeto",
446
459
  firestore_manager: "Gerenciador do Firestore",
460
+ datatalk: "DataTalk",
447
461
  manage_your_firestore_data: "Gerencie os seus dados do Firestore",
448
462
 
449
463
  // ─── FireCMS Cloud Login ──────────────────────────────────────
@@ -460,6 +474,7 @@ export const pt: FireCMSTranslations = {
460
474
  not_the_admin_panel: "não no painel de administração.",
461
475
  join_our_newsletter: "Subscreva a nossa newsletter. Sem spam, apenas atualizações importantes!",
462
476
  by_signing_in_you_agree_to_our: "Ao iniciar sessão, concorda com os nossos",
477
+ please_accept_the_terms_to_continue: "Aceite os termos e condições para continuar",
463
478
  terms_and_conditions: "Termos e Condições",
464
479
  and_our: "e a nossa",
465
480
  privacy_policy: "Política de privacidade",
@@ -502,6 +517,7 @@ export const pt: FireCMSTranslations = {
502
517
  licensed_projects_lowercase: "projetos licenciados",
503
518
  manage: "Gerir",
504
519
  update_payment_method: "Atualizar método de pagamento",
520
+ stripe_portal_error: "Não foi possível abrir o Stripe: {{message}}",
505
521
  your_firecms_cloud_projects: "Os seus projetos FireCMS Cloud",
506
522
  status_active: "Ativo",
507
523
  status_trialing: "Em avaliação",
@@ -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 <></>;
@@ -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
  ;
@@ -70,10 +70,28 @@ export type FireCMSProps<USER extends User> = {
70
70
  }) => React.ReactNode;
71
71
 
72
72
  /**
73
- * If you have a custom API key, you can use it here.
73
+ * Your FireCMS PRO license key. It is checked against
74
+ * `https://api.firecms.co/access_log` when a user signs in.
74
75
  */
75
76
  apiKey?: string;
76
77
 
78
+ /**
79
+ * On sign-in FireCMS sends one request to `https://api.firecms.co/access_log`
80
+ * with the user's ID token (which carries the Firebase project id, the uid
81
+ * and the email), the user's email, the datasource key and the keys of the
82
+ * mounted plugins; the browser adds the page's referrer. It is how the
83
+ * FireCMS PRO license and trial are checked. No database credentials or
84
+ * content are sent.
85
+ *
86
+ * Set to `false` to skip that request. This only takes effect when the
87
+ * app has no `apiKey` and mounts no PRO plugin (collection editor, user
88
+ * management, import/export, entity history, data enhancement, DataTalk):
89
+ * otherwise the request is the license check and is always sent.
90
+ *
91
+ * Defaults to `true`.
92
+ */
93
+ telemetry?: boolean;
94
+
77
95
  /**
78
96
  * Record of custom form fields to be used in the CMS.
79
97
  * You can use the key to reference the custom field in
@@ -11,6 +11,7 @@ import { DialogsController } from "./dialogs_controller";
11
11
  import { CustomizationController } from "./customization_controller";
12
12
  import { AnalyticsController } from "./analytics_controller";
13
13
  import { InternalUserManagement } from "./internal_user_management";
14
+ import { AccessResponse } from "./license";
14
15
 
15
16
  /**
16
17
  * Context that includes the internal controllers and contexts used by the app.
@@ -97,4 +98,12 @@ export type FireCMSContext<USER extends User = User, AuthControllerType extends
97
98
  */
98
99
  userManagement: InternalUserManagement<USER>
99
100
 
101
+ /**
102
+ * FireCMS license status of this project, `null` until the license check
103
+ * answers or when it is not sent. PRO plugins are paused while `blocked`
104
+ * is true.
105
+ * @see useLicenseStatus
106
+ */
107
+ licenseStatus?: AccessResponse | null;
108
+
100
109
  };
@@ -29,3 +29,4 @@ export * from "./modify_collections";
29
29
  export * from "./analytics_controller";
30
30
  export * from "./customization_controller";
31
31
  export * from "./translations";
32
+ export * from "./license";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * What the FireCMS license check says about this project.
3
+ *
4
+ * - `not_required`: no PRO plugin is mounted.
5
+ * - `whitelisted`: the project is exempt from licensing.
6
+ * - `licensed`: a license covers this project.
7
+ * - `trial`: PRO plugins run on the free trial; see `daysLeft` and `trialEndsAt`.
8
+ * - `expired`: the trial ended without a license. PRO plugins are paused.
9
+ * - `invalid_project`: a license key was presented, but this project is not
10
+ * linked to that license. PRO plugins are paused.
11
+ * - `over_quota`: the license has more projects linked than it pays for.
12
+ * Nothing is paused.
13
+ *
14
+ * @group Models
15
+ */
16
+ export type LicenseState =
17
+ | "not_required"
18
+ | "whitelisted"
19
+ | "licensed"
20
+ | "trial"
21
+ | "expired"
22
+ | "invalid_project"
23
+ | "over_quota";
24
+
25
+ /**
26
+ * Response of the FireCMS license check (`POST https://api.firecms.co/access_log`).
27
+ *
28
+ * Servers older than the PRO relaunch send only `blocked` and `message`, so
29
+ * treat a missing `licenseState` as unknown. `blocked` is honoured either way.
30
+ *
31
+ * @group Models
32
+ */
33
+ export type AccessResponse = {
34
+ /**
35
+ * True when the PRO plugins must be paused. The core CMS is never blocked.
36
+ */
37
+ blocked?: boolean;
38
+ licenseState?: LicenseState;
39
+ /**
40
+ * Number of projects the license pays for. Present with `licensed` and `over_quota`.
41
+ */
42
+ licensedProjects?: number;
43
+ /**
44
+ * Number of projects linked to the license.
45
+ */
46
+ linkedProjects?: number;
47
+ projectId?: string;
48
+ licenseId?: string;
49
+ /**
50
+ * ISO date the trial ends or ended. Present with `trial` and `expired`.
51
+ */
52
+ trialEndsAt?: string;
53
+ /**
54
+ * Whole days left in the trial, present with `trial`.
55
+ */
56
+ daysLeft?: number;
57
+ /**
58
+ * Where to get or update a license for this project.
59
+ */
60
+ subscribeUrl?: string;
61
+ message?: string;
62
+ };
@@ -15,6 +15,9 @@ import { InternalUserManagement } from "./internal_user_management";
15
15
  * NOTE: This is a work in progress and the API is not stable yet.
16
16
  * @group Core
17
17
  */
18
+ /** The strings a plugin contributes for one locale. */
19
+ export type PluginTranslations = Record<string, string>;
20
+
18
21
  export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollection = EntityCollection, COL_ACTIONS_PROPS = any, COL_ACTIONS_START__PROPS = any> = {
19
22
 
20
23
  /**
@@ -33,8 +36,14 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
33
36
  * locales, specific for this plugin.
34
37
  * Keys can be existing `FireCMSTranslations` keys, or new keys if using
35
38
  * module augmentation.
39
+ *
40
+ * A value may be the strings themselves, or a function returning a promise
41
+ * of them. Use the function form for locales you ship in the plugin bundle:
42
+ * only the active language is ever read, so importing all of them statically
43
+ * puts every language into the bundle of every app that mounts the plugin.
44
+ * Both forms are supported; a plain record keeps working unchanged.
36
45
  */
37
- i18n?: Record<string, Record<string, string>>;
46
+ i18n?: Record<string, PluginTranslations | (() => Promise<PluginTranslations>)>;
38
47
 
39
48
  /**
40
49
  * You can use this prop to add higher order components to the CMS.
@@ -66,8 +66,32 @@ export interface FireCMSTranslations {
66
66
  select_from: string;
67
67
  done: string;
68
68
  log_out: string;
69
- license_needed: string;
70
- license_description: string;
69
+ /** License banner during the PRO trial. `{{days}}` is the number of days left; plural form. */
70
+ license_trial_banner: string;
71
+ /** `license_trial_banner` when one day is left (i18next `_one` plural form). */
72
+ license_trial_banner_one: string;
73
+ /** License banner once the trial has ended. `{{date}}` is the day it ended. */
74
+ license_expired_banner: string;
75
+ /** `license_expired_banner` when the end date is not known. */
76
+ license_expired_banner_undated: string;
77
+ /** License banner when the license key is not linked to this project. `{{projectId}}` is the Firebase project id. */
78
+ license_invalid_project_banner: string;
79
+ /** `license_invalid_project_banner` when the project id is not known. */
80
+ license_invalid_project_banner_unnamed: string;
81
+ /** License banner when more projects are linked than the license covers. `{{licensed}}` covered, `{{actual}}` linked; plural form. */
82
+ license_over_quota_banner: string;
83
+ /** `license_over_quota_banner` when the license covers one project (i18next `_one` plural form). */
84
+ license_over_quota_banner_one: string;
85
+ /** Link in the license banner to buy a license. */
86
+ license_get_license: string;
87
+ /** Link in the over-quota banner to the license settings. */
88
+ license_update_license: string;
89
+ /** Link in the invalid-project banner to link this project to the license. */
90
+ license_add_project_to_license: string;
91
+ /** Title of the notice a PRO view shows while PRO features are paused. */
92
+ pro_features_paused: string;
93
+ /** Users and Roles views while PRO features are paused. */
94
+ user_management_paused_description: string;
71
95
  column_cannot_be_edited: string;
72
96
  close: string;
73
97
  unsaved_local_changes: string;
@@ -421,6 +445,10 @@ export interface FireCMSTranslations {
421
445
  include_undefined_values: string;
422
446
  /** Toggle in the export dialog, only shown when a filter or sort is active */
423
447
  export_apply_filter_sort: string;
448
+ /** Choice in the export dialog, only shown when entities are selected */
449
+ export_all_entities: string;
450
+ /** Same choice; `{{count}}` is the number of selected entities */
451
+ export_selected_entities: string;
424
452
  submit: string;
425
453
 
426
454
  no_filterable_properties: string;
@@ -470,6 +498,8 @@ export interface FireCMSTranslations {
470
498
  roles_menu: string;
471
499
  project_settings: string;
472
500
  firestore_manager: string;
501
+ /** The DataTalk product name, keyed so a locale can transliterate it. */
502
+ datatalk: string;
473
503
  manage_your_firestore_data: string;
474
504
 
475
505
  // ─── FireCMS Cloud Login ──────────────────────────────────────
@@ -486,6 +516,7 @@ export interface FireCMSTranslations {
486
516
  not_the_admin_panel: string;
487
517
  join_our_newsletter: string;
488
518
  by_signing_in_you_agree_to_our: string;
519
+ please_accept_the_terms_to_continue: string;
489
520
  terms_and_conditions: string;
490
521
  and_our: string;
491
522
  privacy_policy: string;
@@ -528,6 +559,8 @@ export interface FireCMSTranslations {
528
559
  licensed_projects_lowercase: string;
529
560
  manage: string;
530
561
  update_payment_method: string;
562
+ /** Snackbar when a Stripe billing portal link can't be created. `{{message}}` is the reason. */
563
+ stripe_portal_error: string;
531
564
  your_firecms_cloud_projects: string;
532
565
  status_active: string;
533
566
  status_trialing: string;
@@ -0,0 +1,168 @@
1
+ import { useEffect, useReducer } from "react";
2
+ import type { Locale as DateFnsLocale } from "date-fns";
3
+ import { Locale } from "../types";
4
+
5
+ /**
6
+ * On-demand loading of date-fns locales.
7
+ *
8
+ * `import * as locales from "date-fns/locale"` pulls in every locale date-fns
9
+ * ships — 625KB of parsed JavaScript, loaded on every single page view, to
10
+ * support a `locale` setting that is unset for almost every project. Worse, it
11
+ * sat on the critical path: `@firecms/core` imported it statically, so the
12
+ * browser could not render anything until it had come down.
13
+ *
14
+ * Each locale now has its own dynamic import, so exactly one small chunk is
15
+ * fetched, and only when a project actually configures a locale. Projects that
16
+ * do not configure one (the default) pay nothing at all.
17
+ *
18
+ * The map is written out rather than built from a template literal because Vite
19
+ * and webpack both need the specifier to be statically analysable to emit the
20
+ * chunks.
21
+ */
22
+ const loaders: Partial<Record<Locale, () => Promise<DateFnsLocale>>> = {
23
+ af: () => import("date-fns/locale/af").then(m => m.af),
24
+ ar: () => import("date-fns/locale/ar").then(m => m.ar),
25
+ arDZ: () => import("date-fns/locale/ar-DZ").then(m => m.arDZ),
26
+ arMA: () => import("date-fns/locale/ar-MA").then(m => m.arMA),
27
+ arSA: () => import("date-fns/locale/ar-SA").then(m => m.arSA),
28
+ az: () => import("date-fns/locale/az").then(m => m.az),
29
+ be: () => import("date-fns/locale/be").then(m => m.be),
30
+ bg: () => import("date-fns/locale/bg").then(m => m.bg),
31
+ bn: () => import("date-fns/locale/bn").then(m => m.bn),
32
+ ca: () => import("date-fns/locale/ca").then(m => m.ca),
33
+ cs: () => import("date-fns/locale/cs").then(m => m.cs),
34
+ cy: () => import("date-fns/locale/cy").then(m => m.cy),
35
+ da: () => import("date-fns/locale/da").then(m => m.da),
36
+ de: () => import("date-fns/locale/de").then(m => m.de),
37
+ el: () => import("date-fns/locale/el").then(m => m.el),
38
+ enAU: () => import("date-fns/locale/en-AU").then(m => m.enAU),
39
+ enCA: () => import("date-fns/locale/en-CA").then(m => m.enCA),
40
+ enGB: () => import("date-fns/locale/en-GB").then(m => m.enGB),
41
+ enIN: () => import("date-fns/locale/en-IN").then(m => m.enIN),
42
+ enNZ: () => import("date-fns/locale/en-NZ").then(m => m.enNZ),
43
+ enUS: () => import("date-fns/locale/en-US").then(m => m.enUS),
44
+ eo: () => import("date-fns/locale/eo").then(m => m.eo),
45
+ es: () => import("date-fns/locale/es").then(m => m.es),
46
+ et: () => import("date-fns/locale/et").then(m => m.et),
47
+ eu: () => import("date-fns/locale/eu").then(m => m.eu),
48
+ faIR: () => import("date-fns/locale/fa-IR").then(m => m.faIR),
49
+ fi: () => import("date-fns/locale/fi").then(m => m.fi),
50
+ fr: () => import("date-fns/locale/fr").then(m => m.fr),
51
+ frCA: () => import("date-fns/locale/fr-CA").then(m => m.frCA),
52
+ frCH: () => import("date-fns/locale/fr-CH").then(m => m.frCH),
53
+ gd: () => import("date-fns/locale/gd").then(m => m.gd),
54
+ gl: () => import("date-fns/locale/gl").then(m => m.gl),
55
+ gu: () => import("date-fns/locale/gu").then(m => m.gu),
56
+ he: () => import("date-fns/locale/he").then(m => m.he),
57
+ hi: () => import("date-fns/locale/hi").then(m => m.hi),
58
+ hr: () => import("date-fns/locale/hr").then(m => m.hr),
59
+ hu: () => import("date-fns/locale/hu").then(m => m.hu),
60
+ hy: () => import("date-fns/locale/hy").then(m => m.hy),
61
+ id: () => import("date-fns/locale/id").then(m => m.id),
62
+ is: () => import("date-fns/locale/is").then(m => m.is),
63
+ it: () => import("date-fns/locale/it").then(m => m.it),
64
+ ja: () => import("date-fns/locale/ja").then(m => m.ja),
65
+ ka: () => import("date-fns/locale/ka").then(m => m.ka),
66
+ kk: () => import("date-fns/locale/kk").then(m => m.kk),
67
+ kn: () => import("date-fns/locale/kn").then(m => m.kn),
68
+ ko: () => import("date-fns/locale/ko").then(m => m.ko),
69
+ lb: () => import("date-fns/locale/lb").then(m => m.lb),
70
+ lt: () => import("date-fns/locale/lt").then(m => m.lt),
71
+ lv: () => import("date-fns/locale/lv").then(m => m.lv),
72
+ mk: () => import("date-fns/locale/mk").then(m => m.mk),
73
+ ms: () => import("date-fns/locale/ms").then(m => m.ms),
74
+ mt: () => import("date-fns/locale/mt").then(m => m.mt),
75
+ nb: () => import("date-fns/locale/nb").then(m => m.nb),
76
+ nl: () => import("date-fns/locale/nl").then(m => m.nl),
77
+ nlBE: () => import("date-fns/locale/nl-BE").then(m => m.nlBE),
78
+ nn: () => import("date-fns/locale/nn").then(m => m.nn),
79
+ pl: () => import("date-fns/locale/pl").then(m => m.pl),
80
+ pt: () => import("date-fns/locale/pt").then(m => m.pt),
81
+ ptBR: () => import("date-fns/locale/pt-BR").then(m => m.ptBR),
82
+ ro: () => import("date-fns/locale/ro").then(m => m.ro),
83
+ ru: () => import("date-fns/locale/ru").then(m => m.ru),
84
+ sk: () => import("date-fns/locale/sk").then(m => m.sk),
85
+ sl: () => import("date-fns/locale/sl").then(m => m.sl),
86
+ sr: () => import("date-fns/locale/sr").then(m => m.sr),
87
+ srLatn: () => import("date-fns/locale/sr-Latn").then(m => m.srLatn),
88
+ sv: () => import("date-fns/locale/sv").then(m => m.sv),
89
+ ta: () => import("date-fns/locale/ta").then(m => m.ta),
90
+ te: () => import("date-fns/locale/te").then(m => m.te),
91
+ th: () => import("date-fns/locale/th").then(m => m.th),
92
+ tr: () => import("date-fns/locale/tr").then(m => m.tr),
93
+ ug: () => import("date-fns/locale/ug").then(m => m.ug),
94
+ uk: () => import("date-fns/locale/uk").then(m => m.uk),
95
+ uz: () => import("date-fns/locale/uz").then(m => m.uz),
96
+ vi: () => import("date-fns/locale/vi").then(m => m.vi),
97
+ zhCN: () => import("date-fns/locale/zh-CN").then(m => m.zhCN),
98
+ zhTW: () => import("date-fns/locale/zh-TW").then(m => m.zhTW),
99
+ };
100
+
101
+ const cache = new Map<Locale, DateFnsLocale>();
102
+ const inFlight = new Map<Locale, Promise<DateFnsLocale | undefined>>();
103
+
104
+ /**
105
+ * Load a date-fns locale, resolving from cache when it is already in memory.
106
+ * Concurrent callers for the same locale share one request.
107
+ */
108
+ export function loadDateFnsLocale(locale: Locale): Promise<DateFnsLocale | undefined> {
109
+ const cached = cache.get(locale);
110
+ if (cached) {
111
+ return Promise.resolve(cached);
112
+ }
113
+ const pending = inFlight.get(locale);
114
+ if (pending) {
115
+ return pending;
116
+ }
117
+ const loader = loaders[locale];
118
+ if (!loader) {
119
+ // An unknown key behaves as it did when the whole barrel was imported:
120
+ // the lookup missed and date-fns fell back to its built-in English.
121
+ return Promise.resolve(undefined);
122
+ }
123
+ const promise = loader()
124
+ .then((loaded) => {
125
+ cache.set(locale, loaded);
126
+ return loaded;
127
+ })
128
+ .catch(() => undefined)
129
+ .finally(() => {
130
+ inFlight.delete(locale);
131
+ });
132
+ inFlight.set(locale, promise);
133
+ return promise;
134
+ }
135
+
136
+ /** The locale if it is already loaded, otherwise undefined. Never suspends. */
137
+ export function getLoadedDateFnsLocale(locale?: Locale): DateFnsLocale | undefined {
138
+ return locale ? cache.get(locale) : undefined;
139
+ }
140
+
141
+ /**
142
+ * The date-fns locale for `locale`, loading it in the background on first use.
143
+ *
144
+ * Returns undefined until the chunk arrives, which formats the first paint with
145
+ * date-fns' built-in English and then re-renders. That is a change only for
146
+ * projects that set a locale, and only for the few hundred milliseconds before
147
+ * the chunk lands — in exchange for every project no longer waiting on 625KB.
148
+ */
149
+ export function useDateFnsLocale(locale?: Locale): DateFnsLocale | undefined {
150
+ const [, forceUpdate] = useReducer((count: number) => count + 1, 0);
151
+
152
+ useEffect(() => {
153
+ if (!locale || cache.has(locale)) {
154
+ return;
155
+ }
156
+ let active = true;
157
+ loadDateFnsLocale(locale).then((loaded) => {
158
+ if (active && loaded) {
159
+ forceUpdate();
160
+ }
161
+ });
162
+ return () => {
163
+ active = false;
164
+ };
165
+ }, [locale]);
166
+
167
+ return locale ? cache.get(locale) : undefined;
168
+ }
package/src/util/index.ts CHANGED
@@ -28,3 +28,4 @@ export * from "./storage";
28
28
  export * from "./callbacks";
29
29
  export * from "./conditions";
30
30
  export * from "./lazy_eager";
31
+ export * from "./date_fns_locales";
@@ -1,9 +1,35 @@
1
1
  import React from "react";
2
2
 
3
3
  /**
4
- * Returns a React.lazy component that is also preloaded immediately using
5
- * requestIdleCallback or setTimeout.
6
- * This ensures that chunks are split, but fetched in the background before they are actually needed.
4
+ * Prefetch after the page has loaded, never during it.
5
+ *
6
+ * An idle callback scheduled at module evaluation time fires in the gaps between
7
+ * the tasks of the initial load — which is exactly when the critical chunks are
8
+ * still in flight. With a dozen route components each queueing one, the
9
+ * background prefetch was competing with the render path for connections and
10
+ * bandwidth. Waiting for the load event costs the prefetch nothing that matters:
11
+ * it exists to be ready before a navigation, not before first paint.
12
+ */
13
+ function schedulePrefetch(load: () => void) {
14
+ const whenIdle = () => {
15
+ if ("requestIdleCallback" in window) {
16
+ (window as any).requestIdleCallback(load);
17
+ } else {
18
+ setTimeout(load, 500);
19
+ }
20
+ };
21
+
22
+ if (document.readyState === "complete") {
23
+ whenIdle();
24
+ } else {
25
+ window.addEventListener("load", whenIdle, { once: true });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Returns a React.lazy component that is also preloaded in the background, once
31
+ * the page has finished loading.
32
+ * This ensures that chunks are split, but fetched before they are actually needed.
7
33
  */
8
34
  export function lazyEager<T extends React.ComponentType<any>>(
9
35
  factory: () => Promise<any>,
@@ -22,11 +48,7 @@ export function lazyEager<T extends React.ComponentType<any>>(
22
48
  };
23
49
 
24
50
  if (typeof window !== "undefined") {
25
- if ("requestIdleCallback" in window) {
26
- (window as any).requestIdleCallback(load);
27
- } else {
28
- setTimeout(load, 500);
29
- }
51
+ schedulePrefetch(load);
30
52
  }
31
53
 
32
54
  return React.lazy(load);
@@ -281,7 +281,12 @@ export function resolveCollectionPathIds(path: string, allCollections: EntityCol
281
281
  col,
282
282
  match: col.id
283
283
  }])
284
- .filter(p => p.match && remainingPath.startsWith(p.match))
284
+ // A match has to end where a path segment ends. Matching any prefix made
285
+ // "products-test" resolve to the "products" collection with an entity called
286
+ // "-test" (FirebaseError: Invalid document reference), and any collection whose
287
+ // path merely starts with another's — "products_archive", which the collection
288
+ // editor generates from "Products archive" — resolve to the wrong one.
289
+ .filter(p => p.match && (remainingPath === p.match || remainingPath.startsWith(p.match + "/")))
285
290
  .sort((a, b) => b.match.length - a.match.length);
286
291
 
287
292
  if (potentialMatches.length > 0) {
@@ -1,4 +1,4 @@
1
- import Compressor from "compressorjs";
1
+ import type CompressorType from "compressorjs";
2
2
  import equal from "react-fast-compare";
3
3
 
4
4
  import {
@@ -321,6 +321,11 @@ export async function resizeImage(
321
321
  mimeType = `image/${imageResize.format}`;
322
322
  }
323
323
 
324
+ // Loaded here rather than imported: compressing an image is something a
325
+ // small fraction of sessions ever does, and a static import put the library
326
+ // in `@firecms/core` for everyone.
327
+ const Compressor: typeof CompressorType = (await import("compressorjs")).default;
328
+
324
329
  return new Promise<File>((resolve, reject) => {
325
330
  new Compressor(file, {
326
331
  quality,