@firecms/core 3.5.0 → 3.5.2-canary.09e1281
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/dist/components/EntityCollectionView/CollectionCountBadge.d.ts +8 -0
- package/dist/components/EntityCollectionView/CollectionPathTitle.d.ts +24 -0
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +5 -0
- package/dist/components/EntityCollectionView/EntityIdHeaderWidget.d.ts +12 -0
- package/dist/components/EntityCollectionView/utils.d.ts +3 -3
- package/dist/components/HomePage/HomePageDnD.d.ts +4 -2
- package/dist/{de-CBOjCvsy.js → de-CUF9f7S-.js} +2 -1
- package/dist/{de-CBOjCvsy.js.map → de-CUF9f7S-.js.map} +1 -1
- package/dist/{editor-BSo_aS1I.js → editor-WZytSO_1.js} +172 -137
- package/dist/editor-WZytSO_1.js.map +1 -0
- package/dist/{fr-DRfttcG0.js → fr-DmkMX6hp.js} +2 -1
- package/dist/{fr-DRfttcG0.js.map → fr-DmkMX6hp.js.map} +1 -1
- package/dist/{hi-DwieOZNf.js → hi-BKuaMr7w.js} +2 -1
- package/dist/{hi-DwieOZNf.js.map → hi-BKuaMr7w.js.map} +1 -1
- package/dist/index.es.js +787 -638
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +946 -756
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useLeavePageConfirmation.d.ts +11 -0
- package/dist/{it-l2KXtx9n.js → it-BwQflFGz.js} +2 -1
- package/dist/{it-l2KXtx9n.js.map → it-BwQflFGz.js.map} +1 -1
- package/dist/{pl-DPZrdKhi.js → pl-BPsAvZvc.js} +2 -1
- package/dist/{pl-DPZrdKhi.js.map → pl-BPsAvZvc.js.map} +1 -1
- package/dist/{pt-BoHpabw1.js → pt-D5q7OQp1.js} +2 -1
- package/dist/{pt-BoHpabw1.js.map → pt-D5q7OQp1.js.map} +1 -1
- package/dist/types/side_dialogs_controller.d.ts +4 -2
- package/dist/types/translations.d.ts +1 -0
- package/package.json +3 -3
- package/src/components/EntityCollectionView/CollectionCountBadge.tsx +17 -0
- package/src/components/EntityCollectionView/CollectionPathTitle.tsx +68 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +12 -111
- package/src/components/EntityCollectionView/EntityIdHeaderWidget.tsx +124 -0
- package/src/components/EntityCollectionView/utils.ts +7 -7
- package/src/components/HomePage/DefaultHomePage.tsx +2 -0
- package/src/components/HomePage/HomePageDnD.tsx +12 -4
- package/src/components/common/default_entity_actions.tsx +2 -1
- package/src/core/DefaultAppBar.tsx +12 -9
- package/src/core/EntityEditView.tsx +26 -1
- package/src/core/SideDialogs.tsx +6 -13
- package/src/editor/selectors/link-selector.tsx +37 -12
- package/src/hooks/useReferenceDialog.tsx +2 -2
- package/src/internal/useBuildSideDialogsController.tsx +44 -2
- package/src/internal/useLeavePageConfirmation.ts +25 -0
- package/src/locales/de.ts +1 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/hi.ts +1 -0
- package/src/locales/it.ts +1 -0
- package/src/locales/pl.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/types/side_dialogs_controller.tsx +4 -2
- package/src/types/translations.ts +1 -0
- package/dist/editor-BSo_aS1I.js.map +0 -1
- package/dist/internal/useUnsavedChangesDialog.d.ts +0 -12
- package/src/internal/useUnsavedChangesDialog.tsx +0 -142
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
Entity,
|
|
9
9
|
EntityAction,
|
|
10
10
|
EntityCollection,
|
|
11
|
-
EntityReference,
|
|
12
11
|
EntityTableController,
|
|
13
12
|
FilterValues,
|
|
14
13
|
PartialEntityCollection,
|
|
@@ -59,14 +58,9 @@ import {
|
|
|
59
58
|
AddIcon,
|
|
60
59
|
Button,
|
|
61
60
|
cls,
|
|
62
|
-
focusedDisabled,
|
|
63
|
-
IconButton,
|
|
64
61
|
KeyboardTabIcon,
|
|
65
62
|
Markdown,
|
|
66
|
-
Popover,
|
|
67
|
-
SearchIcon,
|
|
68
63
|
Skeleton,
|
|
69
|
-
Tooltip,
|
|
70
64
|
Typography
|
|
71
65
|
} from "@firecms/ui";
|
|
72
66
|
import { setIn } from "@firecms/formex";
|
|
@@ -88,7 +82,8 @@ import { DeleteEntityDialog } from "../DeleteEntityDialog";
|
|
|
88
82
|
import { useAnalyticsController } from "../../hooks/useAnalyticsController";
|
|
89
83
|
import { useSelectionController } from "./useSelectionController";
|
|
90
84
|
import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
|
|
91
|
-
import { addRecentId
|
|
85
|
+
import { addRecentId } from "./utils";
|
|
86
|
+
import { EntityIdHeaderWidget } from "./EntityIdHeaderWidget";
|
|
92
87
|
import { useScrollRestoration } from "../common/useScrollRestoration";
|
|
93
88
|
import { entityCacheKey } from "../../util/entity_cache";
|
|
94
89
|
|
|
@@ -131,6 +126,12 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
|
|
|
131
126
|
*/
|
|
132
127
|
updateUrl?: boolean;
|
|
133
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Called with the number of entities in the collection, with the current filters
|
|
131
|
+
* applied, each time it is counted: the count the breadcrumbs show.
|
|
132
|
+
*/
|
|
133
|
+
onCountChange?: (count: number) => void;
|
|
134
|
+
|
|
134
135
|
} & EntityCollection<M>;
|
|
135
136
|
|
|
136
137
|
/**
|
|
@@ -166,6 +167,7 @@ export const EntityCollectionView = React.memo(
|
|
|
166
167
|
isSubCollection,
|
|
167
168
|
className,
|
|
168
169
|
updateUrl,
|
|
170
|
+
onCountChange,
|
|
169
171
|
...collectionProp
|
|
170
172
|
}: EntityCollectionViewProps<M>
|
|
171
173
|
) {
|
|
@@ -350,7 +352,7 @@ export const EntityCollectionView = React.memo(
|
|
|
350
352
|
});
|
|
351
353
|
|
|
352
354
|
if (collection) {
|
|
353
|
-
addRecentId(
|
|
355
|
+
addRecentId(fullIdPath ?? fullPath, clickedEntity.id);
|
|
354
356
|
}
|
|
355
357
|
|
|
356
358
|
// In a collection group the row's own path is the real one, so the row's segments
|
|
@@ -795,8 +797,9 @@ export const EntityCollectionView = React.memo(
|
|
|
795
797
|
useEffect(() => {
|
|
796
798
|
if (docsCount !== undefined) {
|
|
797
799
|
breadcrumbs.updateCount(fullPath, docsCount);
|
|
800
|
+
onCountChange?.(docsCount);
|
|
798
801
|
}
|
|
799
|
-
}, [docsCount, fullPath, breadcrumbs.updateCount]);
|
|
802
|
+
}, [docsCount, fullPath, breadcrumbs.updateCount, onCountChange]);
|
|
800
803
|
|
|
801
804
|
// EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
|
|
802
805
|
const countFetcher = <EntitiesCount
|
|
@@ -1198,105 +1201,3 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
|
|
|
1198
1201
|
}
|
|
1199
1202
|
return { properties: { [key]: { columnWidth: width } } } as PartialEntityCollection;
|
|
1200
1203
|
}
|
|
1201
|
-
|
|
1202
|
-
function EntityIdHeaderWidget({
|
|
1203
|
-
collection,
|
|
1204
|
-
path,
|
|
1205
|
-
pathSegments,
|
|
1206
|
-
fullIdPath
|
|
1207
|
-
}: {
|
|
1208
|
-
collection: EntityCollection,
|
|
1209
|
-
path: string,
|
|
1210
|
-
/** `path` split at its real segment boundaries, when the caller knows them. */
|
|
1211
|
-
pathSegments?: string[],
|
|
1212
|
-
fullIdPath: string
|
|
1213
|
-
}) {
|
|
1214
|
-
|
|
1215
|
-
const navigation = useNavigationController();
|
|
1216
|
-
const [openPopup, setOpenPopup] = React.useState(false);
|
|
1217
|
-
const [searchString, setSearchString] = React.useState("");
|
|
1218
|
-
const [recentIds, setRecentIds] = React.useState<string[]>(getRecentIds(collection.id));
|
|
1219
|
-
const sideEntityController = useSideEntityController();
|
|
1220
|
-
const { t } = useTranslation();
|
|
1221
|
-
|
|
1222
|
-
const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
|
|
1223
|
-
|
|
1224
|
-
return (
|
|
1225
|
-
<Tooltip title={!openPopup ? t("find_by_id") : undefined} asChild={false}>
|
|
1226
|
-
<Popover
|
|
1227
|
-
open={openPopup}
|
|
1228
|
-
onOpenChange={setOpenPopup}
|
|
1229
|
-
sideOffset={0}
|
|
1230
|
-
align={"start"}
|
|
1231
|
-
alignOffset={-117}
|
|
1232
|
-
trigger={
|
|
1233
|
-
<IconButton size={"small"}>
|
|
1234
|
-
<SearchIcon size={"small"} />
|
|
1235
|
-
</IconButton>
|
|
1236
|
-
}>
|
|
1237
|
-
<div
|
|
1238
|
-
className={cls("my-2 rounded-lg bg-surface-50 dark:bg-surface-950 text-surface-900 dark:text-white")}>
|
|
1239
|
-
<form noValidate={true}
|
|
1240
|
-
onSubmit={(e) => {
|
|
1241
|
-
e.preventDefault();
|
|
1242
|
-
if (!searchString) return;
|
|
1243
|
-
setOpenPopup(false);
|
|
1244
|
-
const entityId = searchString.trim();
|
|
1245
|
-
setRecentIds(addRecentId(collection.id, entityId));
|
|
1246
|
-
navigateToEntity({
|
|
1247
|
-
openEntityMode,
|
|
1248
|
-
collection,
|
|
1249
|
-
entityId,
|
|
1250
|
-
path,
|
|
1251
|
-
pathSegments,
|
|
1252
|
-
fullIdPath,
|
|
1253
|
-
sideEntityController,
|
|
1254
|
-
navigation
|
|
1255
|
-
})
|
|
1256
|
-
}}
|
|
1257
|
-
className={"w-96 max-w-full"}>
|
|
1258
|
-
|
|
1259
|
-
<div className="flex p-2 w-full gap-2">
|
|
1260
|
-
<input
|
|
1261
|
-
autoFocus={openPopup}
|
|
1262
|
-
placeholder={t("find_entity_by_id")}
|
|
1263
|
-
// size={"small"}
|
|
1264
|
-
onChange={(e) => {
|
|
1265
|
-
setSearchString(e.target.value);
|
|
1266
|
-
}}
|
|
1267
|
-
value={searchString}
|
|
1268
|
-
className={"rounded-lg bg-white dark:bg-surface-800 flex-grow bg-transparent outline-none p-2 " + focusedDisabled} />
|
|
1269
|
-
<Button variant={"text"}
|
|
1270
|
-
disabled={!(searchString.trim())}
|
|
1271
|
-
type={"submit"}
|
|
1272
|
-
><KeyboardTabIcon /></Button>
|
|
1273
|
-
</div>
|
|
1274
|
-
</form>
|
|
1275
|
-
{recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
|
|
1276
|
-
{recentIds.map(id => (
|
|
1277
|
-
<ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
|
|
1278
|
-
key={id}
|
|
1279
|
-
hover={true}
|
|
1280
|
-
onClick={() => {
|
|
1281
|
-
setOpenPopup(false);
|
|
1282
|
-
navigateToEntity({
|
|
1283
|
-
openEntityMode,
|
|
1284
|
-
collection,
|
|
1285
|
-
entityId: id,
|
|
1286
|
-
path,
|
|
1287
|
-
pathSegments,
|
|
1288
|
-
fullIdPath,
|
|
1289
|
-
sideEntityController,
|
|
1290
|
-
navigation
|
|
1291
|
-
})
|
|
1292
|
-
}}
|
|
1293
|
-
includeEntityLink={false}
|
|
1294
|
-
size={"small"} />
|
|
1295
|
-
))}
|
|
1296
|
-
</div>}
|
|
1297
|
-
</div>
|
|
1298
|
-
</Popover>
|
|
1299
|
-
|
|
1300
|
-
</Tooltip>
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button, cls, focusedDisabled, IconButton, KeyboardTabIcon, Popover, SearchIcon, Tooltip } from "@firecms/ui";
|
|
3
|
+
import { EntityCollection, EntityReference } from "../../types";
|
|
4
|
+
import { navigateToEntity } from "../../util/navigation_utils";
|
|
5
|
+
import { ReferencePreview } from "../../preview";
|
|
6
|
+
import { useNavigationController } from "../../hooks/useNavigationController";
|
|
7
|
+
import { useSideEntityController } from "../../hooks/useSideEntityController";
|
|
8
|
+
import { useTranslation } from "../../hooks/useTranslation";
|
|
9
|
+
import { addRecentId, getRecentIds } from "./utils";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_ENTITY_OPEN_MODE: "side_panel" | "full_screen" = "side_panel";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* "Find by id" in the header of a collection's id column, listing the ids opened in it lately.
|
|
15
|
+
*/
|
|
16
|
+
export function EntityIdHeaderWidget({
|
|
17
|
+
collection,
|
|
18
|
+
path,
|
|
19
|
+
pathSegments,
|
|
20
|
+
fullIdPath
|
|
21
|
+
}: {
|
|
22
|
+
collection: EntityCollection,
|
|
23
|
+
path: string,
|
|
24
|
+
/** `path` split at its real segment boundaries, when the caller knows them. */
|
|
25
|
+
pathSegments?: string[],
|
|
26
|
+
fullIdPath: string
|
|
27
|
+
}) {
|
|
28
|
+
|
|
29
|
+
const navigation = useNavigationController();
|
|
30
|
+
const [openPopup, setOpenPopup] = React.useState(false);
|
|
31
|
+
const [searchString, setSearchString] = React.useState("");
|
|
32
|
+
// Kept per full path rather than per collection: a subcollection has the same id under
|
|
33
|
+
// every parent, and an id found under one parent does not exist under the others.
|
|
34
|
+
const [recentIds, setRecentIds] = React.useState<string[]>(() => getRecentIds(fullIdPath));
|
|
35
|
+
const sideEntityController = useSideEntityController();
|
|
36
|
+
const { t } = useTranslation();
|
|
37
|
+
|
|
38
|
+
const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Tooltip title={!openPopup ? t("find_by_id") : undefined} asChild={false}>
|
|
42
|
+
<Popover
|
|
43
|
+
open={openPopup}
|
|
44
|
+
onOpenChange={(open) => {
|
|
45
|
+
// rows opened since this header mounted are recent too
|
|
46
|
+
if (open) setRecentIds(getRecentIds(fullIdPath));
|
|
47
|
+
setOpenPopup(open);
|
|
48
|
+
}}
|
|
49
|
+
sideOffset={0}
|
|
50
|
+
align={"start"}
|
|
51
|
+
alignOffset={-117}
|
|
52
|
+
trigger={
|
|
53
|
+
<IconButton size={"small"}>
|
|
54
|
+
<SearchIcon size={"small"} />
|
|
55
|
+
</IconButton>
|
|
56
|
+
}>
|
|
57
|
+
<div
|
|
58
|
+
className={cls("my-2 rounded-lg bg-surface-50 dark:bg-surface-950 text-surface-900 dark:text-white")}>
|
|
59
|
+
<form noValidate={true}
|
|
60
|
+
onSubmit={(e) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
const entityId = searchString.trim();
|
|
63
|
+
// Enter submits even while the button is disabled, and an empty
|
|
64
|
+
// id would open a new entity instead
|
|
65
|
+
if (!entityId) return;
|
|
66
|
+
setOpenPopup(false);
|
|
67
|
+
setRecentIds(addRecentId(fullIdPath, entityId));
|
|
68
|
+
navigateToEntity({
|
|
69
|
+
openEntityMode,
|
|
70
|
+
collection,
|
|
71
|
+
entityId,
|
|
72
|
+
path,
|
|
73
|
+
pathSegments,
|
|
74
|
+
fullIdPath,
|
|
75
|
+
sideEntityController,
|
|
76
|
+
navigation
|
|
77
|
+
})
|
|
78
|
+
}}
|
|
79
|
+
className={"w-96 max-w-full"}>
|
|
80
|
+
|
|
81
|
+
<div className="flex p-2 w-full gap-2">
|
|
82
|
+
<input
|
|
83
|
+
autoFocus={openPopup}
|
|
84
|
+
placeholder={t("find_entity_by_id")}
|
|
85
|
+
// size={"small"}
|
|
86
|
+
onChange={(e) => {
|
|
87
|
+
setSearchString(e.target.value);
|
|
88
|
+
}}
|
|
89
|
+
value={searchString}
|
|
90
|
+
className={"rounded-lg bg-white dark:bg-surface-800 flex-grow bg-transparent outline-none p-2 " + focusedDisabled} />
|
|
91
|
+
<Button variant={"text"}
|
|
92
|
+
disabled={!(searchString.trim())}
|
|
93
|
+
type={"submit"}
|
|
94
|
+
><KeyboardTabIcon /></Button>
|
|
95
|
+
</div>
|
|
96
|
+
</form>
|
|
97
|
+
{recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
|
|
98
|
+
{recentIds.map(id => (
|
|
99
|
+
<ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
|
|
100
|
+
key={id}
|
|
101
|
+
hover={true}
|
|
102
|
+
onClick={() => {
|
|
103
|
+
setOpenPopup(false);
|
|
104
|
+
navigateToEntity({
|
|
105
|
+
openEntityMode,
|
|
106
|
+
collection,
|
|
107
|
+
entityId: id,
|
|
108
|
+
path,
|
|
109
|
+
pathSegments,
|
|
110
|
+
fullIdPath,
|
|
111
|
+
sideEntityController,
|
|
112
|
+
navigation
|
|
113
|
+
})
|
|
114
|
+
}}
|
|
115
|
+
includeEntityLink={false}
|
|
116
|
+
size={"small"} />
|
|
117
|
+
))}
|
|
118
|
+
</div>}
|
|
119
|
+
</div>
|
|
120
|
+
</Popover>
|
|
121
|
+
|
|
122
|
+
</Tooltip>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { getLocalStorageJSON, setLocalStorageItem } from "../../util/local_storage";
|
|
2
2
|
|
|
3
|
-
export function addRecentId(
|
|
4
|
-
const recentIds = getRecentIds(
|
|
3
|
+
export function addRecentId(key: string, id: string) {
|
|
4
|
+
const recentIds = getRecentIds(key);
|
|
5
5
|
const newRecentIds = [id, ...recentIds.filter(i => i !== id)];
|
|
6
6
|
if (newRecentIds.length > 5) {
|
|
7
7
|
newRecentIds.pop();
|
|
8
8
|
}
|
|
9
|
-
saveSearchedIdsLocally(
|
|
9
|
+
saveSearchedIdsLocally(key, newRecentIds);
|
|
10
10
|
return newRecentIds;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function saveSearchedIdsLocally(
|
|
14
|
-
setLocalStorageItem("recent_id_searches::" +
|
|
13
|
+
export function saveSearchedIdsLocally(key: string, ids: string[]) {
|
|
14
|
+
setLocalStorageItem("recent_id_searches::" + key, JSON.stringify(ids));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function getRecentIds(
|
|
18
|
-
return getLocalStorageJSON("recent_id_searches::" +
|
|
17
|
+
export function getRecentIds(key: string): string[] {
|
|
18
|
+
return getLocalStorageJSON("recent_id_searches::" + key, []);
|
|
19
19
|
}
|
|
@@ -465,6 +465,7 @@ export function DefaultHomePage({
|
|
|
465
465
|
isPotentialCardDropTarget={
|
|
466
466
|
isDraggingCardOnly
|
|
467
467
|
}
|
|
468
|
+
disabled={dndDisabled}
|
|
468
469
|
>
|
|
469
470
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 ">
|
|
470
471
|
{entriesInGroup.map(
|
|
@@ -472,6 +473,7 @@ export function DefaultHomePage({
|
|
|
472
473
|
<SortableNavigationCard
|
|
473
474
|
key={entry.url}
|
|
474
475
|
entry={entry}
|
|
476
|
+
disabled={dndDisabled}
|
|
475
477
|
onClick={() => {
|
|
476
478
|
let event: CMSAnalyticsEvent =
|
|
477
479
|
"unmapped_event";
|
|
@@ -67,10 +67,12 @@ const cloneItemsForDnd = (items: { name: string; entries: NavigationEntry[] }[])
|
|
|
67
67
|
/* ─────────────────────────────────────────────────────────── */
|
|
68
68
|
export function SortableNavigationCard({
|
|
69
69
|
entry,
|
|
70
|
-
onClick
|
|
70
|
+
onClick,
|
|
71
|
+
disabled
|
|
71
72
|
}: {
|
|
72
73
|
entry: NavigationEntry;
|
|
73
74
|
onClick?: () => void;
|
|
75
|
+
disabled?: boolean;
|
|
74
76
|
}) {
|
|
75
77
|
const {
|
|
76
78
|
setNodeRef,
|
|
@@ -82,7 +84,8 @@ export function SortableNavigationCard({
|
|
|
82
84
|
} =
|
|
83
85
|
useSortable({
|
|
84
86
|
id: entry.url,
|
|
85
|
-
animateLayoutChanges
|
|
87
|
+
animateLayoutChanges,
|
|
88
|
+
disabled
|
|
86
89
|
});
|
|
87
90
|
|
|
88
91
|
const style = {
|
|
@@ -102,14 +105,19 @@ export function NavigationGroupDroppable({
|
|
|
102
105
|
id,
|
|
103
106
|
itemIds,
|
|
104
107
|
children,
|
|
105
|
-
isPotentialCardDropTarget = false
|
|
108
|
+
isPotentialCardDropTarget = false,
|
|
109
|
+
disabled
|
|
106
110
|
}: {
|
|
107
111
|
id: UniqueIdentifier;
|
|
108
112
|
itemIds: UniqueIdentifier[];
|
|
109
113
|
children: React.ReactNode;
|
|
110
114
|
isPotentialCardDropTarget?: boolean;
|
|
115
|
+
disabled?: boolean;
|
|
111
116
|
}) {
|
|
112
|
-
const { setNodeRef } = useDroppable({
|
|
117
|
+
const { setNodeRef } = useDroppable({
|
|
118
|
+
id,
|
|
119
|
+
disabled
|
|
120
|
+
});
|
|
113
121
|
|
|
114
122
|
return (
|
|
115
123
|
<div
|
|
@@ -38,7 +38,8 @@ export const editEntityAction: EntityAction = {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
if (collection) {
|
|
41
|
-
|
|
41
|
+
// the same key the collection view's "find by id" reads them with
|
|
42
|
+
addRecentId(fullIdPath ?? fullPath ?? collection.id, entity.id);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const path = collection?.collectionGroup ? entity.path : (fullPath ?? collection?.path ?? entity.path);
|
|
@@ -21,6 +21,8 @@ import { User } from "../types";
|
|
|
21
21
|
import { useApp } from "../app/useApp";
|
|
22
22
|
import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
|
|
23
23
|
import { DrawerLogo } from "./DefaultDrawer";
|
|
24
|
+
import { CollectionCountBadge } from "../components/EntityCollectionView/CollectionCountBadge";
|
|
25
|
+
import { PathTitle } from "../components/EntityCollectionView/CollectionPathTitle";
|
|
24
26
|
|
|
25
27
|
export type DefaultAppBarProps<ADDITIONAL_PROPS = object> = {
|
|
26
28
|
|
|
@@ -81,6 +83,7 @@ export const DefaultAppBar = function DefaultAppBar({
|
|
|
81
83
|
const navigation = useNavigationController();
|
|
82
84
|
|
|
83
85
|
const breadcrumbs = useBreadcrumbsController();
|
|
86
|
+
const lastBreadcrumb = breadcrumbs.breadcrumbs?.[breadcrumbs.breadcrumbs.length - 1];
|
|
84
87
|
|
|
85
88
|
const authController = useAuthController();
|
|
86
89
|
const {
|
|
@@ -177,15 +180,7 @@ export const DefaultAppBar = function DefaultAppBar({
|
|
|
177
180
|
{breadcrumb.title}
|
|
178
181
|
</Typography>
|
|
179
182
|
{/* Show count badge for collection breadcrumbs: undefined = not applicable, null = loading, number = count */}
|
|
180
|
-
{breadcrumb.count
|
|
181
|
-
breadcrumb.count !== null ? (
|
|
182
|
-
<span className="text-xs text-surface-accent-500 dark:text-surface-accent-400 bg-surface-100 dark:bg-surface-700 px-1 py-0 rounded">
|
|
183
|
-
{breadcrumb.count}
|
|
184
|
-
</span>
|
|
185
|
-
) : (
|
|
186
|
-
<Skeleton className="w-8 h-4 rounded-md" />
|
|
187
|
-
)
|
|
188
|
-
)}
|
|
183
|
+
<CollectionCountBadge count={breadcrumb.count}/>
|
|
189
184
|
</div>
|
|
190
185
|
</Link>
|
|
191
186
|
</React.Fragment>;
|
|
@@ -193,6 +188,14 @@ export const DefaultAppBar = function DefaultAppBar({
|
|
|
193
188
|
</div>
|
|
194
189
|
</div>}
|
|
195
190
|
|
|
191
|
+
{/* A row of breadcrumbs does not fit a small screen, but where you are and how
|
|
192
|
+
many entities are there still matter, so they are folded on two lines. */}
|
|
193
|
+
{lastBreadcrumb && <PathTitle
|
|
194
|
+
className={"lg:hidden min-w-0 mr-2"}
|
|
195
|
+
parents={breadcrumbs.breadcrumbs.slice(0, -1).map(breadcrumb => breadcrumb.title)}
|
|
196
|
+
name={lastBreadcrumb.title}
|
|
197
|
+
count={lastBreadcrumb.count}/>}
|
|
198
|
+
|
|
196
199
|
|
|
197
200
|
{startAdornment}
|
|
198
201
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
useAuthController,
|
|
26
26
|
useCustomizationController,
|
|
27
|
+
useDataSource,
|
|
27
28
|
useEntityFetch,
|
|
28
29
|
useFireCMSContext,
|
|
29
30
|
useLargeLayout
|
|
@@ -35,6 +36,7 @@ import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
|
|
|
35
36
|
import { EntityJsonPreview } from "../components/EntityJsonPreview";
|
|
36
37
|
import { createFormexStub } from "../util/createFormexStub";
|
|
37
38
|
import { useTranslation } from "../hooks/useTranslation";
|
|
39
|
+
import { CollectionPathTitle } from "../components/EntityCollectionView/CollectionPathTitle";
|
|
38
40
|
|
|
39
41
|
export const MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
|
|
40
42
|
export const JSON_TAB_VALUE = "__json";
|
|
@@ -238,6 +240,11 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
238
240
|
|
|
239
241
|
const subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
|
|
240
242
|
const subcollectionsCount = subcollections?.length ?? 0;
|
|
243
|
+
|
|
244
|
+
// The number of entities in each subcollection tab, as its collection view counts them.
|
|
245
|
+
const [subcollectionCounts, setSubcollectionCounts] = useState<Record<string, number>>({});
|
|
246
|
+
const selectedSubcollection = subcollections.find(subcollection => (subcollection.id ?? subcollection.path) === selectedTab);
|
|
247
|
+
const selectedSubcollectionDataSource = useDataSource(selectedSubcollection ?? collection);
|
|
241
248
|
const customViews = collection.entityViews ?? [];
|
|
242
249
|
const customViewsCount = customViews?.length ?? 0;
|
|
243
250
|
const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
|
|
@@ -386,7 +393,9 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
386
393
|
isSubCollection={true}
|
|
387
394
|
updateUrl={false}
|
|
388
395
|
{...subcollection}
|
|
389
|
-
openEntityMode={layout}
|
|
396
|
+
openEntityMode={layout}
|
|
397
|
+
onCountChange={(count) => setSubcollectionCounts(counts =>
|
|
398
|
+
counts[subcollectionId] === count ? counts : { ...counts, [subcollectionId]: count })} />
|
|
390
399
|
: <div className="flex items-center justify-center w-full h-full p-3">
|
|
391
400
|
<Typography variant={"label"}>
|
|
392
401
|
{t("youd_need_to_save_before_additional_collections")}
|
|
@@ -534,6 +543,20 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
534
543
|
|
|
535
544
|
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews;
|
|
536
545
|
|
|
546
|
+
// A side panel covers the app bar's breadcrumbs, which show where a collection is and
|
|
547
|
+
// how many entities it has, so the panel shows its subcollection's in its own top bar.
|
|
548
|
+
const subcollectionTitle = layout === "side_panel" && selectedSubcollection && usedEntity
|
|
549
|
+
? (() => {
|
|
550
|
+
const subcollectionId = selectedSubcollection.id ?? selectedSubcollection.path;
|
|
551
|
+
return <CollectionPathTitle
|
|
552
|
+
path={fullIdPath
|
|
553
|
+
? `${fullIdPath}/${encodeEntityId(usedEntity.id)}/${removeInitialAndTrailingSlashes(subcollectionId)}`
|
|
554
|
+
: `${path}/${usedEntity.id}/${removeInitialAndTrailingSlashes(selectedSubcollection.path)}`}
|
|
555
|
+
name={selectedSubcollection.name}
|
|
556
|
+
count={subcollectionCounts[subcollectionId] ?? (selectedSubcollectionDataSource.countEntities ? null : undefined)}/>;
|
|
557
|
+
})()
|
|
558
|
+
: null;
|
|
559
|
+
|
|
537
560
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
|
|
538
561
|
|
|
539
562
|
{shouldShowTopBar && <div
|
|
@@ -546,6 +569,8 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
546
569
|
status
|
|
547
570
|
})}
|
|
548
571
|
|
|
572
|
+
{subcollectionTitle}
|
|
573
|
+
|
|
549
574
|
<div className={"flex-grow"} />
|
|
550
575
|
|
|
551
576
|
{pluginActionsTop}
|
package/src/core/SideDialogs.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { useSideDialogsController } from "../hooks";
|
|
3
3
|
import { EntitySidePanelProps, SideDialogPanelProps } from "../types";
|
|
4
4
|
import { Sheet } from "@firecms/ui";
|
|
5
|
-
import {
|
|
5
|
+
import { useLeavePageConfirmation } from "../internal/useLeavePageConfirmation";
|
|
6
6
|
import { ErrorBoundary } from "../components";
|
|
7
7
|
import { UnsavedChangesDialog } from "../components/UnsavedChangesDialog";
|
|
8
8
|
import { EntitySidePanel } from "./EntitySidePanel";
|
|
@@ -123,14 +123,7 @@ function SideDialogView({
|
|
|
123
123
|
|
|
124
124
|
const sideDialogsController = useSideDialogsController();
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
navigationWasBlocked,
|
|
128
|
-
handleOk: handleNavigationOk,
|
|
129
|
-
handleCancel: handleNavigationCancel
|
|
130
|
-
} = useNavigationUnsavedChangesDialog(
|
|
131
|
-
blocked && !drawerCloseRequested,
|
|
132
|
-
() => setBlocked(false)
|
|
133
|
-
);
|
|
126
|
+
useLeavePageConfirmation(blocked && !drawerCloseRequested);
|
|
134
127
|
|
|
135
128
|
useEffect(() => {
|
|
136
129
|
if (panel)
|
|
@@ -140,7 +133,7 @@ function SideDialogView({
|
|
|
140
133
|
const handleDrawerCloseOk = () => {
|
|
141
134
|
setBlocked(false);
|
|
142
135
|
setDrawerCloseRequested(false);
|
|
143
|
-
sideDialogsController.close();
|
|
136
|
+
sideDialogsController.close(panel?.key);
|
|
144
137
|
panel?.onClose?.();
|
|
145
138
|
};
|
|
146
139
|
|
|
@@ -152,7 +145,7 @@ function SideDialogView({
|
|
|
152
145
|
if (blocked && !force) {
|
|
153
146
|
setDrawerCloseRequested(true);
|
|
154
147
|
} else {
|
|
155
|
-
sideDialogsController.close();
|
|
148
|
+
sideDialogsController.close(panel?.key);
|
|
156
149
|
panel?.onClose?.();
|
|
157
150
|
}
|
|
158
151
|
};
|
|
@@ -212,8 +205,8 @@ function SideDialogView({
|
|
|
212
205
|
|
|
213
206
|
<UnsavedChangesDialog
|
|
214
207
|
open={drawerCloseRequested}
|
|
215
|
-
handleOk={
|
|
216
|
-
handleCancel={
|
|
208
|
+
handleOk={handleDrawerCloseOk}
|
|
209
|
+
handleCancel={handleDrawerCloseCancel}
|
|
217
210
|
body={blockedNavigationMessage} />
|
|
218
211
|
|
|
219
212
|
</SideDialogContext.Provider>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef, } from "react";
|
|
2
|
-
import { Button, CheckIcon, cls, DeleteIcon, focusedDisabled, Popover } from "@firecms/ui";
|
|
1
|
+
import { useEffect, useRef, useState} from "react";
|
|
2
|
+
import { Button, CheckIcon, cls, DeleteIcon, focusedDisabled, Popover, Typography } from "@firecms/ui";
|
|
3
3
|
import { useTranslation } from "../../hooks/useTranslation";
|
|
4
4
|
import { useProseMirrorContext } from "../hooks/useProseMirrorContext";
|
|
5
5
|
import { getMarkAttributes, isMarkActive, setMark, unsetMark } from "../utils/prosemirror-utils";
|
|
@@ -39,25 +39,36 @@ export const LinkSelector = ({
|
|
|
39
39
|
const { state, view } = useProseMirrorContext();
|
|
40
40
|
const { t } = useTranslation();
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
const isInputInvalid = !!error;
|
|
44
|
+
|
|
43
45
|
useEffect(() => {
|
|
44
|
-
|
|
45
|
-
inputRef.current.focus();
|
|
46
|
-
}
|
|
46
|
+
setError(null);
|
|
47
47
|
}, [open]);
|
|
48
48
|
|
|
49
49
|
if (!state || !view) return null;
|
|
50
50
|
|
|
51
|
-
const
|
|
51
|
+
const handleChange = (e: React.ChangeEvent<HTMLFormElement>) => {
|
|
52
|
+
if (isInputInvalid) setError(null);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
|
52
56
|
e.preventDefault();
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
setError(null);
|
|
59
|
+
|
|
53
60
|
const value = inputRef.current?.value;
|
|
54
61
|
if (!value) return;
|
|
55
62
|
const url = getUrlFromString(value);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
|
|
64
|
+
if (!url) {
|
|
65
|
+
setError(t("editor_invalid_url"));
|
|
66
|
+
return;
|
|
60
67
|
}
|
|
68
|
+
|
|
69
|
+
setMark(schema.marks.link, { href: url })(view.state, view.dispatch);
|
|
70
|
+
view.focus();
|
|
71
|
+
onOpenChange(false);
|
|
61
72
|
};
|
|
62
73
|
|
|
63
74
|
const handleRemoveLink = () => {
|
|
@@ -86,13 +97,15 @@ export const LinkSelector = ({
|
|
|
86
97
|
<form
|
|
87
98
|
onSubmit={handleSubmit}
|
|
88
99
|
className="flex p-1 gap-1"
|
|
100
|
+
onChange={handleChange}
|
|
89
101
|
>
|
|
90
102
|
<input
|
|
91
103
|
ref={inputRef}
|
|
92
104
|
autoFocus={open}
|
|
93
105
|
placeholder={t("editor_paste_or_type_link")}
|
|
94
106
|
defaultValue={href}
|
|
95
|
-
className={cls("text-surface-900 dark:text-white flex-grow bg-transparent p-1 text-sm outline-none", focusedDisabled)}
|
|
107
|
+
className={cls("text-surface-900 dark:text-white flex-grow bg-transparent p-1 text-sm outline-none", focusedDisabled)}
|
|
108
|
+
aria-invalid={isInputInvalid} />
|
|
96
109
|
|
|
97
110
|
{href ? (
|
|
98
111
|
<Button
|
|
@@ -113,6 +126,18 @@ export const LinkSelector = ({
|
|
|
113
126
|
</Button>
|
|
114
127
|
)}
|
|
115
128
|
</form>
|
|
129
|
+
{error && (
|
|
130
|
+
<div className="p-2">
|
|
131
|
+
<Typography
|
|
132
|
+
variant="caption"
|
|
133
|
+
color="error"
|
|
134
|
+
role="alert"
|
|
135
|
+
aria-live="polite"
|
|
136
|
+
>
|
|
137
|
+
{error}
|
|
138
|
+
</Typography>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
116
141
|
</Popover>
|
|
117
142
|
);
|
|
118
143
|
};
|