@firecms/core 3.4.0-canary.8635253 → 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.
- package/README.md +4 -0
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
- package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/ErrorBoundary.d.ts +1 -3
- package/dist/components/LicenseBanner.d.ts +23 -0
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
- package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
- package/dist/components/common/table_url_params.d.ts +24 -0
- package/dist/components/common/useDataSourceTableController.d.ts +9 -1
- package/dist/components/common/useTableSearchHelper.d.ts +9 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/contexts/LicenseStatusContext.d.ts +16 -0
- package/dist/core/EntityEditView.d.ts +9 -1
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/plugin_translations.d.ts +38 -0
- package/dist/core/pro_plugins.d.ts +40 -0
- package/dist/de-DMp6cIY1.js +702 -0
- package/dist/de-DMp6cIY1.js.map +1 -0
- package/dist/editor/extensions/Image.d.ts +11 -0
- package/dist/editor/index.d.ts +2 -1
- package/dist/editor/lazy_editor.d.ts +6 -0
- package/dist/editor-BSo_aS1I.js +4563 -0
- package/dist/editor-BSo_aS1I.js.map +1 -0
- package/dist/form/EntityForm.d.ts +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/fr-Cckz4IkR.js +702 -0
- package/dist/fr-Cckz4IkR.js.map +1 -0
- package/dist/hi-CDCoIqbu.js +702 -0
- package/dist/hi-CDCoIqbu.js.map +1 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +9 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useLicenseStatus.d.ts +16 -0
- package/dist/hooks/useProjectLog.d.ts +11 -6
- package/dist/hooks/useTranslation.d.ts +1 -1
- package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +3921 -11331
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +8950 -7585
- package/dist/index.umd.js.map +1 -1
- package/dist/it-BJCUqixj.js +702 -0
- package/dist/it-BJCUqixj.js.map +1 -0
- package/dist/pl-DgT_joFf.js +704 -0
- package/dist/pl-DgT_joFf.js.map +1 -0
- package/dist/pt-B3dN3x0X.js +704 -0
- package/dist/pt-B3dN3x0X.js.map +1 -0
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +14 -0
- package/dist/types/datasource.d.ts +67 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/types/entity_actions.d.ts +6 -0
- package/dist/types/entity_callbacks.d.ts +21 -0
- package/dist/types/firecms.d.ts +18 -1
- package/dist/types/firecms_context.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/license.d.ts +54 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +22 -1
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +7 -0
- package/dist/types/translations.d.ts +41 -2
- package/dist/util/__tests__/collections.test.d.ts +1 -0
- package/dist/util/__tests__/urls.test.d.ts +1 -0
- package/dist/util/collections.d.ts +1 -1
- package/dist/util/date_fns_locales.d.ts +18 -0
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/lazy_eager.d.ts +3 -3
- package/dist/util/navigation_from_path.d.ts +7 -0
- package/dist/util/navigation_utils.d.ts +87 -3
- package/dist/util/parent_references_from_path.d.ts +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/dist/util/urls.d.ts +22 -0
- package/package.json +5 -5
- package/src/app/Scaffold.tsx +7 -0
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +4 -1
- package/src/components/ErrorBoundary.tsx +12 -18
- package/src/components/LicenseBanner.tsx +209 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
- package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
- package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +86 -179
- package/src/components/common/useTableSearchHelper.ts +26 -2
- package/src/components/index.tsx +1 -0
- package/src/contexts/LicenseStatusContext.tsx +50 -0
- package/src/contexts/SnackbarProvider.tsx +14 -1
- package/src/core/DefaultAppBar.tsx +28 -16
- package/src/core/EntityEditView.tsx +20 -3
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +8 -4
- package/src/core/FireCMS.tsx +73 -47
- package/src/core/SideDialogs.tsx +44 -1
- package/src/core/index.tsx +1 -0
- package/src/core/plugin_translations.ts +87 -0
- package/src/core/pro_plugins.ts +79 -0
- package/src/editor/components/SlashCommandMenu.tsx +4 -1
- package/src/editor/extensions/Image.ts +17 -1
- package/src/editor/hooks/useProseMirror.ts +5 -0
- package/src/editor/index.ts +11 -1
- package/src/editor/lazy_editor.tsx +33 -0
- package/src/form/EntityForm.tsx +31 -10
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +6 -1
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +35 -4
- package/src/hooks/index.tsx +2 -0
- package/src/hooks/useBuildNavigationController.tsx +70 -15
- package/src/hooks/useFireCMSContext.tsx +7 -3
- package/src/hooks/useLicenseStatus.tsx +32 -0
- package/src/hooks/useProjectLog.tsx +27 -9
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/hooks/useTranslation.ts +3 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +199 -25
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +37 -15
- package/src/internal/useBuildSideEntityController.tsx +19 -5
- package/src/locales/de.ts +23 -2
- package/src/locales/en.ts +26 -3
- package/src/locales/es.ts +23 -2
- package/src/locales/fr.ts +23 -2
- package/src/locales/hi.ts +23 -2
- package/src/locales/it.ts +23 -2
- package/src/locales/pl.ts +23 -2
- package/src/locales/pt.ts +23 -2
- package/src/preview/components/DatePreview.tsx +2 -4
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/preview/components/StorageThumbnail.tsx +24 -2
- package/src/routes/FireCMSRoute.tsx +11 -3
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +16 -0
- package/src/types/datasource.ts +67 -4
- package/src/types/entities.ts +24 -1
- package/src/types/entity_actions.tsx +6 -0
- package/src/types/entity_callbacks.ts +24 -0
- package/src/types/firecms.tsx +19 -1
- package/src/types/firecms_context.tsx +9 -0
- package/src/types/index.ts +1 -0
- package/src/types/license.ts +62 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +23 -1
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +7 -0
- package/src/types/translations.ts +41 -2
- package/src/util/__tests__/collections.test.ts +32 -0
- package/src/util/__tests__/urls.test.ts +131 -0
- package/src/util/collections.ts +1 -1
- package/src/util/date_fns_locales.ts +168 -0
- package/src/util/entities.ts +3 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/index.ts +2 -0
- package/src/util/lazy_eager.tsx +30 -8
- package/src/util/navigation_from_path.ts +25 -2
- package/src/util/navigation_utils.ts +214 -3
- package/src/util/parent_references_from_path.ts +42 -3
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +8 -0
- package/src/util/urls.ts +52 -0
- package/src/util/useStorageUploadController.tsx +6 -1
package/src/core/index.tsx
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { i18n } from "i18next";
|
|
2
|
+
import { FireCMSPlugin, PluginTranslations } from "../types";
|
|
3
|
+
|
|
4
|
+
const FIRECMS_NS = "firecms_core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Record of which bundles have already been written to which locale, so the
|
|
8
|
+
* same one is never written twice.
|
|
9
|
+
*
|
|
10
|
+
* Plugin translation objects are module-level constants and a lazily loaded one
|
|
11
|
+
* resolves to the same module object every time, so identity is a sound test.
|
|
12
|
+
*/
|
|
13
|
+
export type AppliedBundles = Map<string, Set<object>>;
|
|
14
|
+
|
|
15
|
+
export function createAppliedBundles(): AppliedBundles {
|
|
16
|
+
return new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write the plugins' strings for one language into a live i18next instance.
|
|
21
|
+
*
|
|
22
|
+
* Only the language in use is resolved, plus English, which every other language
|
|
23
|
+
* falls back to — a locale given as a function is a bundle that has not been
|
|
24
|
+
* downloaded yet, and downloading all of them would undo the point of deferring
|
|
25
|
+
* them. Plain records are applied for every locale, as before: they are already
|
|
26
|
+
* in the bundle, so there is nothing to save by holding them back.
|
|
27
|
+
*
|
|
28
|
+
* `applied` is what stops the injection feeding itself. A write raises a store
|
|
29
|
+
* event; the event repaints everything subscribed to this instance, including
|
|
30
|
+
* the component that owns the `plugins` array — the collection editor and SaaS
|
|
31
|
+
* plugin hooks both call `useTranslation` — and that array is an inline literal,
|
|
32
|
+
* so its fresh identity re-runs the effect that called this. react-i18next
|
|
33
|
+
* happens to stop re-rendering after one pass, but that is a bail-out rather
|
|
34
|
+
* than a guarantee, and the redundant writes are worth avoiding regardless.
|
|
35
|
+
*
|
|
36
|
+
* Returns a function that reports whether the caller is still mounted, so a
|
|
37
|
+
* bundle that arrives after unmount is dropped rather than written.
|
|
38
|
+
*/
|
|
39
|
+
export function applyPluginTranslations({
|
|
40
|
+
i18n,
|
|
41
|
+
plugins,
|
|
42
|
+
language,
|
|
43
|
+
applied,
|
|
44
|
+
isActive = () => true
|
|
45
|
+
}: {
|
|
46
|
+
i18n: i18n;
|
|
47
|
+
plugins?: FireCMSPlugin<any, any, any>[];
|
|
48
|
+
language: string;
|
|
49
|
+
applied: AppliedBundles;
|
|
50
|
+
isActive?: () => boolean;
|
|
51
|
+
}) {
|
|
52
|
+
const wanted = new Set([language.split("-")[0], "en"]);
|
|
53
|
+
|
|
54
|
+
const add = (locale: string, bundle: PluginTranslations) => {
|
|
55
|
+
if (!isActive()) return;
|
|
56
|
+
let written = applied.get(locale);
|
|
57
|
+
if (!written) {
|
|
58
|
+
written = new Set();
|
|
59
|
+
applied.set(locale, written);
|
|
60
|
+
}
|
|
61
|
+
if (written.has(bundle)) return;
|
|
62
|
+
written.add(bundle);
|
|
63
|
+
i18n.addResourceBundle(
|
|
64
|
+
locale,
|
|
65
|
+
FIRECMS_NS,
|
|
66
|
+
bundle,
|
|
67
|
+
true, // deep merge
|
|
68
|
+
true // overwrite
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
plugins?.forEach((plugin) => {
|
|
73
|
+
if (!plugin.i18n) return;
|
|
74
|
+
Object.entries(plugin.i18n).forEach(([locale, translations]) => {
|
|
75
|
+
if (typeof translations === "function") {
|
|
76
|
+
if (!wanted.has(locale.split("-")[0])) return;
|
|
77
|
+
translations()
|
|
78
|
+
.then((bundle) => add(locale, bundle))
|
|
79
|
+
.catch(() => {
|
|
80
|
+
// A locale that fails to load falls back to English.
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
add(locale, translations);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AccessResponse, FireCMSPlugin } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keys of the plugins that need a FireCMS PRO license.
|
|
5
|
+
*
|
|
6
|
+
* Kept in sync by hand with `PRO_PLUGIN_KEYS` in
|
|
7
|
+
* `saas_backend/functions/src/business_rules/licenses.ts`, which decides from
|
|
8
|
+
* the same list whether a project needs a license. Change both together.
|
|
9
|
+
*
|
|
10
|
+
* @group Core
|
|
11
|
+
*/
|
|
12
|
+
export const PRO_PLUGIN_KEYS: readonly string[] = [
|
|
13
|
+
"collection_editor",
|
|
14
|
+
"user_management",
|
|
15
|
+
"import_export",
|
|
16
|
+
"import",
|
|
17
|
+
"export",
|
|
18
|
+
"entity_history",
|
|
19
|
+
"data_enhancement",
|
|
20
|
+
"datatalk"
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* PRO plugins that stay mounted while PRO is paused. `user_management` supplies
|
|
25
|
+
* sign-in checks and roles, so removing it would lock users out of the CMS
|
|
26
|
+
* instead of pausing a feature; its own Users and Roles views show a notice.
|
|
27
|
+
*/
|
|
28
|
+
const KEPT_WHILE_PAUSED: readonly string[] = ["user_management"];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether PRO features are paused for this license status. Any `blocked`
|
|
32
|
+
* response pauses them, whatever its `licenseState` (`expired` and
|
|
33
|
+
* `invalid_project` both come with it). An unknown status pauses nothing.
|
|
34
|
+
*
|
|
35
|
+
* @group Core
|
|
36
|
+
*/
|
|
37
|
+
export function isProPaused(licenseStatus?: AccessResponse | null): boolean {
|
|
38
|
+
return Boolean(licenseStatus?.blocked);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether this plugin is dropped while PRO is paused.
|
|
43
|
+
*
|
|
44
|
+
* @group Core
|
|
45
|
+
*/
|
|
46
|
+
export function isPausedPlugin(plugin: Pick<FireCMSPlugin, "key">): boolean {
|
|
47
|
+
return PRO_PLUGIN_KEYS.includes(plugin.key) && !KEPT_WHILE_PAUSED.includes(plugin.key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Where to get or fix the license for this status: the server's
|
|
52
|
+
* `subscribeUrl` when it is an https URL, otherwise the license page on
|
|
53
|
+
* app.firecms.co with the project, when known.
|
|
54
|
+
*
|
|
55
|
+
* @group Core
|
|
56
|
+
*/
|
|
57
|
+
export function getLicenseSubscribeUrl(licenseStatus?: AccessResponse | null): string {
|
|
58
|
+
const subscribeUrl = licenseStatus?.subscribeUrl;
|
|
59
|
+
if (subscribeUrl && /^https:\/\//i.test(subscribeUrl))
|
|
60
|
+
return subscribeUrl;
|
|
61
|
+
const params = new URLSearchParams({ intent: "pro" });
|
|
62
|
+
if (licenseStatus?.projectId) params.set("projectId", licenseStatus.projectId);
|
|
63
|
+
return `https://app.firecms.co/subscriptions?${params.toString()}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The plugins that stay active under this license status. Returns the same
|
|
68
|
+
* array when nothing is dropped, so it can feed memo and effect dependencies.
|
|
69
|
+
*
|
|
70
|
+
* @group Core
|
|
71
|
+
*/
|
|
72
|
+
export function filterPausedPlugins<P extends Pick<FireCMSPlugin, "key">>(
|
|
73
|
+
plugins: P[] | undefined,
|
|
74
|
+
licenseStatus?: AccessResponse | null
|
|
75
|
+
): P[] | undefined {
|
|
76
|
+
if (!plugins || !isProPaused(licenseStatus)) return plugins;
|
|
77
|
+
const active = plugins.filter(plugin => !isPausedPlugin(plugin));
|
|
78
|
+
return active.length === plugins.length ? plugins : active;
|
|
79
|
+
}
|
|
@@ -198,11 +198,14 @@ const autocompleteSuggestionItem: SuggestionItem = {
|
|
|
198
198
|
let buffer = "";
|
|
199
199
|
const result = await aiController.autocomplete(textBeforeCursor, textAfterCursor, (delta) => {
|
|
200
200
|
buffer += delta;
|
|
201
|
-
if (delta.length !== 0) {
|
|
201
|
+
if (delta.length !== 0 && !view.isDestroyed) {
|
|
202
202
|
textLoadingCommands.toggleLoadingDecoration(view.state, view.dispatch, buffer);
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
// The editor may have been unmounted while the completion streamed in.
|
|
207
|
+
if (view.isDestroyed) return;
|
|
208
|
+
|
|
206
209
|
// Insert parsed text result at cursor natively
|
|
207
210
|
try {
|
|
208
211
|
// The AI controller might stream literal "\n" characters depending on its JSON decoding layer.
|
|
@@ -4,9 +4,20 @@ import { Plugin, PluginKey } from "prosemirror-state";
|
|
|
4
4
|
|
|
5
5
|
export type UploadFn = (image: File) => Promise<string>;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Shows the image as a placeholder at `pos`, uploads it, and swaps the
|
|
9
|
+
* placeholder for the uploaded image once it has loaded.
|
|
10
|
+
*
|
|
11
|
+
* Every step after the first runs asynchronously — the FileReader callback that
|
|
12
|
+
* calls this, the upload, and the preload of the uploaded image — and the editor
|
|
13
|
+
* can be unmounted during any of them (the user closes the entity or navigates
|
|
14
|
+
* away while an image is still uploading). Dispatching on a destroyed
|
|
15
|
+
* `EditorView` throws inside ProseMirror (`docView` is null), so each step
|
|
16
|
+
* checks the view is still alive before touching it.
|
|
17
|
+
*/
|
|
7
18
|
export async function onFileRead(view: EditorView, readerEvent: ProgressEvent<FileReader>, pos: number, upload: UploadFn, image: File) {
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
if (view.isDestroyed) return;
|
|
10
21
|
|
|
11
22
|
// @ts-ignore
|
|
12
23
|
const plugin = view.state.plugins.find((p: Plugin) => p.key === ImagePluginKey.key);
|
|
@@ -31,7 +42,12 @@ export async function onFileRead(view: EditorView, readerEvent: ProgressEvent<Fi
|
|
|
31
42
|
const src = await upload(image);
|
|
32
43
|
console.debug("Uploaded image", src);
|
|
33
44
|
|
|
45
|
+
// The upload is kept (the caller stored the file), but there is no longer
|
|
46
|
+
// an editor to put it in.
|
|
47
|
+
if (view.isDestroyed) return;
|
|
48
|
+
|
|
34
49
|
const replacePlaceholder = () => {
|
|
50
|
+
if (view.isDestroyed) return;
|
|
35
51
|
// Retrieve the LATEST state after the async upload
|
|
36
52
|
let currentDecos = plugin.getState(view.state) as DecorationSet;
|
|
37
53
|
const foundDecos = currentDecos.find(undefined, undefined, spec => spec.id === decoId);
|
|
@@ -58,6 +58,11 @@ export function useProseMirror({ initialContent, editable = true, handleImageUpl
|
|
|
58
58
|
state: defaultState,
|
|
59
59
|
editable: () => editable,
|
|
60
60
|
dispatchTransaction: (tr: Transaction) => {
|
|
61
|
+
// Async work (image uploads and their preload, AI autocomplete
|
|
62
|
+
// streams) can finish after the editor unmounted. Applying a
|
|
63
|
+
// transaction to a destroyed view throws inside ProseMirror
|
|
64
|
+
// (`docView` is null), so drop it: there is nothing to update.
|
|
65
|
+
if (editorView.isDestroyed) return;
|
|
61
66
|
const newState = editorView.state.apply(tr);
|
|
62
67
|
editorView.updateState(newState);
|
|
63
68
|
setState(newState);
|
package/src/editor/index.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// The editor's runtime entry point is the lazy wrapper, not the implementation:
|
|
2
|
+
// a static re-export here would pull ProseMirror into every consumer of
|
|
3
|
+
// `@firecms/core`. The types come straight from the implementation and are
|
|
4
|
+
// erased at build time, so they cost nothing.
|
|
5
|
+
export { FireCMSEditor } from "./lazy_editor";
|
|
6
|
+
export type {
|
|
7
|
+
CustomEditorComponent,
|
|
8
|
+
MarkdownEditorConfig,
|
|
9
|
+
FireCMSEditorTextSize,
|
|
10
|
+
FireCMSEditorProps
|
|
11
|
+
} from "./editor";
|
|
2
12
|
export * from "./types";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import { CircularProgressCenter } from "../components/CircularProgressCenter";
|
|
6
|
+
import type { FireCMSEditorProps } from "./editor";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `FireCMSEditor`, loaded on demand.
|
|
10
|
+
*
|
|
11
|
+
* The editor is built on ProseMirror — roughly 250KB across a dozen packages —
|
|
12
|
+
* and is only reachable from a markdown field, which most views never render.
|
|
13
|
+
* `MarkdownEditorFieldBinding` already loaded it lazily, but that split was
|
|
14
|
+
* defeated by `src/index.ts` re-exporting the editor from the package barrel:
|
|
15
|
+
* a static re-export puts the module back in the entry graph regardless of who
|
|
16
|
+
* imports it dynamically. So ProseMirror sat in `@firecms/core` for everyone.
|
|
17
|
+
*
|
|
18
|
+
* The name and props are unchanged, and this component carries its own Suspense
|
|
19
|
+
* boundary, so it stays a drop-in for the synchronous component it replaces —
|
|
20
|
+
* a consumer rendering it without a boundary of their own still works.
|
|
21
|
+
*/
|
|
22
|
+
const LazyFireCMSEditor = React.lazy(() =>
|
|
23
|
+
import("./editor").then((module) => ({ default: module.FireCMSEditor })));
|
|
24
|
+
|
|
25
|
+
export function FireCMSEditor(props: FireCMSEditorProps) {
|
|
26
|
+
return (
|
|
27
|
+
<React.Suspense fallback={<CircularProgressCenter/>}>
|
|
28
|
+
<LazyFireCMSEditor {...props}/>
|
|
29
|
+
</React.Suspense>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
FireCMSEditor.displayName = "FireCMSEditor";
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
|
48
48
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
49
49
|
import { ValidationError } from "yup";
|
|
50
50
|
import {
|
|
51
|
+
entityCacheKey,
|
|
51
52
|
flattenKeys,
|
|
52
53
|
getEntityFromCache,
|
|
53
54
|
removeEntityFromCache,
|
|
@@ -64,6 +65,8 @@ export type OnUpdateParams = {
|
|
|
64
65
|
entity: Entity<any>,
|
|
65
66
|
status: EntityStatus,
|
|
66
67
|
path: string,
|
|
68
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
69
|
+
pathSegments?: string[],
|
|
67
70
|
entityId?: string;
|
|
68
71
|
selectedTab?: string;
|
|
69
72
|
collection: EntityCollection<any>
|
|
@@ -71,6 +74,11 @@ export type OnUpdateParams = {
|
|
|
71
74
|
|
|
72
75
|
export type EntityFormProps<M extends Record<string, any>> = {
|
|
73
76
|
path: string;
|
|
77
|
+
/**
|
|
78
|
+
* `path` split at its real segment boundaries; forwarded to the datasource for
|
|
79
|
+
* generateEntityId and checkUniqueField. Never derived from `path`.
|
|
80
|
+
*/
|
|
81
|
+
pathSegments?: string[];
|
|
74
82
|
fullIdPath?: string;
|
|
75
83
|
collection: EntityCollection<M>;
|
|
76
84
|
entityId?: string;
|
|
@@ -214,6 +222,7 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
|
|
|
214
222
|
|
|
215
223
|
export function EntityForm<M extends Record<string, any>>({
|
|
216
224
|
path,
|
|
225
|
+
pathSegments,
|
|
217
226
|
fullIdPath,
|
|
218
227
|
entityId: entityIdProp,
|
|
219
228
|
collection,
|
|
@@ -270,6 +279,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
270
279
|
entityId: entityIdProp,
|
|
271
280
|
collection,
|
|
272
281
|
path,
|
|
282
|
+
pathSegments,
|
|
273
283
|
values: valuesToBeSaved
|
|
274
284
|
});
|
|
275
285
|
}, false, 2000);
|
|
@@ -292,7 +302,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
292
302
|
if (mustSetCustomId) {
|
|
293
303
|
return undefined;
|
|
294
304
|
} else {
|
|
295
|
-
return dataSource.generateEntityId(path, collection);
|
|
305
|
+
return dataSource.generateEntityId(path, collection, pathSegments);
|
|
296
306
|
}
|
|
297
307
|
} else {
|
|
298
308
|
return entityIdProp;
|
|
@@ -308,7 +318,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
308
318
|
const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
|
|
309
319
|
|
|
310
320
|
const localChangesDataRaw = useMemo(() => entityId
|
|
311
|
-
? getEntityFromCache(path
|
|
321
|
+
? getEntityFromCache(entityCacheKey(path, entityId))
|
|
312
322
|
: getEntityFromCache(path + "#new"), [entityId, path]);
|
|
313
323
|
|
|
314
324
|
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
@@ -387,8 +397,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
387
397
|
onValuesModified?.(false, initialValues as M);
|
|
388
398
|
},
|
|
389
399
|
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
390
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
391
|
-
if (controller.dirty) {
|
|
400
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
401
|
+
if (controller.dirty && localChangesBackup !== false) {
|
|
392
402
|
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
393
403
|
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
394
404
|
saveEntityToCache(key, touchedValues);
|
|
@@ -468,8 +478,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
468
478
|
removeEntityFromMemoryCache(path + "#new");
|
|
469
479
|
removeEntityFromCache(path + "#new");
|
|
470
480
|
} else {
|
|
471
|
-
removeEntityFromMemoryCache(path
|
|
472
|
-
removeEntityFromCache(path
|
|
481
|
+
removeEntityFromMemoryCache(entityCacheKey(path, entityId));
|
|
482
|
+
removeEntityFromCache(entityCacheKey(path, entityId));
|
|
473
483
|
}
|
|
474
484
|
}
|
|
475
485
|
|
|
@@ -491,6 +501,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
491
501
|
entity: updatedEntity,
|
|
492
502
|
status,
|
|
493
503
|
path,
|
|
504
|
+
pathSegments,
|
|
494
505
|
entityId: updatedEntity.id,
|
|
495
506
|
collection
|
|
496
507
|
});
|
|
@@ -511,16 +522,20 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
511
522
|
previousValues,
|
|
512
523
|
entityId,
|
|
513
524
|
collection,
|
|
514
|
-
path
|
|
525
|
+
path,
|
|
526
|
+
pathSegments
|
|
515
527
|
}: {
|
|
516
528
|
collection: EntityCollection<M>,
|
|
517
529
|
path: string,
|
|
530
|
+
/** `path` split at its real segment boundaries, travelling with the `path` above. */
|
|
531
|
+
pathSegments?: string[],
|
|
518
532
|
entityId: string | undefined,
|
|
519
533
|
values: M,
|
|
520
534
|
previousValues?: M,
|
|
521
535
|
}) => {
|
|
522
536
|
return saveEntityWithCallbacks({
|
|
523
537
|
path,
|
|
538
|
+
pathSegments,
|
|
524
539
|
entityId,
|
|
525
540
|
values,
|
|
526
541
|
previousValues,
|
|
@@ -538,6 +553,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
538
553
|
type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
539
554
|
collection: ResolvedEntityCollection<M>,
|
|
540
555
|
path: string,
|
|
556
|
+
pathSegments?: string[],
|
|
541
557
|
entityId: string | undefined,
|
|
542
558
|
values: EntityValues<M>,
|
|
543
559
|
previousValues?: EntityValues<M>,
|
|
@@ -547,6 +563,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
547
563
|
const onSaveEntityRequest = async ({
|
|
548
564
|
collection,
|
|
549
565
|
path,
|
|
566
|
+
pathSegments,
|
|
550
567
|
entityId,
|
|
551
568
|
values,
|
|
552
569
|
previousValues,
|
|
@@ -560,6 +577,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
560
577
|
return saveEntity({
|
|
561
578
|
collection,
|
|
562
579
|
path,
|
|
580
|
+
pathSegments,
|
|
563
581
|
entityId,
|
|
564
582
|
values,
|
|
565
583
|
previousValues
|
|
@@ -573,6 +591,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
573
591
|
return onSaveEntityRequest({
|
|
574
592
|
collection: resolvedCollection,
|
|
575
593
|
path,
|
|
594
|
+
pathSegments,
|
|
576
595
|
entityId,
|
|
577
596
|
values,
|
|
578
597
|
previousValues: entity?.values,
|
|
@@ -623,13 +642,14 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
623
642
|
const plugins = customizationController.plugins;
|
|
624
643
|
|
|
625
644
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
626
|
-
const parentCollectionIds = navigationController.getParentCollectionIds(path);
|
|
645
|
+
const parentCollectionIds = navigationController.getParentCollectionIds(path, pathSegments);
|
|
627
646
|
|
|
628
647
|
if (plugins && collection) {
|
|
629
648
|
const actionProps: PluginFormActionProps = {
|
|
630
649
|
entityId,
|
|
631
650
|
parentCollectionIds,
|
|
632
651
|
path,
|
|
652
|
+
pathSegments,
|
|
633
653
|
status,
|
|
634
654
|
collection,
|
|
635
655
|
context,
|
|
@@ -692,7 +712,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
692
712
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
693
713
|
name,
|
|
694
714
|
value
|
|
695
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
715
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection, pathSegments),
|
|
696
716
|
[dataSource, path, entityId]);
|
|
697
717
|
|
|
698
718
|
const validationSchema = useMemo(() => entityId
|
|
@@ -890,6 +910,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
890
910
|
const dialogActions = <EntityFormActionsComponent
|
|
891
911
|
collection={resolvedCollection}
|
|
892
912
|
path={path}
|
|
913
|
+
pathSegments={pathSegments}
|
|
893
914
|
fullPath={path}
|
|
894
915
|
fullIdPath={fullIdPath}
|
|
895
916
|
entity={entity}
|
|
@@ -924,7 +945,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
924
945
|
|
|
925
946
|
{manualApplyLocalChanges && hasLocalChanges &&
|
|
926
947
|
<LocalChangesMenu
|
|
927
|
-
cacheKey={status === "new" || status === "copy" ? path + "#new" : path
|
|
948
|
+
cacheKey={status === "new" || status === "copy" ? path + "#new" : entityCacheKey(path, entityId)}
|
|
928
949
|
properties={resolvedCollection.properties}
|
|
929
950
|
cachedData={localChangesDataRaw as Partial<M>}
|
|
930
951
|
formex={formex}
|
|
@@ -26,6 +26,8 @@ export interface EntityFormActionsProps {
|
|
|
26
26
|
fullIdPath?: string;
|
|
27
27
|
collection: ResolvedEntityCollection;
|
|
28
28
|
path: string;
|
|
29
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
30
|
+
pathSegments?: string[];
|
|
29
31
|
entity?: Entity;
|
|
30
32
|
layout: "bottom" | "side";
|
|
31
33
|
savingError?: Error;
|
package/src/hooks/data/delete.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type DeleteEntityWithCallbacksProps<M extends Record<string, any>, USER e
|
|
|
46
46
|
export async function deleteEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
47
47
|
dataSource,
|
|
48
48
|
entity,
|
|
49
|
+
pathSegments,
|
|
49
50
|
collection,
|
|
50
51
|
callbacks,
|
|
51
52
|
onDeleteSuccess,
|
|
@@ -62,11 +63,17 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
62
63
|
|
|
63
64
|
console.debug("Deleting entity", entity.path, entity.id);
|
|
64
65
|
|
|
66
|
+
// An explicit `pathSegments` wins, then the ones the entity was loaded with. The prop
|
|
67
|
+
// used to be accepted and silently ignored, which is the same class of bug this field
|
|
68
|
+
// exists to prevent.
|
|
69
|
+
const resolvedPathSegments = pathSegments ?? entity.pathSegments;
|
|
70
|
+
|
|
65
71
|
const entityDeleteProps: EntityOnDeleteProps<M, any> = {
|
|
66
72
|
entity,
|
|
67
73
|
collection,
|
|
68
74
|
entityId: entity.id,
|
|
69
75
|
path: entity.path,
|
|
76
|
+
pathSegments: resolvedPathSegments,
|
|
70
77
|
context
|
|
71
78
|
};
|
|
72
79
|
|
|
@@ -81,6 +88,7 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
return dataSource.deleteEntity({
|
|
91
|
+
pathSegments: resolvedPathSegments,
|
|
84
92
|
entity,
|
|
85
93
|
collection
|
|
86
94
|
}).then(() => {
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -43,6 +43,7 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, any>> =
|
|
|
43
43
|
export async function saveEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
44
44
|
collection,
|
|
45
45
|
path,
|
|
46
|
+
pathSegments,
|
|
46
47
|
entityId,
|
|
47
48
|
values,
|
|
48
49
|
previousValues,
|
|
@@ -67,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
67
68
|
|
|
68
69
|
const customizationController = context.customizationController;
|
|
69
70
|
|
|
70
|
-
const resolvedPath = context.navigation.resolveIdsFrom(path);
|
|
71
|
+
const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
|
|
71
72
|
|
|
72
73
|
const callbacks = collection.callbacks;
|
|
73
74
|
if (callbacks?.onPreSave) {
|
|
@@ -83,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
83
84
|
updatedValues = await callbacks.onPreSave({
|
|
84
85
|
collection: resolvedCollection,
|
|
85
86
|
path,
|
|
87
|
+
pathSegments,
|
|
86
88
|
resolvedPath,
|
|
87
89
|
entityId,
|
|
88
90
|
values,
|
|
@@ -103,6 +105,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
103
105
|
return dataSource.saveEntity({
|
|
104
106
|
collection,
|
|
105
107
|
path: resolvedPath,
|
|
108
|
+
pathSegments,
|
|
106
109
|
entityId,
|
|
107
110
|
values: updatedValues,
|
|
108
111
|
previousValues,
|
|
@@ -121,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
121
124
|
callbacks.onSaveSuccess({
|
|
122
125
|
collection: resolvedCollection,
|
|
123
126
|
path,
|
|
127
|
+
pathSegments,
|
|
124
128
|
resolvedPath,
|
|
125
129
|
entityId: entity.id,
|
|
126
130
|
values: updatedValues,
|
|
@@ -150,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
150
154
|
callbacks.onSaveFailure({
|
|
151
155
|
collection: resolvedCollection,
|
|
152
156
|
path,
|
|
157
|
+
pathSegments,
|
|
153
158
|
resolvedPath,
|
|
154
159
|
entityId,
|
|
155
160
|
values: updatedValues,
|
|
@@ -13,6 +13,12 @@ export interface CollectionFetchProps<M extends Record<string, any>> {
|
|
|
13
13
|
* Absolute collection path
|
|
14
14
|
*/
|
|
15
15
|
path: string;
|
|
16
|
+
/**
|
|
17
|
+
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
18
|
+
* entity id containing "/" stays unambiguous. Never derived from `path` — see
|
|
19
|
+
* `pathSegments` on the datasource props.
|
|
20
|
+
*/
|
|
21
|
+
pathSegments?: string[];
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* collection of the entity displayed by this collection
|
|
@@ -63,6 +69,7 @@ export interface CollectionFetchResult<M extends Record<string, any>> {
|
|
|
63
69
|
export function useCollectionFetch<M extends Record<string, any>, USER extends User>(
|
|
64
70
|
{
|
|
65
71
|
path: inputPath,
|
|
72
|
+
pathSegments: inputPathSegments,
|
|
66
73
|
collection,
|
|
67
74
|
filterValues,
|
|
68
75
|
sortBy,
|
|
@@ -73,7 +80,13 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
73
80
|
const dataSource = useDataSource(collection);
|
|
74
81
|
const navigationController = useNavigationController();
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
// Resolved together so the two representations always describe the same chain.
|
|
84
|
+
// Segments are resolved, never derived: only collection ids are swapped for their
|
|
85
|
+
// real paths, and entity ids are carried through whole (see useEntityFetch).
|
|
86
|
+
const pathSegments = inputPathSegments
|
|
87
|
+
? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
|
|
88
|
+
: undefined;
|
|
89
|
+
const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
|
|
77
90
|
|
|
78
91
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
79
92
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
@@ -91,6 +104,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
91
104
|
setDataLoading(true);
|
|
92
105
|
|
|
93
106
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
107
|
+
// See useEntityFetch: entities carry the segments they were loaded with.
|
|
108
|
+
if (pathSegments) {
|
|
109
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments });
|
|
110
|
+
}
|
|
94
111
|
if (collection.callbacks?.onFetch) {
|
|
95
112
|
try {
|
|
96
113
|
entities = await Promise.all(
|
|
@@ -98,6 +115,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
98
115
|
collection.callbacks!.onFetch!({
|
|
99
116
|
collection,
|
|
100
117
|
path,
|
|
118
|
+
pathSegments,
|
|
101
119
|
entity,
|
|
102
120
|
context
|
|
103
121
|
})));
|
|
@@ -124,6 +142,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
124
142
|
if (dataSource.listenCollection) {
|
|
125
143
|
return dataSource.listenCollection<M>({
|
|
126
144
|
path,
|
|
145
|
+
pathSegments,
|
|
127
146
|
collection,
|
|
128
147
|
onUpdate: onEntitiesUpdate,
|
|
129
148
|
onError,
|
|
@@ -137,6 +156,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
137
156
|
} else {
|
|
138
157
|
dataSource.fetchCollection<M>({
|
|
139
158
|
path,
|
|
159
|
+
pathSegments,
|
|
140
160
|
collection,
|
|
141
161
|
searchString,
|
|
142
162
|
filter: filterValues,
|