@rebasepro/admin 0.13.1-canary.gef9608c → 0.14.0
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/{CollectionEditorDialog-7b-kSLno.js → CollectionEditorDialog-CSheud_E.js} +19 -9
- package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
- package/dist/{PropertyEditView-C-31uvpG.js → PropertyEditView-CWZlC1gq.js} +5 -5
- package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-BJylABq3.js → RouterCollectionsStudioView-CwTpdzWF.js} +9 -13
- package/dist/RouterCollectionsStudioView-CwTpdzWF.js.map +1 -0
- package/dist/collection_editor/serializable_types.d.ts +1 -0
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/ClearFilterSortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
- package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
- package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +101 -0
- package/dist/components/CollectionViewBinding/SlotValue.d.ts +26 -0
- package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -6
- package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +1 -1
- package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +26 -1
- package/dist/components/DetailViewBinding.d.ts +7 -0
- package/dist/components/DrawerNavigationGroup.d.ts +4 -2
- package/dist/components/DrawerNavigationItem.d.ts +13 -5
- package/dist/components/EditViewBinding.d.ts +22 -7
- package/dist/components/EntityIdentityBar.d.ts +24 -1
- package/dist/components/RelationSelector.d.ts +13 -0
- package/dist/data_export/export/export.d.ts +7 -0
- package/dist/data_export/export/fetch_export_data.d.ts +48 -0
- package/dist/data_export/export/index.d.ts +1 -0
- package/dist/data_import/utils/csv.d.ts +30 -0
- package/dist/data_import/utils/index.d.ts +2 -0
- package/dist/data_import/utils/save_entities.d.ts +38 -0
- package/dist/data_import/utils/transforms.d.ts +9 -0
- package/dist/editor.js +3 -0
- package/dist/editor.js.map +1 -1
- package/dist/{export-C-XG3aRh.js → export-L53WektO.js} +175 -39
- package/dist/export-L53WektO.js.map +1 -0
- package/dist/form/form_utils.d.ts +38 -1
- package/dist/{history-DfMuvPfr.js → history-D5SKygpJ.js} +2 -2
- package/dist/{history-DfMuvPfr.js.map → history-D5SKygpJ.js.map} +1 -1
- package/dist/{import-B0GyllB0.js → import-C_4edkoD.js} +5 -4
- package/dist/import-C_4edkoD.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +96 -80
- package/dist/index.js.map +1 -1
- package/dist/preview/components/date_fns_locales.d.ts +9 -0
- package/dist/util/view_constants.d.ts +24 -0
- package/dist/util/view_mode.d.ts +18 -0
- package/dist/{util-DHNXodBo.js → util-C-D5yD2n.js} +2173 -673
- package/dist/util-C-D5yD2n.js.map +1 -0
- package/package.json +10 -9
- package/src/collection_editor/serializable_types.ts +1 -0
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +8 -3
- package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
- package/src/components/ClearFilterSortButton.tsx +16 -7
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +2 -2
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
- package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
- package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +18 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
- package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +816 -335
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +45 -10
- package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
- package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
- package/src/components/CollectionViewBinding/SlotValue.tsx +96 -0
- package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -9
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +3 -3
- package/src/components/CollectionViewBinding/SplitListView.tsx +36 -9
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +76 -24
- package/src/components/CollectionViewBinding/utils.ts +7 -4
- package/src/components/DefaultDrawer.tsx +7 -0
- package/src/components/DetailViewBinding.tsx +33 -9
- package/src/components/DrawerNavigationGroup.tsx +12 -32
- package/src/components/DrawerNavigationItem.tsx +33 -12
- package/src/components/EditViewBinding.tsx +175 -28
- package/src/components/EntityIdentityBar.tsx +109 -17
- package/src/components/EntityInspector.tsx +24 -6
- package/src/components/EntityViewBinding.tsx +4 -2
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
- package/src/components/RelationSelector.tsx +61 -11
- package/src/components/SearchIconsView.tsx +14 -33
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
- package/src/components/SidePanelBinding.tsx +36 -25
- package/src/components/UserSelector.tsx +52 -9
- package/src/components/history/LastEditedByIndicator.tsx +7 -12
- package/src/data_export/export/ExportCollectionAction.tsx +101 -48
- package/src/data_export/export/export.ts +63 -16
- package/src/data_export/export/fetch_export_data.ts +90 -0
- package/src/data_export/export/index.ts +1 -0
- package/src/data_import/components/DataNewPropertiesMapping.tsx +20 -3
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -0
- package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
- package/src/data_import/import/ImportCollectionAction.tsx +13 -2
- package/src/data_import/utils/csv.ts +153 -0
- package/src/data_import/utils/data.ts +16 -2
- package/src/data_import/utils/file_to_json.ts +77 -6
- package/src/data_import/utils/index.ts +2 -0
- package/src/data_import/utils/save_entities.ts +138 -0
- package/src/data_import/utils/transforms.ts +23 -0
- package/src/editor/components/image-bubble.tsx +2 -0
- package/src/editor/selectors/link-selector.tsx +1 -0
- package/src/form/EntityForm.tsx +12 -3
- package/src/form/EntityFormBinding.tsx +8 -12
- package/src/form/PropertyFieldBinding.tsx +2 -2
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
- package/src/form/field_bindings/BinaryFieldBinding.tsx +1 -0
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +15 -2
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
- package/src/form/field_bindings/VectorFieldBinding.tsx +1 -0
- package/src/form/form_utils.ts +78 -0
- package/src/index.ts +9 -1
- package/src/preview/PropertyPreview.tsx +7 -3
- package/src/preview/components/DatePreview.tsx +15 -2
- package/src/preview/components/ReferencePreview.tsx +5 -8
- package/src/preview/components/RelationPreview.tsx +8 -7
- package/src/preview/components/date_fns_locales.ts +125 -0
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
- package/src/util/navigation_utils.ts +4 -2
- package/src/util/property_utils.tsx +6 -1
- package/src/util/view_constants.ts +27 -0
- package/src/util/view_mode.ts +36 -0
- package/dist/CollectionEditorDialog-7b-kSLno.js.map +0 -1
- package/dist/PropertyEditView-C-31uvpG.js.map +0 -1
- package/dist/RouterCollectionsStudioView-BJylABq3.js.map +0 -1
- package/dist/export-C-XG3aRh.js.map +0 -1
- package/dist/import-B0GyllB0.js.map +0 -1
- package/dist/util-DHNXodBo.js.map +0 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { PreviewSize } from "../../types/components/PropertyPreviewProps";
|
|
3
3
|
import { PropertyPreview } from "../../preview";
|
|
4
|
+
import { StorageThumbnail } from "../../preview/components/StorageThumbnail";
|
|
5
|
+
import { UrlComponentPreview } from "../../preview/components/UrlComponentPreview";
|
|
6
|
+
import { Chip } from "@rebasepro/ui";
|
|
4
7
|
import type { PreviewSlot } from "./usePreviewSlots";
|
|
5
8
|
|
|
6
9
|
export interface SlotValueProps {
|
|
@@ -42,6 +45,15 @@ export function SlotValue({ slot, size, textOnly = true, fill }: SlotValueProps)
|
|
|
42
45
|
if (!slot || slot.value === undefined || slot.value === null) return null;
|
|
43
46
|
|
|
44
47
|
if (!slot.property || !slot.propertyKey) {
|
|
48
|
+
// A computed image is still an image. "There is no property, so all that
|
|
49
|
+
// can be said about it is that it is text" holds for a title and fails
|
|
50
|
+
// here: `display.image` accepts "a storage path or a URL … so a resolver
|
|
51
|
+
// may return either", and both used to be printed as their own string
|
|
52
|
+
// inside the image frame.
|
|
53
|
+
if (slot.role === "image" && typeof slot.value === "string" && slot.value.trim()) {
|
|
54
|
+
return <ComputedImage value={slot.value.trim()} size={size} fill={fill}/>;
|
|
55
|
+
}
|
|
56
|
+
|
|
45
57
|
// Computed. Arrays (a `tags` resolver) join rather than rendering as
|
|
46
58
|
// "[object Object]" via String().
|
|
47
59
|
const text = Array.isArray(slot.value)
|
|
@@ -58,3 +70,87 @@ export function SlotValue({ slot, size, textOnly = true, fill }: SlotValueProps)
|
|
|
58
70
|
textOnly={textOnly}
|
|
59
71
|
fill={fill}/>;
|
|
60
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The `tags` slot, as chips beside the status.
|
|
76
|
+
*
|
|
77
|
+
* Honours both arms the same way the rest of the file does. A declared **path**
|
|
78
|
+
* goes through {@link SlotValue}, so an array of enum values keeps its own
|
|
79
|
+
* colours — that is the stated advantage of naming a property. A **resolver**
|
|
80
|
+
* returns bare strings with no property to render them as, so they become plain
|
|
81
|
+
* chips; a single string is accepted as one tag, which the role's type documents.
|
|
82
|
+
*
|
|
83
|
+
* Lives here rather than in the list, the card and the board card because that is
|
|
84
|
+
* three copies of the same decision, and this file exists to hold exactly one.
|
|
85
|
+
*/
|
|
86
|
+
export function TagChips({ slot, max = 4 }: { slot: PreviewSlot, max?: number }) {
|
|
87
|
+
|
|
88
|
+
if (slot.property && slot.propertyKey) {
|
|
89
|
+
return <SlotValue slot={slot} size="small"/>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const values = (Array.isArray(slot.value) ? slot.value : [slot.value])
|
|
93
|
+
.filter(item => item !== undefined && item !== null && String(item).trim() !== "")
|
|
94
|
+
.map(item => String(item));
|
|
95
|
+
|
|
96
|
+
if (values.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
const shown = values.slice(0, max);
|
|
99
|
+
const hidden = values.length - shown.length;
|
|
100
|
+
|
|
101
|
+
return <>
|
|
102
|
+
{shown.map(value => (
|
|
103
|
+
<Chip key={value}
|
|
104
|
+
size="smallest"
|
|
105
|
+
className="!text-[10px] !leading-tight !py-0 shrink-0 max-w-[100px] truncate">
|
|
106
|
+
{value}
|
|
107
|
+
</Chip>
|
|
108
|
+
))}
|
|
109
|
+
{hidden > 0 && (
|
|
110
|
+
<span className="text-[10px] text-surface-400 dark:text-surface-500 shrink-0">+{hidden}</span>
|
|
111
|
+
)}
|
|
112
|
+
</>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether a computed image string can be fetched as-is, or has to be resolved
|
|
117
|
+
* through storage first.
|
|
118
|
+
*
|
|
119
|
+
* Exported because it is the whole decision: rendering either branch needs a
|
|
120
|
+
* provider (a storage source, a DOM), and the thing worth pinning is which
|
|
121
|
+
* branch a given string takes. `//host/x` is protocol-relative and equally
|
|
122
|
+
* fetchable; `data:` and `blob:` are already the bytes.
|
|
123
|
+
*/
|
|
124
|
+
export function isFetchableImageUrl(value: string): boolean {
|
|
125
|
+
return /^(https?:)?\/\//i.test(value) || /^(data|blob):/i.test(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A string a resolver returned for the `image` role, rendered as a picture.
|
|
130
|
+
*
|
|
131
|
+
* Two things can arrive here and they need different handling, which is the whole
|
|
132
|
+
* reason a property-path image is preferable: a storage property knows which
|
|
133
|
+
* backend holds it, and a bare string does not.
|
|
134
|
+
*
|
|
135
|
+
* - An **absolute URL** is already fetchable. Rendering it through the storage
|
|
136
|
+
* layer would ask a bucket to sign a path that was never in it.
|
|
137
|
+
* - Anything else is read as a **storage path** on the default source, which is
|
|
138
|
+
* the only source a value with no property can be attributed to. A resolver
|
|
139
|
+
* pointing at a named source should name the property instead.
|
|
140
|
+
*/
|
|
141
|
+
function ComputedImage({ value, size, fill }: { value: string, size: PreviewSize, fill?: boolean }) {
|
|
142
|
+
const isAbsolute = isFetchableImageUrl(value);
|
|
143
|
+
|
|
144
|
+
if (isAbsolute) {
|
|
145
|
+
return <UrlComponentPreview previewType="image"
|
|
146
|
+
url={value}
|
|
147
|
+
size={size}
|
|
148
|
+
fill={fill}
|
|
149
|
+
hint={value}/>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return <StorageThumbnail storagePathOrDownloadUrl={value}
|
|
153
|
+
storeUrl={false}
|
|
154
|
+
size={size}
|
|
155
|
+
fill={fill}/>;
|
|
156
|
+
}
|
|
@@ -24,6 +24,12 @@ export type SortButtonProps<M extends Record<string, unknown>> = {
|
|
|
24
24
|
* the options offered.
|
|
25
25
|
*/
|
|
26
26
|
properties: Properties;
|
|
27
|
+
/**
|
|
28
|
+
* Toolbar showing icon-only controls (split view, or a layout too narrow
|
|
29
|
+
* for the filters button's label). Only decides the icon size, which
|
|
30
|
+
* follows that button — see `CollectionViewStartActions`.
|
|
31
|
+
*/
|
|
32
|
+
compact?: boolean;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -40,7 +46,8 @@ export type SortButtonProps<M extends Record<string, unknown>> = {
|
|
|
40
46
|
*/
|
|
41
47
|
export function SortButton<M extends Record<string, unknown>>({
|
|
42
48
|
tableController,
|
|
43
|
-
properties
|
|
49
|
+
properties,
|
|
50
|
+
compact
|
|
44
51
|
}: SortButtonProps<M>) {
|
|
45
52
|
|
|
46
53
|
const { t } = useTranslation();
|
|
@@ -97,6 +104,8 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
97
104
|
// `asChild` — is what toggles it. A handler of our own would toggle it a
|
|
98
105
|
// second time in the same click and the popover would never open; a
|
|
99
106
|
// `Tooltip` in between would swallow the trigger's ref.
|
|
107
|
+
const arrowSize = compact ? iconSize.smallest : iconSize.small;
|
|
108
|
+
|
|
100
109
|
const trigger = (
|
|
101
110
|
<IconButton
|
|
102
111
|
size={"small"}
|
|
@@ -104,11 +113,14 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
104
113
|
title={label}
|
|
105
114
|
className={cls(activeOption && "text-primary")}
|
|
106
115
|
>
|
|
116
|
+
{/* Same icon size as the filter controls it sits between: 16 in the
|
|
117
|
+
split view's narrow toolbar, 20 everywhere else. It was a flat
|
|
118
|
+
16, which read as a smaller button next to them. */}
|
|
107
119
|
{activeDirection === "desc"
|
|
108
|
-
? <ArrowDownIcon size={
|
|
120
|
+
? <ArrowDownIcon size={arrowSize}/>
|
|
109
121
|
: activeDirection === "asc"
|
|
110
|
-
? <ArrowUpIcon size={
|
|
111
|
-
: <ArrowUpDownIcon size={
|
|
122
|
+
? <ArrowUpIcon size={arrowSize}/>
|
|
123
|
+
: <ArrowUpDownIcon size={arrowSize}/>}
|
|
112
124
|
</IconButton>
|
|
113
125
|
);
|
|
114
126
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { IconButton, iconSize, PanelLeftCloseIcon, Tooltip } from "@rebasepro/ui";
|
|
4
4
|
import { useTranslation } from "@rebasepro/app";
|
|
5
5
|
|
|
6
6
|
import { useSplitView } from "./SplitViewContext";
|
|
@@ -9,13 +9,24 @@ import { useSplitView } from "./SplitViewContext";
|
|
|
9
9
|
* Closes the list of a split view, leaving the open record on its own.
|
|
10
10
|
*
|
|
11
11
|
* It leads the record's own app bar, where the breadcrumb starts — this is the
|
|
12
|
-
* entity pane's old "Open full screen" button, moved to that edge
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* entity pane's old "Open full screen" button, moved to that edge: closing the
|
|
13
|
+
* list and opening the record full screen are the same thing, so there is one
|
|
14
|
+
* control and one route (`#full`) rather than two. Full screen carries the
|
|
15
|
+
* mirrored icon to bring the list back.
|
|
16
|
+
*
|
|
17
|
+
* A pane glyph rather than the double chevron it wore first. Now that the bar
|
|
18
|
+
* ends in a ✕ that dismisses the record, an arrow pointing off the left edge of
|
|
19
|
+
* the record's own bar was the other thing on it that read as "close me" — and
|
|
20
|
+
* it is the one control here that does not act on the record at all.
|
|
17
21
|
*/
|
|
18
|
-
export function SplitListCloseButton(
|
|
22
|
+
export function SplitListCloseButton({ onBeforeHide }: {
|
|
23
|
+
/**
|
|
24
|
+
* Run before the layout changes, while this form is still mounted: the edit
|
|
25
|
+
* view uses it to hand its edit in progress to the full-screen form that is
|
|
26
|
+
* about to show the same record.
|
|
27
|
+
*/
|
|
28
|
+
onBeforeHide?: () => void;
|
|
29
|
+
} = {}) {
|
|
19
30
|
|
|
20
31
|
const { t } = useTranslation();
|
|
21
32
|
const splitView = useSplitView();
|
|
@@ -26,9 +37,12 @@ export function SplitListCloseButton() {
|
|
|
26
37
|
<Tooltip title={t("hide_list")}>
|
|
27
38
|
<IconButton
|
|
28
39
|
size="small"
|
|
29
|
-
onClick={
|
|
40
|
+
onClick={() => {
|
|
41
|
+
onBeforeHide?.();
|
|
42
|
+
splitView.openFullScreen();
|
|
43
|
+
}}
|
|
30
44
|
aria-label={t("hide_list")}>
|
|
31
|
-
<
|
|
45
|
+
<PanelLeftCloseIcon size={iconSize.smallest}/>
|
|
32
46
|
</IconButton>
|
|
33
47
|
</Tooltip>
|
|
34
48
|
);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { IconButton, iconSize, PanelLeftOpenIcon, Tooltip } from "@rebasepro/ui";
|
|
4
4
|
import { useTranslation } from "@rebasepro/app";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Brings the list back beside a record that was opened full screen.
|
|
8
8
|
*
|
|
9
9
|
* The mirror of {@link SplitListCloseButton}: the same edge of the same bar,
|
|
10
|
-
* the same
|
|
10
|
+
* the same pane glyph opening rather than closing, and the same single route —
|
|
11
11
|
* showing the list is dropping the `#full` hash. It replaces the back arrow
|
|
12
12
|
* where it applies, because the list it unfolds already holds the record.
|
|
13
13
|
*/
|
|
@@ -21,7 +21,7 @@ export function SplitListShowButton({ onClick }: { onClick: () => void }) {
|
|
|
21
21
|
size="small"
|
|
22
22
|
onClick={onClick}
|
|
23
23
|
aria-label={t("show_list")}>
|
|
24
|
-
<
|
|
24
|
+
<PanelLeftOpenIcon size={iconSize.smallest}/>
|
|
25
25
|
</IconButton>
|
|
26
26
|
</Tooltip>
|
|
27
27
|
);
|
|
@@ -85,12 +85,14 @@ function isListShortcutTarget(target: HTMLElement | null, detailPanel: HTMLEleme
|
|
|
85
85
|
* The list only has to identify a record — a title, a subtitle and a status
|
|
86
86
|
* chip — while the detail beside it carries the whole record and its rail. At
|
|
87
87
|
* 30% the list was taking width from the side that had four columns of fields
|
|
88
|
-
* to lay out.
|
|
88
|
+
* to lay out; 26% still was, by about the width of one field.
|
|
89
89
|
*
|
|
90
90
|
* A default, not a constraint: {@link savePanelSize} remembers the drag per
|
|
91
|
-
* collection, so anyone who has already sized this list keeps their size.
|
|
91
|
+
* collection, so anyone who has already sized this list keeps their size. The
|
|
92
|
+
* drag floor is `minPanelSizePx`, and it holds below ~1100px of split — at that
|
|
93
|
+
* width this percentage is already the floor.
|
|
92
94
|
*/
|
|
93
|
-
const DEFAULT_PANEL_SIZE =
|
|
95
|
+
const DEFAULT_PANEL_SIZE = 22;
|
|
94
96
|
|
|
95
97
|
function getSavedPanelSize(path: string): number {
|
|
96
98
|
try {
|
|
@@ -163,8 +165,20 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
163
165
|
dirty.current = modified;
|
|
164
166
|
}, []);
|
|
165
167
|
|
|
166
|
-
// The form remounts per entity, so its dirty state must not leak into the
|
|
168
|
+
// The form remounts per entity, so its dirty state must not leak into the
|
|
169
|
+
// next one — and it has to be cleared here rather than left to the next
|
|
170
|
+
// form's own first report, because that form spends the load in a skeleton
|
|
171
|
+
// and a navigation during it would prompt about the record before it.
|
|
172
|
+
//
|
|
173
|
+
// On a *change* of record only, never on this view's own mount: a record
|
|
174
|
+
// arriving from full screen carries its edit back in and opens dirty, and
|
|
175
|
+
// the form says so from its first effect — which runs before this one, being
|
|
176
|
+
// a child's. Clearing unconditionally overwrote that report with `false`,
|
|
177
|
+
// and the way out of the split stopped asking about a live edit.
|
|
178
|
+
const previousEntityId = useRef(selectedEntityId);
|
|
167
179
|
useEffect(() => {
|
|
180
|
+
if (previousEntityId.current === selectedEntityId) return;
|
|
181
|
+
previousEntityId.current = selectedEntityId;
|
|
168
182
|
dirty.current = false;
|
|
169
183
|
}, [selectedEntityId]);
|
|
170
184
|
|
|
@@ -183,12 +197,19 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
183
197
|
return true;
|
|
184
198
|
}, []));
|
|
185
199
|
|
|
186
|
-
// Panel size state with persistence
|
|
200
|
+
// Panel size state with persistence.
|
|
201
|
+
//
|
|
202
|
+
// Written on drag, not on mount. An effect that saved on mount stored
|
|
203
|
+
// {@link DEFAULT_PANEL_SIZE} under this collection's key the first time
|
|
204
|
+
// anyone opened the split — after which the default was frozen in every
|
|
205
|
+
// browser that had ever seen it, and changing it here reached nobody but a
|
|
206
|
+
// new visitor. Only a size someone chose is a size worth remembering.
|
|
187
207
|
const [panelSize, setPanelSize] = useState(() => getSavedPanelSize(path));
|
|
188
208
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
209
|
+
const handlePanelSizeChange = useCallback((size: number) => {
|
|
210
|
+
setPanelSize(size);
|
|
211
|
+
savePanelSize(path, size);
|
|
212
|
+
}, [path]);
|
|
192
213
|
|
|
193
214
|
// ── Animation state ──
|
|
194
215
|
// We track the "rendered" entity to keep the detail panel mounted during the exit animation.
|
|
@@ -379,6 +400,7 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
379
400
|
parentEntityIds={usedParentEntityIds}
|
|
380
401
|
selectedTab={selectedTab}
|
|
381
402
|
layout="split"
|
|
403
|
+
onCloseRequest={handleCloseDetail}
|
|
382
404
|
onEditClick={() => {
|
|
383
405
|
navigate(buildUrl(`${path}/${renderedEntityId}/edit`));
|
|
384
406
|
}}
|
|
@@ -400,6 +422,11 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
400
422
|
parentEntityIds={usedParentEntityIds}
|
|
401
423
|
selectedTab={selectedTab}
|
|
402
424
|
layout="split"
|
|
425
|
+
// The ✕, and where "save and close" lands. A plain
|
|
426
|
+
// navigation: `useNavigationBlocker` above already
|
|
427
|
+
// stands between it and an unsaved edit, so the ✕ asks
|
|
428
|
+
// exactly what Escape and the browser's Back ask.
|
|
429
|
+
onCloseRequest={handleCloseDetail}
|
|
403
430
|
onValuesModified={setDirty}
|
|
404
431
|
onSaved={() => {
|
|
405
432
|
setDirty(false);
|
|
@@ -444,7 +471,7 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
444
471
|
secondPanel={detailPanel}
|
|
445
472
|
showSecondPanel={isDetailVisible}
|
|
446
473
|
panelSizePercent={animationPhase === "entering" ? 100 : panelSize}
|
|
447
|
-
onPanelSizeChange={
|
|
474
|
+
onPanelSizeChange={handlePanelSizeChange}
|
|
448
475
|
minPanelSizePx={240}
|
|
449
476
|
/>
|
|
450
477
|
|
|
@@ -79,12 +79,17 @@ export function ViewModeToggle({
|
|
|
79
79
|
const { t } = useTranslation();
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
// Get icon for current view mode
|
|
82
|
+
// Get icon for current view mode.
|
|
83
|
+
//
|
|
84
|
+
// `small` on the trigger, matching the filters button beside it — the two
|
|
85
|
+
// labelled buttons open the toolbar together, and at 16 against its 20 this
|
|
86
|
+
// one read as the smaller of the pair. The options inside the popover stay
|
|
87
|
+
// at `smallest`, which is the size every other menu icon uses.
|
|
83
88
|
const getViewModeIcon = () => {
|
|
84
|
-
if (viewMode === "kanban") return <KanbanIcon size={iconSize.
|
|
85
|
-
if (viewMode === "cards") return <LayoutGridIcon size={iconSize.
|
|
86
|
-
if (viewMode === "table") return <TableIcon size={iconSize.
|
|
87
|
-
return <ListIcon size={iconSize.
|
|
89
|
+
if (viewMode === "kanban") return <KanbanIcon size={iconSize.small}/>;
|
|
90
|
+
if (viewMode === "cards") return <LayoutGridIcon size={iconSize.small}/>;
|
|
91
|
+
if (viewMode === "table") return <TableIcon size={iconSize.small}/>;
|
|
92
|
+
return <ListIcon size={iconSize.small}/>;
|
|
88
93
|
};
|
|
89
94
|
|
|
90
95
|
const getViewModeName = () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { Entity, FilterValues } from "@rebasepro/types";
|
|
2
|
+
import { Entity, FilterValues, MAX_LIST_LIMIT } from "@rebasepro/types";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { getRelationIncludeParams, useData, useRebaseContext } from "@rebasepro/app";
|
|
5
5
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
@@ -220,6 +220,14 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
|
|
|
220
220
|
// Skip if we're in the middle of cleanup
|
|
221
221
|
if (isCleaningUpRef.current) return;
|
|
222
222
|
|
|
223
|
+
// `itemCount` grows by one page per "load more" click with nothing
|
|
224
|
+
// stopping it, and a read above `MAX_LIST_LIMIT` is refused rather than
|
|
225
|
+
// trimmed — a column would stop loading with an error instead of
|
|
226
|
+
// stopping at the ceiling. Ask for what the platform serves; `hasMore`
|
|
227
|
+
// below still compares against the count the board asked for, so the
|
|
228
|
+
// column reports itself exhausted exactly where it did before.
|
|
229
|
+
const requestedCount = Math.min(itemCount, MAX_LIST_LIMIT);
|
|
230
|
+
|
|
223
231
|
const currentDataClient = dataClientRef.current;
|
|
224
232
|
const currentCollection = collectionRef.current;
|
|
225
233
|
const currentContext = contextRef.current;
|
|
@@ -406,7 +414,7 @@ values: { ...e.values,
|
|
|
406
414
|
|
|
407
415
|
const fetchOnce = () => accessor.find({
|
|
408
416
|
where: whereFilter,
|
|
409
|
-
limit:
|
|
417
|
+
limit: requestedCount,
|
|
410
418
|
orderBy: orderByParam,
|
|
411
419
|
include: includeParams,
|
|
412
420
|
searchString: currentSearchString
|
|
@@ -445,7 +453,7 @@ values: { ...e.values,
|
|
|
445
453
|
let liveDataReceived = false;
|
|
446
454
|
const unsubscribe = accessor.listen({
|
|
447
455
|
where: whereFilter,
|
|
448
|
-
limit:
|
|
456
|
+
limit: requestedCount,
|
|
449
457
|
orderBy: orderByParam,
|
|
450
458
|
include: includeParams,
|
|
451
459
|
// Read into a local at the top of this function and then used
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { Property, RelationProperty } from "@rebasepro/types";
|
|
3
3
|
import type { Entity, EntityRelation } from "@rebasepro/types";
|
|
4
|
-
import type { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
|
|
4
|
+
import type { PropertyConfig, AdminCollection, EntityDisplayRole } from "@rebasepro/admin-types";
|
|
5
5
|
import { getEntityImagePreviewPropertyKey } from "@rebasepro/app";
|
|
6
6
|
import { getDisplayPropertyKey } from "@rebasepro/app";
|
|
7
7
|
import { getLeadingRelationTitleKey, getTitlePropertyCandidates, looksLikeIdentifierValue } from "@rebasepro/app";
|
|
8
8
|
import { getEntityFromCache } from "@rebasepro/app";
|
|
9
9
|
import { getEntityPreviewKeys } from "../../util/previews";
|
|
10
|
-
import { getValueInPath } from "@rebasepro/utils";
|
|
10
|
+
import { formatRelativeTime, getValueInPath } from "@rebasepro/utils";
|
|
11
11
|
import type { AuthController } from "@rebasepro/admin-types";
|
|
12
12
|
import { ChipColorScheme, CHIP_COLORS, CHIP_SEED_KEYS } from "@rebasepro/ui";
|
|
13
13
|
import { useEntityDisplay } from "../../hooks/useEntityDisplay";
|
|
@@ -38,6 +38,16 @@ export interface PreviewSlot {
|
|
|
38
38
|
property?: Property;
|
|
39
39
|
propertyKey?: string;
|
|
40
40
|
value: unknown;
|
|
41
|
+
/**
|
|
42
|
+
* Which display role this slot fills.
|
|
43
|
+
*
|
|
44
|
+
* Load-bearing for the computed case. Without a property there is nothing to
|
|
45
|
+
* infer from, and "render it as text" is right for a title and wrong for an
|
|
46
|
+
* image: a resolver that returned a URL had that URL printed inside the card's
|
|
47
|
+
* image frame. The role is the one thing that distinguishes them, so it
|
|
48
|
+
* travels with the slot rather than being re-derived at each call site.
|
|
49
|
+
*/
|
|
50
|
+
role?: EntityDisplayRole;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
/**
|
|
@@ -97,6 +107,19 @@ export interface EntityPreviewSlots {
|
|
|
97
107
|
status: PreviewSlot | undefined;
|
|
98
108
|
/** META — date / timestamp */
|
|
99
109
|
date: DatePreviewSlot | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* TAGS — free chips beside the status.
|
|
112
|
+
*
|
|
113
|
+
* Declared only. Every other role has an inference ladder behind it, and
|
|
114
|
+
* this one deliberately does not: there is no "first enum" or "looks like a
|
|
115
|
+
* timestamp" that means *labels*, so guessing would scatter arbitrary chips
|
|
116
|
+
* across every row of every panel that never asked for them.
|
|
117
|
+
*
|
|
118
|
+
* The role existed on `EntityDisplay` and in `useEntityDisplay` before this
|
|
119
|
+
* field did, which is why a declared `display.tags` resolved correctly and
|
|
120
|
+
* then had nowhere to go.
|
|
121
|
+
*/
|
|
122
|
+
tags: PreviewSlot | undefined;
|
|
100
123
|
/** Entity ID (always available) */
|
|
101
124
|
entityId: string | number;
|
|
102
125
|
}
|
|
@@ -116,6 +139,8 @@ export interface CollectionSlotKeys {
|
|
|
116
139
|
relationKeys: string[];
|
|
117
140
|
statusKey: string | undefined;
|
|
118
141
|
dateKey: string | undefined;
|
|
142
|
+
/** Declared only — see {@link EntityPreviewSlots.tags}. */
|
|
143
|
+
tagsKey: string | undefined;
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
/**
|
|
@@ -250,7 +275,8 @@ imageKey,
|
|
|
250
275
|
subtitleKey,
|
|
251
276
|
relationKeys,
|
|
252
277
|
statusKey,
|
|
253
|
-
dateKey
|
|
278
|
+
dateKey,
|
|
279
|
+
tagsKey: getDisplayPropertyKey(collection, "tags") };
|
|
254
280
|
}
|
|
255
281
|
|
|
256
282
|
// ── Image unwrapping helper ───────────────────────────────────────────
|
|
@@ -281,6 +307,7 @@ function resolveImageSlot(
|
|
|
281
307
|
if (resolvedValue === undefined || resolvedValue === null) return undefined;
|
|
282
308
|
|
|
283
309
|
return {
|
|
310
|
+
role: "image",
|
|
284
311
|
property: resolvedProperty as Property,
|
|
285
312
|
propertyKey: imageKey,
|
|
286
313
|
value: resolvedValue
|
|
@@ -319,15 +346,11 @@ function formatDateValue(value: unknown): string {
|
|
|
319
346
|
if (isNaN(date.getTime())) return String(value);
|
|
320
347
|
|
|
321
348
|
const now = new Date();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
if (diffMins < 1) return "Just now";
|
|
328
|
-
if (diffMins < 60) return `${diffMins}m ago`;
|
|
329
|
-
if (diffHours < 24) return `${diffHours}h ago`;
|
|
330
|
-
if (diffDays < 7) return `${diffDays}d ago`;
|
|
349
|
+
// A date slot holds whatever property the collection points it at, which is
|
|
350
|
+
// as often a due or publish date as a `updated_at`. `formatRelativeTime`
|
|
351
|
+
// reads the sign, so a scheduled date says "in 3d" instead of "Just now".
|
|
352
|
+
const relative = formatRelativeTime(date, { now });
|
|
353
|
+
if (relative) return relative;
|
|
331
354
|
|
|
332
355
|
return date.toLocaleDateString(undefined, {
|
|
333
356
|
month: "short",
|
|
@@ -368,7 +391,7 @@ export function resolveEntitySlots(
|
|
|
368
391
|
collection: AdminCollection<Record<string, unknown>>,
|
|
369
392
|
slotKeys: CollectionSlotKeys
|
|
370
393
|
): EntityPreviewSlots {
|
|
371
|
-
const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey } = slotKeys;
|
|
394
|
+
const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey, tagsKey } = slotKeys;
|
|
372
395
|
|
|
373
396
|
// Image
|
|
374
397
|
const image = imageKey ? resolveImageSlot(collection, imageKey, entity) : undefined;
|
|
@@ -381,7 +404,8 @@ export function resolveEntitySlots(
|
|
|
381
404
|
if (resolvedTitleKey) {
|
|
382
405
|
const prop = collection.properties[resolvedTitleKey] as Property | undefined;
|
|
383
406
|
const val = getValueInPath(entity.values, resolvedTitleKey);
|
|
384
|
-
if (prop) title = {
|
|
407
|
+
if (prop) title = { role: "title",
|
|
408
|
+
property: prop,
|
|
385
409
|
propertyKey: resolvedTitleKey,
|
|
386
410
|
value: val };
|
|
387
411
|
}
|
|
@@ -392,7 +416,8 @@ value: val };
|
|
|
392
416
|
const prop = collection.properties[subtitleKey] as Property | undefined;
|
|
393
417
|
const val = getValueInPath(entity.values, subtitleKey);
|
|
394
418
|
if (prop && val !== undefined && val !== null && val !== "") {
|
|
395
|
-
subtitle = {
|
|
419
|
+
subtitle = { role: "subtitle",
|
|
420
|
+
property: prop,
|
|
396
421
|
propertyKey: subtitleKey,
|
|
397
422
|
value: val };
|
|
398
423
|
}
|
|
@@ -468,7 +493,8 @@ id: relation.id });
|
|
|
468
493
|
const prop = collection.properties[statusKey] as Property | undefined;
|
|
469
494
|
const val = getValueInPath(entity.values, statusKey);
|
|
470
495
|
if (prop && val !== undefined && val !== null) {
|
|
471
|
-
status = {
|
|
496
|
+
status = { role: "status",
|
|
497
|
+
property: prop,
|
|
472
498
|
propertyKey: statusKey,
|
|
473
499
|
value: val };
|
|
474
500
|
}
|
|
@@ -481,6 +507,7 @@ value: val };
|
|
|
481
507
|
const val = getValueInPath(entity.values, dateKey);
|
|
482
508
|
if (prop && val !== undefined && val !== null) {
|
|
483
509
|
date = {
|
|
510
|
+
role: "date",
|
|
484
511
|
property: prop,
|
|
485
512
|
propertyKey: dateKey,
|
|
486
513
|
value: val,
|
|
@@ -489,6 +516,23 @@ value: val };
|
|
|
489
516
|
}
|
|
490
517
|
}
|
|
491
518
|
|
|
519
|
+
// Tags — declared only, and empty means absent: an empty array is "this
|
|
520
|
+
// record has no labels", which must render as nothing rather than as an
|
|
521
|
+
// empty chip row that shifts every other slot down.
|
|
522
|
+
let tags: PreviewSlot | undefined;
|
|
523
|
+
if (tagsKey) {
|
|
524
|
+
const prop = collection.properties[tagsKey] as Property | undefined;
|
|
525
|
+
const val = getValueInPath(entity.values, tagsKey);
|
|
526
|
+
const empty = val === undefined || val === null || val === ""
|
|
527
|
+
|| (Array.isArray(val) && val.length === 0);
|
|
528
|
+
if (prop && !empty) {
|
|
529
|
+
tags = { role: "tags",
|
|
530
|
+
property: prop,
|
|
531
|
+
propertyKey: tagsKey,
|
|
532
|
+
value: val };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
492
536
|
return {
|
|
493
537
|
image,
|
|
494
538
|
title,
|
|
@@ -496,6 +540,7 @@ value: val };
|
|
|
496
540
|
relations,
|
|
497
541
|
status,
|
|
498
542
|
date,
|
|
543
|
+
tags,
|
|
499
544
|
entityId: entity.id
|
|
500
545
|
};
|
|
501
546
|
}
|
|
@@ -622,30 +667,37 @@ entity };
|
|
|
622
667
|
const image = useEntityDisplay<string>("image", params);
|
|
623
668
|
const status = useEntityDisplay<string>("status", params);
|
|
624
669
|
const date = useEntityDisplay<Date | string | number>("date", params);
|
|
670
|
+
const tags = useEntityDisplay<string[] | string>("tags", params);
|
|
625
671
|
|
|
626
672
|
return useMemo(() => {
|
|
627
673
|
// `source` is set only when the value came from a property, so its
|
|
628
674
|
// absence is exactly the "this was computed" signal — no second flag to
|
|
629
675
|
// keep in step with the first.
|
|
676
|
+
// The role travels with the computed slot too. It is the only thing left
|
|
677
|
+
// to say about a value with no property behind it, and `SlotValue` needs
|
|
678
|
+
// it to tell a computed image from a computed title.
|
|
630
679
|
const computed = (
|
|
680
|
+
role: EntityDisplayRole,
|
|
631
681
|
resolved: { value: unknown, source?: unknown }
|
|
632
682
|
): PreviewSlot | undefined =>
|
|
633
683
|
resolved.value !== undefined && resolved.source === undefined
|
|
634
|
-
? {
|
|
684
|
+
? { role,
|
|
685
|
+
value: resolved.value }
|
|
635
686
|
: undefined;
|
|
636
687
|
|
|
637
|
-
const computedDate = computed(date);
|
|
688
|
+
const computedDate = computed("date", date);
|
|
638
689
|
|
|
639
690
|
return {
|
|
640
691
|
...derived,
|
|
641
|
-
title: computed(title) ?? derived.title,
|
|
642
|
-
subtitle: computed(subtitle) ?? derived.subtitle,
|
|
643
|
-
image: computed(image) ?? derived.image,
|
|
644
|
-
status: computed(status) ?? derived.status,
|
|
692
|
+
title: computed("title", title) ?? derived.title,
|
|
693
|
+
subtitle: computed("subtitle", subtitle) ?? derived.subtitle,
|
|
694
|
+
image: computed("image", image) ?? derived.image,
|
|
695
|
+
status: computed("status", status) ?? derived.status,
|
|
645
696
|
date: computedDate
|
|
646
697
|
? { ...computedDate,
|
|
647
698
|
formatted: formatDateValue(computedDate.value) }
|
|
648
|
-
: derived.date
|
|
699
|
+
: derived.date,
|
|
700
|
+
tags: computed("tags", tags) ?? derived.tags
|
|
649
701
|
};
|
|
650
|
-
}, [derived, title, subtitle, image, status, date]);
|
|
702
|
+
}, [derived, title, subtitle, image, status, date, tags]);
|
|
651
703
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isArrayValue, readStoredJson, writeStoredJson } from "@rebasepro/utils";
|
|
2
|
+
|
|
1
3
|
export function addRecentId(collectionId: string, id: string | number) {
|
|
2
4
|
const recentIds = getRecentIds(collectionId);
|
|
3
5
|
const newRecentIds = [id, ...recentIds.filter(i => i !== id)];
|
|
@@ -9,11 +11,12 @@ export function addRecentId(collectionId: string, id: string | number) {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function saveSearchedIdsLocally(collectionId: string, ids: (string | number)[]) {
|
|
12
|
-
|
|
14
|
+
writeStoredJson("recent_id_searches::" + collectionId, ids);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function getRecentIds(collectionId: string): (string | number)[] {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
return readStoredJson<(string | number)[]>(
|
|
19
|
+
"recent_id_searches::" + collectionId,
|
|
20
|
+
{ fallback: [], accept: isArrayValue }
|
|
21
|
+
);
|
|
19
22
|
}
|
|
@@ -274,13 +274,20 @@ export function DrawerLogo({
|
|
|
274
274
|
}) {
|
|
275
275
|
|
|
276
276
|
const showFullContent = drawerOpen || drawerHovered;
|
|
277
|
+
const { t } = useTranslation();
|
|
277
278
|
|
|
278
279
|
return (
|
|
279
280
|
<div className="flex flex-row items-center shrink-0 pt-4 pb-0 px-2">
|
|
280
281
|
{/* Logo — always visible */}
|
|
282
|
+
{/* Named explicitly because neither branch below carries text: the
|
|
283
|
+
stock `RebaseLogo` is a bare <svg> with no <title>, and a custom
|
|
284
|
+
`logo` only ever gets the generic alt "Logo", which names the image
|
|
285
|
+
rather than saying where the link goes. The title link beside it is
|
|
286
|
+
no help either — it renders only when a `title` is set. */}
|
|
281
287
|
<Link
|
|
282
288
|
className="shrink-0 flex items-center justify-center w-[56px] h-[40px]"
|
|
283
289
|
to={logoDestination}
|
|
290
|
+
aria-label={t("home") || "Home"}
|
|
284
291
|
>
|
|
285
292
|
{logo
|
|
286
293
|
? <img src={logo} alt="Logo" className="w-[28px] h-[28px] object-contain"/>
|