@rebasepro/admin 0.12.1-canary.gf5f1d39 → 0.13.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/README.md +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-CjXkWZ64.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-W8I3YV89.js} +36 -16
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-BsLTNoqX.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-BsLTNoqX.js.map} +1 -1
- package/dist/collection_editor/serializable_types.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
- package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/collection_editor_ui.js.map +1 -1
- package/dist/components/CollectionTableBinding/column_utils.d.ts +17 -0
- package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +7 -2
- package/dist/components/CollectionViewBinding/SortButton.d.ts +24 -0
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
- package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
- package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
- package/dist/components/DetailViewBinding.d.ts +5 -0
- package/dist/components/EditFormActions.d.ts +12 -1
- package/dist/components/EditViewBinding.d.ts +6 -1
- package/dist/components/EntityIdentityBar.d.ts +53 -0
- package/dist/components/EntityInspector.d.ts +35 -0
- package/dist/components/EntityPreviewNesting.d.ts +25 -0
- package/dist/components/EntityViewBinding.d.ts +9 -0
- package/dist/components/InlineEntityPreview.d.ts +47 -0
- package/dist/components/history/EntityHistoryView.d.ts +4 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/editor.js +18 -10
- package/dist/editor.js.map +1 -1
- package/dist/{export-BzIpUdSK.js → export-J79rMoNj.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-J79rMoNj.js.map} +1 -1
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/EntityFormActions.d.ts +11 -1
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/FieldBlock.d.ts +30 -0
- package/dist/form/components/FormRail.d.ts +18 -0
- package/dist/form/components/FormSections.d.ts +16 -0
- package/dist/form/components/index.d.ts +3 -2
- package/dist/form/components/useRailVisible.d.ts +24 -0
- package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
- package/dist/{history-g9688IaK.js → history-D7L_JlLO.js} +35 -33
- package/dist/history-D7L_JlLO.js.map +1 -0
- package/dist/hooks/navigation/utils.d.ts +16 -0
- package/dist/hooks/useBuildSidePanel.d.ts +14 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
- package/dist/hooks/useHistory.d.ts +7 -0
- package/dist/hooks/useRecordActions.d.ts +17 -0
- package/dist/{import-BjUG-cIY.js → import-BvYl--U4.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-BvYl--U4.js.map} +1 -1
- package/dist/index.js +106 -35
- package/dist/index.js.map +1 -1
- package/dist/markdown-CklalUUk.js.map +1 -1
- package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
- package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
- package/dist/types/components/EntityFormProps.d.ts +0 -5
- package/dist/types/fields.d.ts +24 -0
- package/dist/{util-B5fJm1FA.js → util-GiwPgxnL.js} +2106 -1117
- package/dist/util-GiwPgxnL.js.map +1 -0
- package/package.json +32 -34
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/serializable_types.ts +4 -3
- package/src/collection_editor/serializable_utils.ts +51 -17
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
- package/src/collection_editor/validateCollectionJson.ts +3 -3
- package/src/components/AdminModeSyncer.tsx +1 -1
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +59 -2
- package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +9 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +47 -12
- package/src/components/CollectionViewBinding/SortButton.tsx +189 -0
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
- package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
- package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
- package/src/components/DefaultAppBar.tsx +1 -1
- package/src/components/DefaultDrawer.tsx +1 -1
- package/src/components/DetailViewBinding.tsx +112 -148
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditFormActions.tsx +54 -129
- package/src/components/EditViewBinding.tsx +202 -99
- package/src/components/EntityIdentityBar.tsx +288 -0
- package/src/components/EntityInspector.tsx +137 -0
- package/src/components/EntityPreviewBinding.tsx +6 -2
- package/src/components/EntityPreviewNesting.tsx +50 -0
- package/src/components/EntityViewBinding.tsx +35 -32
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
- package/src/components/InlineEntityPreview.tsx +149 -0
- package/src/components/RebaseLayout.tsx +1 -1
- package/src/components/RebaseNavigation.tsx +45 -5
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SidePanelBinding.tsx +19 -13
- package/src/components/UserSelector.tsx +12 -6
- package/src/components/common/default_entity_actions.tsx +4 -3
- package/src/components/history/EntityHistoryEntry.tsx +28 -7
- package/src/components/history/EntityHistoryView.tsx +29 -24
- package/src/components/history/LastEditedByIndicator.tsx +4 -3
- package/src/components/index.ts +2 -0
- package/src/data_import/utils/get_import_inference_type.ts +6 -0
- package/src/form/EntityForm.tsx +267 -179
- package/src/form/EntityFormActions.tsx +50 -99
- package/src/form/EntityFormBinding.tsx +4 -2
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/FieldBlock.tsx +119 -0
- package/src/form/components/FormRail.tsx +125 -0
- package/src/form/components/FormSections.tsx +118 -0
- package/src/form/components/index.tsx +3 -2
- package/src/form/components/useRailVisible.tsx +46 -0
- package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
- package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
- package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
- package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
- package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
- package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
- package/src/hooks/navigation/utils.ts +20 -0
- package/src/hooks/useBuildSideDialogsController.tsx +2 -2
- package/src/hooks/useBuildSidePanel.tsx +22 -61
- package/src/hooks/useEntityDisplayTitle.tsx +60 -0
- package/src/hooks/useHistory.ts +20 -10
- package/src/hooks/useRecordActions.tsx +69 -0
- package/src/hooks/useResolvedUsers.ts +10 -5
- package/src/preview/PropertyPreview.tsx +48 -22
- package/src/preview/components/InlineEntityListPreview.tsx +42 -0
- package/src/preview/components/ReferencePreview.tsx +34 -14
- package/src/preview/components/RelationPreview.tsx +34 -14
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
- package/src/routes/RebaseRoute.tsx +2 -2
- package/src/types/components/EntityFormActionsProps.tsx +0 -1
- package/src/types/components/EntityFormProps.tsx +0 -6
- package/src/types/fields.tsx +27 -0
- package/dist/form/components/FormEntry.d.ts +0 -6
- package/dist/form/components/FormLayout.d.ts +0 -5
- package/dist/history-g9688IaK.js.map +0 -1
- package/dist/util-B5fJm1FA.js.map +0 -1
- package/src/form/components/FormEntry.tsx +0 -22
- package/src/form/components/FormLayout.tsx +0 -16
|
@@ -9,8 +9,6 @@ import { getIcon } from "@rebasepro/app";
|
|
|
9
9
|
import {
|
|
10
10
|
AlertCircleIcon,
|
|
11
11
|
Button,
|
|
12
|
-
cls,
|
|
13
|
-
defaultBorderMixin,
|
|
14
12
|
DialogActions,
|
|
15
13
|
IconButton,
|
|
16
14
|
LoadingButton,
|
|
@@ -19,11 +17,20 @@ import {
|
|
|
19
17
|
} from "@rebasepro/ui";
|
|
20
18
|
import { FormexController } from "@rebasepro/forms";
|
|
21
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The form's own action bar.
|
|
22
|
+
*
|
|
23
|
+
* There used to be two variants: this one, and a `w-80 2xl:w-96` sticky rail
|
|
24
|
+
* holding the same two buttons above several hundred pixels of nothing — a
|
|
25
|
+
* fifth of a 1440px viewport reserved for a Save button. The rail is gone. When
|
|
26
|
+
* the container supplies its own actions (the entity view's identity bar does)
|
|
27
|
+
* it passes `showDefaultActions={false}`, and this renders only what the bar
|
|
28
|
+
* cannot host: the saving error, plugin actions, and collection form actions.
|
|
29
|
+
*/
|
|
22
30
|
export function EntityFormActions({
|
|
23
31
|
path,
|
|
24
32
|
collection,
|
|
25
33
|
entity,
|
|
26
|
-
layout,
|
|
27
34
|
savingError,
|
|
28
35
|
formex,
|
|
29
36
|
disabled,
|
|
@@ -31,10 +38,11 @@ export function EntityFormActions({
|
|
|
31
38
|
pluginActions,
|
|
32
39
|
openEntityMode,
|
|
33
40
|
navigateBack,
|
|
34
|
-
formContext
|
|
41
|
+
formContext,
|
|
42
|
+
showDefaultActions = true
|
|
35
43
|
}: EntityFormActionsProps) {
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
return buildBottomActions({
|
|
38
46
|
path,
|
|
39
47
|
savingError,
|
|
40
48
|
entity,
|
|
@@ -46,35 +54,8 @@ export function EntityFormActions({
|
|
|
46
54
|
navigateBack,
|
|
47
55
|
formContext,
|
|
48
56
|
formex,
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const sideActionsProps = {
|
|
53
|
-
path,
|
|
54
|
-
savingError,
|
|
55
|
-
entity,
|
|
56
|
-
collection,
|
|
57
|
-
disabled,
|
|
58
|
-
status,
|
|
59
|
-
pluginActions,
|
|
60
|
-
openEntityMode,
|
|
61
|
-
navigateBack,
|
|
62
|
-
formContext,
|
|
63
|
-
formex,
|
|
64
|
-
className: layout === "responsive" ? "hidden @6xl:flex" : undefined
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const bottomActions = buildBottomActions(bottomActionsProps);
|
|
68
|
-
const sideActions = buildSideActions(sideActionsProps);
|
|
69
|
-
|
|
70
|
-
if (layout === "responsive") {
|
|
71
|
-
return <>
|
|
72
|
-
{bottomActions}
|
|
73
|
-
{sideActions}
|
|
74
|
-
</>;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return layout === "bottom" ? bottomActions : sideActions;
|
|
57
|
+
showDefaultActions
|
|
58
|
+
});
|
|
78
59
|
}
|
|
79
60
|
|
|
80
61
|
type ActionsViewProps<M extends Record<string, unknown>> = {
|
|
@@ -90,6 +71,7 @@ type ActionsViewProps<M extends Record<string, unknown>> = {
|
|
|
90
71
|
navigateBack: () => void;
|
|
91
72
|
formContext: FormContext,
|
|
92
73
|
formex: FormexController<Record<string, unknown>>;
|
|
74
|
+
showDefaultActions: boolean;
|
|
93
75
|
className?: string;
|
|
94
76
|
};
|
|
95
77
|
|
|
@@ -106,19 +88,30 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
106
88
|
navigateBack,
|
|
107
89
|
formContext,
|
|
108
90
|
formex,
|
|
91
|
+
showDefaultActions,
|
|
109
92
|
className
|
|
110
93
|
}: ActionsViewProps<M>) {
|
|
111
94
|
|
|
112
95
|
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
96
|
+
const entityActions = entity ? (formActions ?? []) : [];
|
|
97
|
+
const hasPluginActions = Boolean(pluginActions && (pluginActions as React.ReactNode[]).length > 0);
|
|
98
|
+
|
|
99
|
+
// Nothing to show means no footer at all, rather than an empty bar pinned
|
|
100
|
+
// over the last field. This is also what made `showDefaultActions` start
|
|
101
|
+
// meaning something: it was accepted as a prop and then never read, so
|
|
102
|
+
// passing `false` silently kept the buttons.
|
|
103
|
+
if (!showDefaultActions && !savingError && !entityActions.length && !hasPluginActions) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
113
106
|
|
|
114
|
-
return <DialogActions position={"
|
|
107
|
+
return <DialogActions position={"sticky"} className={className}>
|
|
115
108
|
{savingError &&
|
|
116
109
|
<div className="text-right">
|
|
117
110
|
<Typography color={"error"}>{savingError.message}</Typography>
|
|
118
111
|
</div>
|
|
119
112
|
}
|
|
120
|
-
{
|
|
121
|
-
{
|
|
113
|
+
{entityActions.length > 0 && <div className="grow flex overflow-auto no-scrollbar">
|
|
114
|
+
{entityActions.map(action => (
|
|
122
115
|
<IconButton
|
|
123
116
|
key={action.name}
|
|
124
117
|
color="primary"
|
|
@@ -142,68 +135,26 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
142
135
|
))}
|
|
143
136
|
</div>}
|
|
144
137
|
{pluginActions}
|
|
145
|
-
<Button variant="text" disabled={disabled || formex.isSubmitting}
|
|
146
|
-
color={"primary"}
|
|
147
|
-
type="reset">
|
|
148
|
-
{status === "existing" ? "Discard" : "Clear"}
|
|
149
|
-
</Button>
|
|
150
|
-
<Tooltip title={hasErrors ? "Fix highlighted errors before saving" : undefined}>
|
|
151
|
-
<LoadingButton variant={"filled"}
|
|
152
|
-
color="primary"
|
|
153
|
-
type="submit"
|
|
154
|
-
loading={formex.isSubmitting}
|
|
155
|
-
disabled={disabled || formex.isSubmitting}
|
|
156
|
-
startIcon={hasErrors ? <AlertCircleIcon/> : undefined}>
|
|
157
|
-
{status === "existing" && "Save"}
|
|
158
|
-
{status === "copy" && "Create copy"}
|
|
159
|
-
{status === "new" && "Create"}
|
|
160
|
-
</LoadingButton>
|
|
161
|
-
</Tooltip>
|
|
162
138
|
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return <div
|
|
183
|
-
className={cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin, className)}>
|
|
184
|
-
<Tooltip title={hasErrors ? "Fix highlighted errors before saving" : undefined}>
|
|
185
|
-
<LoadingButton fullWidth={true}
|
|
186
|
-
variant="filled"
|
|
187
|
-
color="primary"
|
|
188
|
-
type="submit"
|
|
189
|
-
loading={formex.isSubmitting}
|
|
190
|
-
startIcon={hasErrors ? <AlertCircleIcon/> : undefined}
|
|
191
|
-
disabled={disabled || formex.isSubmitting}>
|
|
192
|
-
{status === "existing" && "Save"}
|
|
193
|
-
{status === "copy" && "Create copy"}
|
|
194
|
-
{status === "new" && "Create"}
|
|
195
|
-
</LoadingButton>
|
|
196
|
-
</Tooltip>
|
|
197
|
-
<Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
|
|
198
|
-
{status === "existing" ? "Discard" : "Clear"}
|
|
199
|
-
</Button>
|
|
200
|
-
|
|
201
|
-
{pluginActions}
|
|
139
|
+
{showDefaultActions && <>
|
|
140
|
+
<Button variant="text" disabled={disabled || formex.isSubmitting}
|
|
141
|
+
color={"primary"}
|
|
142
|
+
type="reset">
|
|
143
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
144
|
+
</Button>
|
|
145
|
+
<Tooltip title={hasErrors ? "Fix highlighted errors before saving" : undefined}>
|
|
146
|
+
<LoadingButton variant={"filled"}
|
|
147
|
+
color="primary"
|
|
148
|
+
type="submit"
|
|
149
|
+
loading={formex.isSubmitting}
|
|
150
|
+
disabled={disabled || formex.isSubmitting}
|
|
151
|
+
startIcon={hasErrors ? <AlertCircleIcon/> : undefined}>
|
|
152
|
+
{status === "existing" && "Save"}
|
|
153
|
+
{status === "copy" && "Create copy"}
|
|
154
|
+
{status === "new" && "Create"}
|
|
155
|
+
</LoadingButton>
|
|
156
|
+
</Tooltip>
|
|
157
|
+
</>}
|
|
202
158
|
|
|
203
|
-
|
|
204
|
-
<div className="text-right">
|
|
205
|
-
<Typography color={"error"}>{savingError.message}</Typography>
|
|
206
|
-
</div>
|
|
207
|
-
}
|
|
208
|
-
</div>;
|
|
159
|
+
</DialogActions>;
|
|
209
160
|
}
|
|
@@ -287,7 +287,9 @@ export function EntityFormBinding<M extends Record<string, unknown>>({
|
|
|
287
287
|
disabled: false
|
|
288
288
|
}), [entityId, parentCollectionSlugs, parentEntityIds, path, status, collection, context, openEntityMode]);
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
// `form.actions` is rendered by the entity view's identity bar, which has
|
|
291
|
+
// the real formContext and does not cost a footer. Rendering it here too
|
|
292
|
+
// would show Autofill twice.
|
|
291
293
|
const pluginFormBefore = useSlot("form.before", formActionProps);
|
|
292
294
|
const pluginFormAfter = useSlot("form.after", formActionProps);
|
|
293
295
|
|
|
@@ -314,7 +316,7 @@ export function EntityFormBinding<M extends Record<string, unknown>>({
|
|
|
314
316
|
// Slots
|
|
315
317
|
beforeFields={pluginFormBefore}
|
|
316
318
|
afterFields={pluginFormAfter}
|
|
317
|
-
pluginActions={
|
|
319
|
+
pluginActions={[]}
|
|
318
320
|
// Local changes
|
|
319
321
|
computedInitialValues={computedInitialValues as Partial<M>}
|
|
320
322
|
hasLocalChanges={hasLocalChanges}
|
|
@@ -78,6 +78,7 @@ function PropertyFieldBindingInternal<M extends Record<string, unknown> = Record
|
|
|
78
78
|
property,
|
|
79
79
|
context,
|
|
80
80
|
includeDescription,
|
|
81
|
+
hideLabel,
|
|
81
82
|
underlyingValueHasChanged,
|
|
82
83
|
disabled: disabledProp,
|
|
83
84
|
partOfArray,
|
|
@@ -154,6 +155,7 @@ function PropertyFieldBindingInternal<M extends Record<string, unknown> = Record
|
|
|
154
155
|
propertyKey,
|
|
155
156
|
property: resolvedProperty,
|
|
156
157
|
includeDescription,
|
|
158
|
+
hideLabel,
|
|
157
159
|
underlyingValueHasChanged,
|
|
158
160
|
context,
|
|
159
161
|
disabled,
|
|
@@ -188,6 +190,7 @@ function FieldInternal<CustomProps, M extends Record<string, unknown>>
|
|
|
188
190
|
propertyKey,
|
|
189
191
|
property,
|
|
190
192
|
includeDescription,
|
|
193
|
+
hideLabel,
|
|
191
194
|
underlyingValueHasChanged,
|
|
192
195
|
partOfArray,
|
|
193
196
|
partOfBlock,
|
|
@@ -249,6 +252,7 @@ function FieldInternal<CustomProps, M extends Record<string, unknown>>
|
|
|
249
252
|
showError,
|
|
250
253
|
isSubmitting,
|
|
251
254
|
includeDescription: includeDescription ?? true,
|
|
255
|
+
hideLabel: hideLabel ?? false,
|
|
252
256
|
property: property as Property,
|
|
253
257
|
disabled: disabled ?? false,
|
|
254
258
|
underlyingValueHasChanged: underlyingValueHasChanged ?? false,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Property } from "@rebasepro/types";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { cls, ErrorBoundary, Typography } from "@rebasepro/ui";
|
|
4
|
+
import { getIconForProperty } from "../../util/property_utils";
|
|
5
|
+
import { PropertyIdCopyTooltip } from "../../components/PropertyIdCopyTooltip";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Does this property's editor supply its own heading?
|
|
9
|
+
*
|
|
10
|
+
* Arrays, maps, `oneOf` blocks and key-value editors render as a collapsible
|
|
11
|
+
* panel whose header *is* the label — hiding it would leave a panel you cannot
|
|
12
|
+
* identify or, worse, cannot expand. Those keep their own; everything else gets
|
|
13
|
+
* its label from {@link FieldBlock}, which is what makes a text field and a
|
|
14
|
+
* select finally agree on where a label goes.
|
|
15
|
+
*
|
|
16
|
+
* Exported because the layout resolver's span rules and this rule have to stay
|
|
17
|
+
* in step: the self-labelling types are exactly the ones that take a full row.
|
|
18
|
+
*/
|
|
19
|
+
export function isSelfLabellingProperty(property: Property | undefined): boolean {
|
|
20
|
+
if (!property) return false;
|
|
21
|
+
switch (property.type) {
|
|
22
|
+
case "map":
|
|
23
|
+
// `spreadChildren` inlines the children into the parent grid, so
|
|
24
|
+
// there is no panel and therefore no header to reuse as the label.
|
|
25
|
+
return !property.admin?.spreadChildren;
|
|
26
|
+
case "array": {
|
|
27
|
+
// Most arrays render as an expandable panel whose header is the
|
|
28
|
+
// label — `repeat`, `block`, `custom_array`, `multi_references`.
|
|
29
|
+
// The exceptions are the ones that resolve to an inline control:
|
|
30
|
+
// an enum array is a multi-select and a storage array is a
|
|
31
|
+
// dropzone, and both take their label from the form.
|
|
32
|
+
//
|
|
33
|
+
// This mirrors `getFieldConfig`'s array branch. Getting it backwards
|
|
34
|
+
// is visible: an array of plain strings is a `repeat` panel, and
|
|
35
|
+
// treating it as inline printed "Available Locales" twice, once
|
|
36
|
+
// from the form and once from the panel's own header.
|
|
37
|
+
if (property.oneOf) return true;
|
|
38
|
+
if (Array.isArray(property.of) || !property.of) return true;
|
|
39
|
+
const of = property.of;
|
|
40
|
+
if (of.type === "string" && (of.enum || of.storage)) return false;
|
|
41
|
+
if (of.type === "number" && of.enum) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FieldBlockProps {
|
|
50
|
+
propertyKey: string;
|
|
51
|
+
property?: Property;
|
|
52
|
+
/** Rendered by the form rather than by the field, unless self-labelling. */
|
|
53
|
+
showLabel: boolean;
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One field's worth of the form grid: label, control, description.
|
|
59
|
+
*
|
|
60
|
+
* The label carries a small type icon so a number reads differently from a
|
|
61
|
+
* relation at a glance, and the description sits under the control where it
|
|
62
|
+
* annotates what you just filled in.
|
|
63
|
+
*/
|
|
64
|
+
export function FieldBlock({
|
|
65
|
+
propertyKey,
|
|
66
|
+
property,
|
|
67
|
+
showLabel,
|
|
68
|
+
children
|
|
69
|
+
}: FieldBlockProps) {
|
|
70
|
+
|
|
71
|
+
const description = property?.description?.trim();
|
|
72
|
+
const required = Boolean(property?.validation?.required);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
// Top-aligned: with the description below the control, every label is a
|
|
76
|
+
// single line, so the controls line up on their own. (Bottom-aligning
|
|
77
|
+
// was only needed while the description sat between label and control.)
|
|
78
|
+
<div
|
|
79
|
+
id={`form_field_${propertyKey}`}
|
|
80
|
+
className={"relative flex flex-col min-w-0"}>
|
|
81
|
+
|
|
82
|
+
{showLabel && (
|
|
83
|
+
<PropertyIdCopyTooltip propertyKey={propertyKey}>
|
|
84
|
+
<div className={cls(
|
|
85
|
+
"flex items-center gap-1.5 text-sm font-medium leading-tight mb-1.5",
|
|
86
|
+
"text-text-secondary dark:text-text-secondary-dark"
|
|
87
|
+
)}>
|
|
88
|
+
{/* Small and quiet: enough to tell a number from a
|
|
89
|
+
relation at a glance, not enough to compete with the
|
|
90
|
+
field name. */}
|
|
91
|
+
{property && (
|
|
92
|
+
<span className={"shrink-0 text-text-disabled dark:text-text-disabled-dark"}>
|
|
93
|
+
{getIconForProperty(property, "smallest")}
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
<span className={"truncate"}>{property?.name ?? propertyKey}</span>
|
|
97
|
+
{required && <span className={"text-red-500 dark:text-red-500 -ml-1"}>*</span>}
|
|
98
|
+
</div>
|
|
99
|
+
</PropertyIdCopyTooltip>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<div className={"min-w-0"}>
|
|
103
|
+
<ErrorBoundary>
|
|
104
|
+
{children}
|
|
105
|
+
</ErrorBoundary>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Under the control, where it reads as a note about the thing you
|
|
109
|
+
just filled in rather than a subtitle of the next label. */}
|
|
110
|
+
{showLabel && description && (
|
|
111
|
+
<Typography variant={"caption"}
|
|
112
|
+
color={"disabled"}
|
|
113
|
+
className={"mt-1.5 ml-0.5 leading-snug"}>
|
|
114
|
+
{description}
|
|
115
|
+
</Typography>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Entity } from "@rebasepro/types";
|
|
2
|
+
import type { ResolvedFormField } from "@rebasepro/app";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { CheckIcon, cls, CopyIcon, defaultBorderMixin, IconButton, iconSize, Tooltip, Typography } from "@rebasepro/ui";
|
|
5
|
+
|
|
6
|
+
export interface FormRailProps {
|
|
7
|
+
fields: ResolvedFormField[];
|
|
8
|
+
showRecordMeta: boolean;
|
|
9
|
+
entity?: Entity<Record<string, unknown>>;
|
|
10
|
+
renderField: (field: ResolvedFormField) => React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The metadata column beside the form.
|
|
15
|
+
*
|
|
16
|
+
* This replaces a `w-80 2xl:w-96` rail that held a Save button and several
|
|
17
|
+
* hundred pixels of nothing. It renders only when it has something to say, and
|
|
18
|
+
* the actions it used to hold now live in the identity bar, where they stay
|
|
19
|
+
* visible while you scroll.
|
|
20
|
+
*/
|
|
21
|
+
export function FormRail({
|
|
22
|
+
fields,
|
|
23
|
+
showRecordMeta,
|
|
24
|
+
entity,
|
|
25
|
+
renderField
|
|
26
|
+
}: FormRailProps) {
|
|
27
|
+
|
|
28
|
+
if (!fields.length && !showRecordMeta) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<aside className={cls(
|
|
32
|
+
// No responsive hiding here. The caller decides whether the rail is
|
|
33
|
+
// shown, by measuring; a CSS `hidden` would leave the fields the
|
|
34
|
+
// resolver moved in here unreachable rather than folding them back.
|
|
35
|
+
"flex flex-col gap-6 shrink-0 w-72 border-l overflow-y-auto",
|
|
36
|
+
"px-5 py-6 bg-surface-50 dark:bg-surface-900",
|
|
37
|
+
defaultBorderMixin
|
|
38
|
+
)}>
|
|
39
|
+
{fields.length > 0 && (
|
|
40
|
+
<div className={"flex flex-col gap-5"}>
|
|
41
|
+
{fields.map(renderField)}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{showRecordMeta && entity && <RecordMeta entity={entity}/>}
|
|
46
|
+
</aside>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function RecordMeta({ entity }: { entity: Entity<Record<string, unknown>> }) {
|
|
51
|
+
|
|
52
|
+
const createdAt = pickDate(entity.values, ["created_at", "createdAt", "created_on"]);
|
|
53
|
+
const updatedAt = pickDate(entity.values, ["updated_at", "updatedAt", "modified_at"]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={"flex flex-col gap-2.5"}>
|
|
57
|
+
<Typography variant={"caption"}
|
|
58
|
+
className={"text-xs font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark"}>
|
|
59
|
+
Record
|
|
60
|
+
</Typography>
|
|
61
|
+
|
|
62
|
+
<MetaRow label={"ID"} value={String(entity.id)} copyable/>
|
|
63
|
+
{createdAt && <MetaRow label={"Created"} value={createdAt}/>}
|
|
64
|
+
{updatedAt && <MetaRow label={"Updated"} value={updatedAt}/>}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function MetaRow({
|
|
70
|
+
label,
|
|
71
|
+
value,
|
|
72
|
+
copyable
|
|
73
|
+
}: {
|
|
74
|
+
label: string;
|
|
75
|
+
value: string;
|
|
76
|
+
copyable?: boolean;
|
|
77
|
+
}) {
|
|
78
|
+
const [copied, setCopied] = React.useState(false);
|
|
79
|
+
|
|
80
|
+
const copy = () => {
|
|
81
|
+
navigator.clipboard?.writeText(value).then(() => {
|
|
82
|
+
setCopied(true);
|
|
83
|
+
setTimeout(() => setCopied(false), 1600);
|
|
84
|
+
}, () => undefined);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className={"flex items-baseline justify-between gap-3 min-w-0"}>
|
|
89
|
+
<span className={"text-xs text-text-disabled dark:text-text-disabled-dark shrink-0"}>
|
|
90
|
+
{label}
|
|
91
|
+
</span>
|
|
92
|
+
<span className={"flex items-center gap-1 min-w-0"}>
|
|
93
|
+
<span className={"font-mono text-xs text-text-secondary dark:text-text-secondary-dark truncate"}
|
|
94
|
+
title={value}>
|
|
95
|
+
{value}
|
|
96
|
+
</span>
|
|
97
|
+
{copyable && (
|
|
98
|
+
<Tooltip title={copied ? "Copied" : "Copy"}>
|
|
99
|
+
<IconButton size={"smallest"} onClick={copy}>
|
|
100
|
+
{copied ? <CheckIcon size={iconSize.smallest}/> : <CopyIcon size={iconSize.smallest}/>}
|
|
101
|
+
</IconButton>
|
|
102
|
+
</Tooltip>
|
|
103
|
+
)}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Audit timestamps are a convention, not a contract — a collection may name them
|
|
111
|
+
* anything or not have them at all — so this reads the values it recognises and
|
|
112
|
+
* stays quiet otherwise rather than rendering an empty row.
|
|
113
|
+
*/
|
|
114
|
+
function pickDate(values: Record<string, unknown> | undefined, keys: string[]): string | undefined {
|
|
115
|
+
if (!values) return undefined;
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
const value = values[key];
|
|
118
|
+
if (value instanceof Date) return value.toLocaleDateString();
|
|
119
|
+
if (typeof value === "string" && value) {
|
|
120
|
+
const parsed = new Date(value);
|
|
121
|
+
if (!Number.isNaN(parsed.getTime())) return parsed.toLocaleDateString();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ResolvedFormField, ResolvedFormSection } from "@rebasepro/app";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { ChevronDownIcon, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
4
|
+
|
|
5
|
+
export interface FormSectionsProps {
|
|
6
|
+
sections: ResolvedFormSection[];
|
|
7
|
+
/** Section keys the user has collapsed. */
|
|
8
|
+
collapsed: ReadonlySet<string>;
|
|
9
|
+
onToggle: (key: string) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Field keys currently showing a validation error. A section holding one is
|
|
12
|
+
* forced open — an error that hides inside a collapsed group is a form the
|
|
13
|
+
* user cannot submit and cannot debug.
|
|
14
|
+
*/
|
|
15
|
+
errorKeys: ReadonlySet<string>;
|
|
16
|
+
renderField: (field: ResolvedFormField) => React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function FormSections({
|
|
20
|
+
sections,
|
|
21
|
+
collapsed,
|
|
22
|
+
onToggle,
|
|
23
|
+
errorKeys,
|
|
24
|
+
renderField
|
|
25
|
+
}: FormSectionsProps) {
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={"flex flex-col gap-8"}>
|
|
29
|
+
{sections.map((section) => {
|
|
30
|
+
|
|
31
|
+
const hasError = section.fields.some(f => errorKeys.has(f.key));
|
|
32
|
+
const isCollapsed = section.collapsible && collapsed.has(section.key) && !hasError;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section key={section.key} className={"min-w-0"}>
|
|
36
|
+
|
|
37
|
+
{section.title && (
|
|
38
|
+
<Header
|
|
39
|
+
title={section.title}
|
|
40
|
+
collapsible={section.collapsible}
|
|
41
|
+
collapsed={isCollapsed}
|
|
42
|
+
controls={`form_section_${section.key}`}
|
|
43
|
+
onToggle={() => onToggle(section.key)}/>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
{!isCollapsed && (
|
|
47
|
+
// A container query, not a media query: the form is
|
|
48
|
+
// rendered at four very different widths (full
|
|
49
|
+
// screen, side panel, split pane, dialog) inside the
|
|
50
|
+
// same viewport, so spans have to answer to the
|
|
51
|
+
// column they are in, not to the window.
|
|
52
|
+
<div id={`form_section_${section.key}`}
|
|
53
|
+
className={cls(
|
|
54
|
+
"grid gap-x-4 gap-y-5 min-w-0",
|
|
55
|
+
"grid-cols-1 @2xl:grid-cols-4"
|
|
56
|
+
)}>
|
|
57
|
+
{section.fields.map(renderField)}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</section>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Header({
|
|
68
|
+
title,
|
|
69
|
+
collapsible,
|
|
70
|
+
collapsed,
|
|
71
|
+
controls,
|
|
72
|
+
onToggle
|
|
73
|
+
}: {
|
|
74
|
+
title: string;
|
|
75
|
+
collapsible: boolean;
|
|
76
|
+
collapsed: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Id of the region this header shows and hides. Only referenced while that
|
|
79
|
+
* region exists — a collapsed section unmounts its fields, and pointing
|
|
80
|
+
* `aria-controls` at an id that is not in the document is its own defect.
|
|
81
|
+
*/
|
|
82
|
+
controls: string;
|
|
83
|
+
onToggle: () => void;
|
|
84
|
+
}) {
|
|
85
|
+
|
|
86
|
+
const content = (
|
|
87
|
+
<>
|
|
88
|
+
{collapsible && (
|
|
89
|
+
<ChevronDownIcon
|
|
90
|
+
size={iconSize.smallest}
|
|
91
|
+
className={cls(
|
|
92
|
+
"text-text-disabled dark:text-text-disabled-dark transition-transform duration-150",
|
|
93
|
+
collapsed && "-rotate-90"
|
|
94
|
+
)}/>
|
|
95
|
+
)}
|
|
96
|
+
<span className={"text-xs font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark whitespace-nowrap"}>
|
|
97
|
+
{title}
|
|
98
|
+
</span>
|
|
99
|
+
<span className={cls("flex-1 border-t", defaultBorderMixin)}/>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const className = "flex items-center gap-2.5 w-full mb-3.5";
|
|
104
|
+
|
|
105
|
+
if (!collapsible) {
|
|
106
|
+
return <div className={className}>{content}</div>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<button type={"button"}
|
|
111
|
+
onClick={onToggle}
|
|
112
|
+
aria-expanded={!collapsed}
|
|
113
|
+
aria-controls={collapsed ? undefined : controls}
|
|
114
|
+
className={cls(className, "text-left hover:opacity-80 transition-opacity")}>
|
|
115
|
+
{content}
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./FieldHelperText";
|
|
2
2
|
export * from "./LabelWithIcon";
|
|
3
3
|
export * from "./LabelWithIconAndTooltip";
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
4
|
+
export * from "./FieldBlock";
|
|
5
|
+
export * from "./FormRail";
|
|
6
|
+
export * from "./FormSections";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useLayoutEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Below this the form is a single column and the rail is not shown.
|
|
5
|
+
*
|
|
6
|
+
* Roughly the 288px rail plus a content column still wide enough to hold a
|
|
7
|
+
* four-up row. Above it the rail is worth having even in the split view; below
|
|
8
|
+
* it — a narrow split pane, the side panel, the dialog, a phone — its fields
|
|
9
|
+
* fold back into the column.
|
|
10
|
+
*/
|
|
11
|
+
export const RAIL_MIN_WIDTH = 1000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Is there room beside the form for the metadata rail?
|
|
15
|
+
*
|
|
16
|
+
* Measured on the form element rather than the viewport, because the same form
|
|
17
|
+
* renders in a side panel and a dialog inside a wide window — a media query
|
|
18
|
+
* would say "desktop" and hand a 600px panel a rail it cannot fit.
|
|
19
|
+
*
|
|
20
|
+
* This has to be a real measurement rather than a CSS `hidden` class: the
|
|
21
|
+
* layout resolver *moves* fields into the rail, so hiding it with CSS did not
|
|
22
|
+
* fold them back into the column, it made them unreachable. Status, category
|
|
23
|
+
* and any other rail field simply could not be edited in the side panel, the
|
|
24
|
+
* dialog, or on a phone.
|
|
25
|
+
*/
|
|
26
|
+
export function useRailVisible(ref: React.RefObject<HTMLElement | null>): boolean {
|
|
27
|
+
|
|
28
|
+
const [visible, setVisible] = useState(false);
|
|
29
|
+
|
|
30
|
+
// Layout effect, not effect: measured before paint, so a wide form does
|
|
31
|
+
// not render single-column for a frame and then snap the rail in.
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
const element = ref.current;
|
|
34
|
+
if (!element) return;
|
|
35
|
+
|
|
36
|
+
const measure = () => setVisible(element.getBoundingClientRect().width >= RAIL_MIN_WIDTH);
|
|
37
|
+
measure();
|
|
38
|
+
|
|
39
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
40
|
+
const observer = new ResizeObserver(measure);
|
|
41
|
+
observer.observe(element);
|
|
42
|
+
return () => observer.disconnect();
|
|
43
|
+
}, [ref]);
|
|
44
|
+
|
|
45
|
+
return visible;
|
|
46
|
+
}
|