@firecms/core 3.3.0 → 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.
- package/README.md +4 -0
- package/dist/app/useApp.d.ts +1 -0
- package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
- 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/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/EntityPreview.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/DefaultDrawer.d.ts +10 -3
- package/dist/core/EntityEditView.d.ts +9 -1
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/field_configs.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-BF5vcQPY.js +703 -0
- package/dist/de-BF5vcQPY.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/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/fr-DH9_wyFI.js +703 -0
- package/dist/fr-DH9_wyFI.js.map +1 -0
- package/dist/hi-BM8dm1Lu.js +703 -0
- package/dist/hi-BM8dm1Lu.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 +4516 -10668
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9403 -6774
- package/dist/index.umd.js.map +1 -1
- package/dist/it-D3K1gfe4.js +703 -0
- package/dist/it-D3K1gfe4.js.map +1 -0
- package/dist/locales/pl.d.ts +2 -0
- package/dist/pl-CP1zeLzn.js +705 -0
- package/dist/pl-CP1zeLzn.js.map +1 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
- package/dist/pt-Cus7eKQR.js +705 -0
- package/dist/pt-Cus7eKQR.js.map +1 -0
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +31 -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 +55 -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 +43 -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/entities.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/geopoint.d.ts +22 -0
- package/dist/util/index.d.ts +3 -0
- package/dist/util/lazy_eager.d.ts +3 -3
- package/dist/util/navigation_blocking.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +17 -0
- package/dist/util/navigation_utils.d.ts +107 -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 +17 -11
- package/src/app/Scaffold.tsx +41 -44
- package/src/app/useApp.tsx +1 -0
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
- package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
- 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 +8 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +45 -41
- package/src/components/ErrorBoundary.tsx +12 -18
- package/src/components/LicenseBanner.tsx +207 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -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/VirtualTable/VirtualTable.tsx +19 -8
- 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 +126 -163
- 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/DefaultDrawer.tsx +150 -64
- package/src/core/DrawerNavigationGroup.tsx +27 -29
- package/src/core/DrawerNavigationItem.tsx +10 -12
- package/src/core/EntityEditView.tsx +77 -35
- 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/field_configs.tsx +15 -0
- 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/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
- package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
- package/src/form/index.tsx +1 -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 -23
- 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 +20 -5
- package/src/locales/de.ts +26 -4
- package/src/locales/en.ts +30 -6
- package/src/locales/es.ts +26 -4
- package/src/locales/fr.ts +26 -4
- package/src/locales/hi.ts +26 -4
- package/src/locales/it.ts +26 -4
- package/src/locales/pl.ts +752 -0
- package/src/locales/pt.ts +26 -4
- package/src/preview/PropertyPreview.tsx +12 -0
- 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/preview/index.ts +1 -0
- package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
- package/src/routes/FireCMSRoute.tsx +54 -27
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +34 -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 +63 -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 +43 -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 +16 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +3 -0
- package/src/util/lazy_eager.tsx +30 -8
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +47 -7
- package/src/util/navigation_utils.ts +250 -5
- package/src/util/parent_references_from_path.ts +45 -4
- 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
|
@@ -63,7 +63,7 @@ export function EntityPreview({
|
|
|
63
63
|
|
|
64
64
|
const navigationController = useNavigationController();
|
|
65
65
|
|
|
66
|
-
const collection = collectionProp ?? navigationController.getCollection(entity.path);
|
|
66
|
+
const collection = collectionProp ?? navigationController.getCollection(entity.path, false, entity.pathSegments);
|
|
67
67
|
|
|
68
68
|
if (!collection) {
|
|
69
69
|
throw Error(`Couldn't find the corresponding collection view for the path: ${entity.path}`);
|
|
@@ -177,6 +177,9 @@ export function EntityPreview({
|
|
|
177
177
|
sideEntityController.open({
|
|
178
178
|
entityId: entity.id,
|
|
179
179
|
path: entity.path,
|
|
180
|
+
// Paired with `entity.path` above; undefined if the entity was
|
|
181
|
+
// loaded without them.
|
|
182
|
+
pathSegments: entity.pathSegments,
|
|
180
183
|
collection,
|
|
181
184
|
updateUrl: true
|
|
182
185
|
});
|
|
@@ -200,43 +203,44 @@ export type EntityPreviewContainerProps = {
|
|
|
200
203
|
onClick?: (e: React.SyntheticEvent) => void;
|
|
201
204
|
};
|
|
202
205
|
|
|
203
|
-
export
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
206
|
+
export function EntityPreviewContainer({
|
|
207
|
+
children,
|
|
208
|
+
hover,
|
|
209
|
+
onClick,
|
|
210
|
+
size = "medium",
|
|
211
|
+
style,
|
|
212
|
+
className,
|
|
213
|
+
fullwidth = true,
|
|
214
|
+
ref
|
|
215
|
+
}: EntityPreviewContainerProps & { ref?: React.Ref<HTMLDivElement> }) {
|
|
216
|
+
const divClassName = cls(
|
|
217
|
+
"bg-white dark:bg-surface-900",
|
|
218
|
+
"min-h-[44px]",
|
|
219
|
+
fullwidth ? "w-full" : "",
|
|
220
|
+
"items-center",
|
|
221
|
+
hover ? "hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800" : "",
|
|
222
|
+
size === "small" ? "p-1" : "px-2 py-1",
|
|
223
|
+
"flex border rounded-lg",
|
|
224
|
+
onClick ? "cursor-pointer" : "",
|
|
225
|
+
defaultBorderMixin,
|
|
226
|
+
className
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const handleClick = onClick
|
|
230
|
+
? (event: React.MouseEvent<HTMLDivElement>) => {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
onClick(event);
|
|
233
|
+
}
|
|
234
|
+
: undefined;
|
|
235
|
+
|
|
236
|
+
const divProps: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> } = {
|
|
237
|
+
ref,
|
|
238
|
+
tabIndex: 0,
|
|
239
|
+
style,
|
|
240
|
+
className: divClassName,
|
|
241
|
+
onClick: handleClick,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return <div {...divProps}>{children}</div>;
|
|
245
|
+
}
|
|
246
|
+
|
|
@@ -4,17 +4,16 @@ import { useTranslation } from "../hooks/useTranslation";
|
|
|
4
4
|
import { ErrorIcon, Typography } from "@firecms/ui";
|
|
5
5
|
|
|
6
6
|
export class ErrorBoundary extends React.Component<PropsWithChildren<Record<string, unknown>>, {
|
|
7
|
-
|
|
8
|
-
error?: Error
|
|
7
|
+
error: Error | null
|
|
9
8
|
}> {
|
|
10
9
|
constructor(props: any) {
|
|
11
10
|
super(props);
|
|
12
|
-
this.state = {
|
|
11
|
+
this.state = { error: null };
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
// eslint-disable-next-line n/handle-callback-err
|
|
16
15
|
static getDerivedStateFromError(error: Error) {
|
|
17
|
-
return {
|
|
16
|
+
return { error };
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
@@ -23,9 +22,8 @@ export class ErrorBoundary extends React.Component<PropsWithChildren<Record<stri
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
render() {
|
|
26
|
-
if (this.state.
|
|
27
|
-
|
|
28
|
-
return <FallbackView message={this.state.error?.message}/>;
|
|
25
|
+
if (this.state.error) {
|
|
26
|
+
return <FallbackView message={this.state.error.message}/>;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
return this.props.children;
|
|
@@ -35,18 +33,14 @@ export class ErrorBoundary extends React.Component<PropsWithChildren<Record<stri
|
|
|
35
33
|
function FallbackView({ message }: { message?: string }) {
|
|
36
34
|
const { t } = useTranslation();
|
|
37
35
|
return (
|
|
38
|
-
<div className="
|
|
39
|
-
<div
|
|
40
|
-
|
|
41
|
-
<div className="
|
|
42
|
-
<ErrorIcon/>
|
|
43
|
-
<div className="ml-4">{t("error")}</div>
|
|
44
|
-
</div>
|
|
45
|
-
<div className="flex justify-center text-gray-500 dark:text-gray-400">
|
|
46
|
-
{/* Error message is purposely removed since it's hard to access state here, but typical ErrorBoundary fallback doesn't always show the raw message */}
|
|
47
|
-
{t("see_console_details")}
|
|
48
|
-
</div>
|
|
36
|
+
<div className="flex flex-col m-2">
|
|
37
|
+
<div className="flex items-center m-2">
|
|
38
|
+
<ErrorIcon color={"error"} size={"small"}/>
|
|
39
|
+
<div className="ml-4">{t("error")}</div>
|
|
49
40
|
</div>
|
|
41
|
+
<Typography variant={"caption"}>
|
|
42
|
+
{message ?? t("see_console_details")}
|
|
43
|
+
</Typography>
|
|
50
44
|
</div>
|
|
51
45
|
);
|
|
52
46
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Alert, Button, CloseIcon, IconButton } from "@firecms/ui";
|
|
3
|
+
import { AccessResponse } from "../types";
|
|
4
|
+
import { getLicenseSubscribeUrl } from "../core/pro_plugins";
|
|
5
|
+
import { useLicenseStatus } from "../hooks/useLicenseStatus";
|
|
6
|
+
import { useTranslation } from "../hooks/useTranslation";
|
|
7
|
+
|
|
8
|
+
const DISMISSED_STORAGE_KEY = "firecms_license_banner_dismissed";
|
|
9
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @group Components
|
|
13
|
+
*/
|
|
14
|
+
export interface LicenseBannerProps {
|
|
15
|
+
/**
|
|
16
|
+
* Applied to the banner's outer element when there is something to show.
|
|
17
|
+
*/
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: React.CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type BannerContent = {
|
|
23
|
+
state: string;
|
|
24
|
+
color: "info" | "warning";
|
|
25
|
+
message: string;
|
|
26
|
+
linkLabel: string;
|
|
27
|
+
href: string;
|
|
28
|
+
dismissible: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tells the user where their FireCMS PRO license stands: days left in the
|
|
33
|
+
* trial, or why PRO is paused (the trial ended, the key is not linked to this
|
|
34
|
+
* project, or the license pays for other projects). Renders nothing when
|
|
35
|
+
* licensed, when no license is needed, and while the status is unknown.
|
|
36
|
+
*
|
|
37
|
+
* The default `Scaffold` already renders it above the main content. Place it
|
|
38
|
+
* yourself only in a custom layout.
|
|
39
|
+
*
|
|
40
|
+
* @group Components
|
|
41
|
+
*/
|
|
42
|
+
export function LicenseBanner({
|
|
43
|
+
className,
|
|
44
|
+
style
|
|
45
|
+
}: LicenseBannerProps) {
|
|
46
|
+
|
|
47
|
+
const licenseStatus = useLicenseStatus();
|
|
48
|
+
const { t, i18n } = useTranslation();
|
|
49
|
+
|
|
50
|
+
// States dismissed in this browser session; a different state shows again.
|
|
51
|
+
const [dismissed, setDismissed] = useState<string[]>(readDismissed);
|
|
52
|
+
|
|
53
|
+
// `language`, not `resolvedLanguage`: locale bundles are added after init,
|
|
54
|
+
// and `resolvedLanguage` stays on the English fallback when they are.
|
|
55
|
+
const content = licenseStatus
|
|
56
|
+
? buildBannerContent(licenseStatus, t, i18n?.language)
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
if (!content || (content.dismissible && dismissed.includes(content.state)))
|
|
60
|
+
return null;
|
|
61
|
+
|
|
62
|
+
const dismiss = () => {
|
|
63
|
+
const next = [...dismissed, content.state];
|
|
64
|
+
writeDismissed(next);
|
|
65
|
+
setDismissed(next);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={className} style={style}>
|
|
70
|
+
<Alert
|
|
71
|
+
color={content.color}
|
|
72
|
+
size={"small"}
|
|
73
|
+
className={"text-sm"}
|
|
74
|
+
action={<div className={"flex flex-row items-center gap-1 flex-shrink-0"}>
|
|
75
|
+
<Button
|
|
76
|
+
variant={"text"}
|
|
77
|
+
size={"small"}
|
|
78
|
+
component={"a"}
|
|
79
|
+
href={content.href}
|
|
80
|
+
target={"_blank"}
|
|
81
|
+
rel={"noopener noreferrer"}>
|
|
82
|
+
{content.linkLabel}
|
|
83
|
+
</Button>
|
|
84
|
+
{content.dismissible && <IconButton
|
|
85
|
+
size={"small"}
|
|
86
|
+
aria-label={t("close")}
|
|
87
|
+
onClick={dismiss}>
|
|
88
|
+
<CloseIcon size={"small"}/>
|
|
89
|
+
</IconButton>}
|
|
90
|
+
</div>}>
|
|
91
|
+
{content.message}
|
|
92
|
+
</Alert>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type Translate = ReturnType<typeof useTranslation>["t"];
|
|
98
|
+
|
|
99
|
+
function buildBannerContent(status: AccessResponse,
|
|
100
|
+
t: Translate,
|
|
101
|
+
language: string | undefined): BannerContent | null {
|
|
102
|
+
const href = getLicenseSubscribeUrl(status);
|
|
103
|
+
switch (status.licenseState) {
|
|
104
|
+
case "trial": {
|
|
105
|
+
const days = trialDaysLeft(status);
|
|
106
|
+
if (days === null) return null;
|
|
107
|
+
return {
|
|
108
|
+
state: "trial",
|
|
109
|
+
color: "info",
|
|
110
|
+
message: t("license_trial_banner", {
|
|
111
|
+
count: days,
|
|
112
|
+
days: String(days)
|
|
113
|
+
}),
|
|
114
|
+
linkLabel: t("license_get_license"),
|
|
115
|
+
href,
|
|
116
|
+
dismissible: true
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
case "expired": {
|
|
120
|
+
const date = formatDate(status.trialEndsAt, language);
|
|
121
|
+
return {
|
|
122
|
+
state: "expired",
|
|
123
|
+
color: "warning",
|
|
124
|
+
message: date
|
|
125
|
+
? t("license_expired_banner", { date })
|
|
126
|
+
: t("license_expired_banner_undated"),
|
|
127
|
+
linkLabel: t("license_get_license"),
|
|
128
|
+
href,
|
|
129
|
+
dismissible: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
case "invalid_project":
|
|
133
|
+
return {
|
|
134
|
+
state: "invalid_project",
|
|
135
|
+
color: "warning",
|
|
136
|
+
message: status.projectId
|
|
137
|
+
? t("license_invalid_project_banner", { projectId: status.projectId })
|
|
138
|
+
: t("license_invalid_project_banner_unnamed"),
|
|
139
|
+
linkLabel: t("license_add_project_to_license"),
|
|
140
|
+
href,
|
|
141
|
+
dismissible: false
|
|
142
|
+
};
|
|
143
|
+
case "over_quota": {
|
|
144
|
+
const licensed = status.licensedProjects;
|
|
145
|
+
if (!isCount(licensed)) return null;
|
|
146
|
+
return {
|
|
147
|
+
state: "over_quota",
|
|
148
|
+
color: "warning",
|
|
149
|
+
message: t("license_over_quota_banner", {
|
|
150
|
+
count: licensed,
|
|
151
|
+
licensed: String(licensed)
|
|
152
|
+
}),
|
|
153
|
+
linkLabel: t("license_update_license"),
|
|
154
|
+
href,
|
|
155
|
+
dismissible: false
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
// licensed, not_required, whitelisted, and servers too old to say
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isCount(value: unknown): value is number {
|
|
165
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function trialDaysLeft(status: AccessResponse): number | null {
|
|
169
|
+
if (isCount(status.daysLeft)) return Math.ceil(status.daysLeft);
|
|
170
|
+
const end = status.trialEndsAt ? new Date(status.trialEndsAt).getTime() : NaN;
|
|
171
|
+
if (Number.isNaN(end)) return null;
|
|
172
|
+
return Math.max(0, Math.ceil((end - Date.now()) / DAY_MS));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatDate(iso: string | undefined, language: string | undefined): string | null {
|
|
176
|
+
if (!iso) return null;
|
|
177
|
+
const date = new Date(iso);
|
|
178
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
179
|
+
const options: Intl.DateTimeFormatOptions = {
|
|
180
|
+
year: "numeric",
|
|
181
|
+
month: "long",
|
|
182
|
+
day: "numeric"
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
return new Intl.DateTimeFormat(language, options).format(date);
|
|
186
|
+
} catch {
|
|
187
|
+
return date.toLocaleDateString(undefined, options);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readDismissed(): string[] {
|
|
192
|
+
try {
|
|
193
|
+
const value = sessionStorage.getItem(DISMISSED_STORAGE_KEY);
|
|
194
|
+
const parsed = value ? JSON.parse(value) : [];
|
|
195
|
+
return Array.isArray(parsed) ? parsed.filter((s): s is string => typeof s === "string") : [];
|
|
196
|
+
} catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function writeDismissed(states: string[]) {
|
|
202
|
+
try {
|
|
203
|
+
sessionStorage.setItem(DISMISSED_STORAGE_KEY, JSON.stringify(states));
|
|
204
|
+
} catch {
|
|
205
|
+
// Private mode or storage disabled: dismissed for this page view only.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -44,6 +44,15 @@ export interface ReferenceSelectionInnerProps<M extends Record<string, any>> {
|
|
|
44
44
|
* dynamic. If not set, the dialog won't open.
|
|
45
45
|
*/
|
|
46
46
|
path: string;
|
|
47
|
+
/**
|
|
48
|
+
* `path` split at its real segment boundaries, when the caller knows them.
|
|
49
|
+
*
|
|
50
|
+
* Left undefined by default and deliberately NOT derived from `path`: a reference
|
|
51
|
+
* path comes from a property's configuration, so there is no entity chain to recover
|
|
52
|
+
* the boundaries from. Splitting it would silently produce the wrong segments for a
|
|
53
|
+
* subcollection under a slash-bearing parent id, which is worse than not answering.
|
|
54
|
+
*/
|
|
55
|
+
pathSegments?: string[];
|
|
47
56
|
|
|
48
57
|
/**
|
|
49
58
|
* If you are opening the dialog for the first time, you can select some
|
|
@@ -96,6 +105,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
96
105
|
multiselect,
|
|
97
106
|
collection,
|
|
98
107
|
path: pathInput,
|
|
108
|
+
pathSegments,
|
|
99
109
|
selectedEntityIds: selectedEntityIdsProp,
|
|
100
110
|
description,
|
|
101
111
|
forceFilter,
|
|
@@ -110,7 +120,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
110
120
|
const analyticsController = useAnalyticsController();
|
|
111
121
|
const customizationController = useCustomizationController();
|
|
112
122
|
|
|
113
|
-
const fullPath = navigation.resolveIdsFrom(pathInput);
|
|
123
|
+
const fullPath = navigation.resolveIdsFrom(pathInput, pathSegments);
|
|
114
124
|
|
|
115
125
|
const dataSource = useDataSource(collection);
|
|
116
126
|
|
|
@@ -153,6 +163,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
153
163
|
selectedEntityIds.map((entityId) =>
|
|
154
164
|
dataSource.fetchEntity({
|
|
155
165
|
path: fullPath,
|
|
166
|
+
pathSegments,
|
|
156
167
|
entityId,
|
|
157
168
|
collection
|
|
158
169
|
})))
|
|
@@ -204,6 +215,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
204
215
|
});
|
|
205
216
|
sideEntityController.open({
|
|
206
217
|
path: fullPath,
|
|
218
|
+
pathSegments,
|
|
207
219
|
collection,
|
|
208
220
|
updateUrl: true,
|
|
209
221
|
onUpdate: ({ entity }) => {
|
|
@@ -277,6 +289,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
277
289
|
textSearchEnabled
|
|
278
290
|
} =
|
|
279
291
|
useTableSearchHelper({
|
|
292
|
+
pathSegments,
|
|
280
293
|
collection,
|
|
281
294
|
fullPath,
|
|
282
295
|
});
|
|
@@ -315,6 +328,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
315
328
|
actions={<ReferenceDialogActions
|
|
316
329
|
collection={collection}
|
|
317
330
|
path={fullPath}
|
|
331
|
+
pathSegments={pathSegments}
|
|
318
332
|
onNewClick={onNewClick}
|
|
319
333
|
onClear={onClear}/>
|
|
320
334
|
}
|
|
@@ -341,11 +355,14 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
341
355
|
function ReferenceDialogActions({
|
|
342
356
|
collection,
|
|
343
357
|
path,
|
|
358
|
+
pathSegments,
|
|
344
359
|
onClear,
|
|
345
360
|
onNewClick
|
|
346
361
|
}: {
|
|
347
362
|
collection: EntityCollection<any>,
|
|
348
363
|
path: string,
|
|
364
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
365
|
+
pathSegments?: string[],
|
|
349
366
|
onClear: () => void,
|
|
350
367
|
onNewClick: () => void
|
|
351
368
|
}) {
|
|
@@ -360,7 +377,7 @@ function ReferenceDialogActions({
|
|
|
360
377
|
onNewClick();
|
|
361
378
|
}
|
|
362
379
|
: undefined;
|
|
363
|
-
const addButton = canCreateEntity(collection, authController, path, null) &&
|
|
380
|
+
const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
|
|
364
381
|
onClick && (largeLayout
|
|
365
382
|
? <Button
|
|
366
383
|
onClick={onClick}
|
|
@@ -73,7 +73,7 @@ export function ReferenceWidget<M extends Record<string, any>>({
|
|
|
73
73
|
if (disabled)
|
|
74
74
|
return;
|
|
75
75
|
if (onMultipleReferenceSelected) {
|
|
76
|
-
const references = entities ? entities.map(e => getReferenceFrom(e)) : null;
|
|
76
|
+
const references = entities ? entities.filter(Boolean).map(e => getReferenceFrom(e)) : null;
|
|
77
77
|
onMultipleReferenceSelected({
|
|
78
78
|
references,
|
|
79
79
|
entities
|
|
@@ -113,11 +113,11 @@ export function ReferenceFilterField({
|
|
|
113
113
|
}, [path]);
|
|
114
114
|
|
|
115
115
|
const onSingleEntitySelected = (entity: Entity<any>) => {
|
|
116
|
-
updateFilter(operation, getReferenceFrom(entity));
|
|
116
|
+
if (entity) updateFilter(operation, getReferenceFrom(entity));
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
const onMultipleEntitiesSelected = (entities: Entity<any>[]) => {
|
|
120
|
-
updateFilter(operation, entities.map(e => getReferenceFrom(e)));
|
|
120
|
+
updateFilter(operation, entities.filter(Boolean).map(e => getReferenceFrom(e)));
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
const multiple = multipleSelectOperations.includes(operation);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TextField
|
|
12
12
|
} from "@firecms/ui";
|
|
13
13
|
import { EnumValueConfig } from "../../../types";
|
|
14
|
+
import { filterValueToText, parseFilterTextInput } from "./filter_text_input";
|
|
14
15
|
|
|
15
16
|
interface StringNumberFilterFieldProps {
|
|
16
17
|
name: string,
|
|
@@ -60,8 +61,15 @@ export function StringNumberFilterField({
|
|
|
60
61
|
const [fieldOperation, fieldValue] = value || [possibleOperations[0], undefined];
|
|
61
62
|
const [operation, setOperation] = useState<VirtualTableWhereFilterOp | "is-null">(fieldOperation === "==" && fieldValue === null ? "is-null" : fieldOperation);
|
|
62
63
|
const [internalValue, setInternalValue] = useState<string | number | string[] | number[] | null | undefined>(fieldValue);
|
|
64
|
+
// Raw text of the text input, kept apart from `internalValue` so that intermediate
|
|
65
|
+
// inputs like "-", "0." or ".0" are not destroyed while the user is typing.
|
|
66
|
+
const [inputText, setInputText] = useState<string>(filterValueToText(fieldValue));
|
|
67
|
+
// Value the current `inputText` was built from, used to tell updates coming from
|
|
68
|
+
// outside apart from the ones triggered by the user typing.
|
|
69
|
+
const syncedValueRef = React.useRef(fieldValue);
|
|
63
70
|
|
|
64
71
|
React.useEffect(() => {
|
|
72
|
+
const newValue = value ? value[1] : undefined;
|
|
65
73
|
if (value) {
|
|
66
74
|
const [op, val] = value;
|
|
67
75
|
setOperation(op === "==" && val === null ? "is-null" : op);
|
|
@@ -70,15 +78,25 @@ export function StringNumberFilterField({
|
|
|
70
78
|
setOperation(possibleOperations[0]);
|
|
71
79
|
setInternalValue(undefined);
|
|
72
80
|
}
|
|
81
|
+
// only reset the text when the value was changed somewhere else, so we don't
|
|
82
|
+
// interfere with what the user is currently typing
|
|
83
|
+
if (syncedValueRef.current !== newValue) {
|
|
84
|
+
syncedValueRef.current = newValue;
|
|
85
|
+
setInputText(filterValueToText(newValue));
|
|
86
|
+
}
|
|
73
87
|
}, [value, possibleOperations[0]]);
|
|
74
88
|
|
|
75
89
|
const isNullOperation = operation === "is-null";
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
// `keepInputText` is used when the update originates from the text input, where the
|
|
92
|
+
// text typed by the user is authoritative and must not be overwritten with `val`
|
|
93
|
+
function updateFilter(op: VirtualTableWhereFilterOp | "is-null", val: string | number | string[] | number[] | null | undefined, keepInputText = false) {
|
|
78
94
|
// Handle "is null" operation
|
|
79
95
|
if (op === "is-null") {
|
|
80
96
|
setOperation(op);
|
|
81
97
|
setInternalValue(null);
|
|
98
|
+
syncedValueRef.current = null;
|
|
99
|
+
setInputText("");
|
|
82
100
|
setValue(["==", null]);
|
|
83
101
|
return;
|
|
84
102
|
}
|
|
@@ -96,6 +114,9 @@ export function StringNumberFilterField({
|
|
|
96
114
|
|
|
97
115
|
setOperation(op);
|
|
98
116
|
setInternalValue(newValue);
|
|
117
|
+
syncedValueRef.current = newValue;
|
|
118
|
+
if (!keepInputText)
|
|
119
|
+
setInputText(filterValueToText(newValue));
|
|
99
120
|
|
|
100
121
|
const hasNewValue = newValue !== null && Array.isArray(newValue)
|
|
101
122
|
? newValue.length > 0
|
|
@@ -111,6 +132,14 @@ export function StringNumberFilterField({
|
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
134
|
|
|
135
|
+
function updateFilterFromText(text: string) {
|
|
136
|
+
setInputText(text);
|
|
137
|
+
const { updateValue, value: parsedValue } = parseFilterTextInput(text, dataType);
|
|
138
|
+
// intermediate inputs like "-" or "0." keep the filter as it is
|
|
139
|
+
if (updateValue)
|
|
140
|
+
updateFilter(operation, parsedValue, true);
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
const multiple = multipleSelectOperations.includes(operation);
|
|
115
144
|
|
|
116
145
|
return (
|
|
@@ -138,16 +167,13 @@ export function StringNumberFilterField({
|
|
|
138
167
|
{!enumValues && <TextField
|
|
139
168
|
size={"medium"}
|
|
140
169
|
type={dataType === "number" ? "number" : undefined}
|
|
141
|
-
value={
|
|
170
|
+
value={inputText}
|
|
142
171
|
disabled={isNullOperation}
|
|
143
172
|
placeholder={isNullOperation ? "null" : undefined}
|
|
144
173
|
onChange={(evt) => {
|
|
145
|
-
|
|
146
|
-
? parseFloat(evt.target.value)
|
|
147
|
-
: evt.target.value;
|
|
148
|
-
updateFilter(operation, val);
|
|
174
|
+
updateFilterFromText(evt.target.value);
|
|
149
175
|
}}
|
|
150
|
-
endAdornment={
|
|
176
|
+
endAdornment={inputText !== "" && <IconButton
|
|
151
177
|
onClick={(e) => updateFilter(operation, undefined)}>
|
|
152
178
|
<CloseIcon />
|
|
153
179
|
</IconButton>}
|