@rebasepro/admin 0.2.5 → 0.4.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-Cn8-tGyL.js → CollectionEditorDialog-Bo86h-d-.js} +3 -3
- package/dist/{CollectionEditorDialog-Cn8-tGyL.js.map → CollectionEditorDialog-Bo86h-d-.js.map} +1 -1
- package/dist/{CollectionsStudioView-C-Ts1rZt.js → CollectionsStudioView-bsg4AT_Z.js} +4 -4
- package/dist/{CollectionsStudioView-C-Ts1rZt.js.map → CollectionsStudioView-bsg4AT_Z.js.map} +1 -1
- package/dist/{ExportCollectionAction-BRdKM3DF.js → ExportCollectionAction-Cf-Id5uO.js} +35 -27
- package/dist/ExportCollectionAction-Cf-Id5uO.js.map +1 -0
- package/dist/{ImportCollectionAction-U-v7lGxO.js → ImportCollectionAction-LN5sreK9.js} +2 -2
- package/dist/{ImportCollectionAction-U-v7lGxO.js.map → ImportCollectionAction-LN5sreK9.js.map} +1 -1
- package/dist/{PropertyEditView-BDNYkfNf.js → PropertyEditView-jCWCxmCz.js} +2 -2
- package/dist/{PropertyEditView-BDNYkfNf.js.map → PropertyEditView-jCWCxmCz.js.map} +1 -1
- package/dist/collection_editor_ui.js +3 -3
- package/dist/components/CollectionPanel.d.ts +83 -0
- package/dist/components/EntityDetailView.d.ts +0 -3
- package/dist/components/EntityEditView.d.ts +2 -3
- package/dist/components/SelectableTable/SelectableTable.d.ts +2 -2
- package/dist/components/SelectableTable/SelectableTableContext.d.ts +1 -1
- package/dist/components/SelectableTable/SelectionStore.d.ts +3 -3
- package/dist/components/index.d.ts +1 -0
- package/dist/editor.js +15 -5
- package/dist/editor.js.map +1 -1
- package/dist/{index-DJSL_SCr.js → index-BrqSvMG0.js} +3 -3
- package/dist/index-BrqSvMG0.js.map +1 -0
- package/dist/{index-DHaOV-7A.js → index-Cb0bPnzZ.js} +3 -3
- package/dist/index-Cb0bPnzZ.js.map +1 -0
- package/dist/{index-XMII4H3d.js → index-z3H8iqsj.js} +2 -2
- package/dist/{index-XMII4H3d.js.map → index-z3H8iqsj.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +142 -4
- package/dist/index.js.map +1 -1
- package/dist/util/entity_view_constants.d.ts +6 -0
- package/dist/util/resolutions.d.ts +1 -2
- package/dist/{util-0GYaJqL_.js → util-CvNp6A-r.js} +283 -168
- package/dist/util-CvNp6A-r.js.map +1 -0
- package/package.json +8 -8
- package/src/collection_editor/validateCollectionJson.ts +12 -12
- package/src/components/CollectionPanel.tsx +155 -0
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +65 -35
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +1 -1
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +1 -1
- package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
- package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +69 -18
- package/src/components/EntityDetailView.tsx +36 -37
- package/src/components/EntityEditView.tsx +108 -51
- package/src/components/EntityEditViewFormActions.tsx +4 -4
- package/src/components/EntitySidePanel.tsx +47 -14
- package/src/components/PropertyCollectionView.tsx +1 -0
- package/src/components/SelectableTable/SelectableTable.tsx +24 -22
- package/src/components/SelectableTable/SelectableTableContext.tsx +2 -1
- package/src/components/SelectableTable/SelectionStore.ts +4 -4
- package/src/components/index.ts +2 -0
- package/src/data_export/export/BasicExportAction.tsx +11 -9
- package/src/data_export/export/ExportCollectionAction.tsx +15 -13
- package/src/editor/editor.tsx +20 -4
- package/src/form/EntityForm.tsx +3 -3
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
- package/src/hooks/navigation/useResolvedCollections.ts +2 -10
- package/src/hooks/useBuildSideEntityController.tsx +20 -3
- package/src/index.ts +4 -2
- package/src/util/entity_view_constants.ts +6 -0
- package/src/util/resolutions.ts +2 -6
- package/dist/ExportCollectionAction-BRdKM3DF.js.map +0 -1
- package/dist/index-DHaOV-7A.js.map +0 -1
- package/dist/index-DJSL_SCr.js.map +0 -1
- package/dist/util-0GYaJqL_.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentRef, EntityCollection, EntityCustomViewParams,
|
|
1
|
+
import type { ComponentRef, EntityCollection, EntityCustomViewParams, FormViewConfig } from "@rebasepro/types";
|
|
2
2
|
import type { FormContext } from "../types/fields";
|
|
3
3
|
import type { PluginFormActionProps } from "@rebasepro/types";
|
|
4
4
|
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -47,9 +47,7 @@ import { EntityJsonPreview } from "../components/EntityJsonPreview";
|
|
|
47
47
|
|
|
48
48
|
const EntityHistoryView = lazy(() => import("../components/history").then(m => ({ default: m.EntityHistoryView })));
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
export const JSON_TAB_VALUE = "__json";
|
|
52
|
-
export const HISTORY_TAB_VALUE = "__rebase_history";
|
|
50
|
+
import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/entity_view_constants";
|
|
53
51
|
|
|
54
52
|
export type BarActionsParams = {
|
|
55
53
|
values: object,
|
|
@@ -154,13 +152,27 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
|
|
|
154
152
|
{ status: "existing", entityId }
|
|
155
153
|
), [collection, entityId]);
|
|
156
154
|
|
|
157
|
-
|
|
155
|
+
// Track whether we've applied the defaultSelectedView already.
|
|
156
|
+
// See EntityEditView for a full explanation of this pattern.
|
|
157
|
+
const hasAppliedDefault = useRef(false);
|
|
158
|
+
|
|
159
|
+
const [selectedTab, setSelectedTab] = useState<string>(() => {
|
|
160
|
+
const val = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
|
|
161
|
+
hasAppliedDefault.current = true;
|
|
162
|
+
return val === "edit" ? MAIN_TAB_VALUE : val;
|
|
163
|
+
});
|
|
158
164
|
useEffect(() => {
|
|
159
|
-
const
|
|
165
|
+
const val = hasAppliedDefault.current
|
|
166
|
+
? (selectedTabProp ?? MAIN_TAB_VALUE)
|
|
167
|
+
: (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
|
|
168
|
+
const target = val === "edit" ? MAIN_TAB_VALUE : val;
|
|
169
|
+
if (!hasAppliedDefault.current) {
|
|
170
|
+
hasAppliedDefault.current = true;
|
|
171
|
+
}
|
|
160
172
|
if (target !== selectedTab) {
|
|
161
173
|
setSelectedTab(target);
|
|
162
174
|
}
|
|
163
|
-
}, [selectedTabProp
|
|
175
|
+
}, [selectedTabProp]);
|
|
164
176
|
|
|
165
177
|
const subcollections = getSubcollections(collection).filter(c => !c.hideFromNavigation);
|
|
166
178
|
const subcollectionsCount = subcollections?.length ?? 0;
|
|
@@ -223,8 +235,9 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
|
|
|
223
235
|
}), [entityId, parentCollectionSlugs, parentEntityIds, path, collection, context, readOnlyFormContext, layout]);
|
|
224
236
|
const pluginActionsTop = useSlot("form.actions.top", formActionTopProps);
|
|
225
237
|
|
|
226
|
-
//
|
|
227
|
-
const
|
|
238
|
+
// Resolve formView.Builder if provided
|
|
239
|
+
const formViewConfig = (collection as EntityCollection<M> & { formView?: FormViewConfig<M> }).formView;
|
|
240
|
+
const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
|
|
228
241
|
|
|
229
242
|
// Title resolution
|
|
230
243
|
const titlePropertyKey = getEntityTitlePropertyKey(collection, customizationController.propertyConfigs);
|
|
@@ -344,43 +357,29 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
|
|
|
344
357
|
}, [onTabChange, path, entityId, collection]);
|
|
345
358
|
|
|
346
359
|
const propertyDetailView = () => {
|
|
347
|
-
//
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
360
|
+
// formView.Builder replaces the default property display
|
|
361
|
+
if (FormViewBuilder && usedEntity) {
|
|
362
|
+
return <ErrorBoundary>
|
|
363
|
+
<Suspense fallback={<CircularProgressCenter />}>
|
|
364
|
+
<FormViewBuilder
|
|
365
|
+
collection={collection}
|
|
366
|
+
parentCollectionSlugs={parentCollectionSlugs}
|
|
367
|
+
parentEntityIds={parentEntityIds}
|
|
368
|
+
entity={usedEntity}
|
|
369
|
+
modifiedValues={usedEntity?.values}
|
|
370
|
+
formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
|
|
371
|
+
/>
|
|
372
|
+
</Suspense>
|
|
373
|
+
</ErrorBoundary>;
|
|
358
374
|
}
|
|
359
375
|
|
|
360
|
-
const HeaderComponent = detailViewConfig?.Header ? resolveComponentRef(detailViewConfig.Header) : null;
|
|
361
|
-
const FooterComponent = detailViewConfig?.Footer ? resolveComponentRef(detailViewConfig.Footer) : null;
|
|
362
|
-
|
|
363
376
|
return (
|
|
364
377
|
<>
|
|
365
|
-
{HeaderComponent && usedEntity && <HeaderComponent
|
|
366
|
-
collection={collection}
|
|
367
|
-
entity={usedEntity}
|
|
368
|
-
path={path}
|
|
369
|
-
onEditClick={onEditClick ?? (() => {})}
|
|
370
|
-
/>}
|
|
371
|
-
|
|
372
378
|
{usedEntity && <EntityView
|
|
373
379
|
entity={usedEntity}
|
|
374
380
|
collection={collection}
|
|
375
381
|
path={path}
|
|
376
382
|
/>}
|
|
377
|
-
|
|
378
|
-
{FooterComponent && usedEntity && <FooterComponent
|
|
379
|
-
collection={collection}
|
|
380
|
-
entity={usedEntity}
|
|
381
|
-
path={path}
|
|
382
|
-
onEditClick={onEditClick ?? (() => {})}
|
|
383
|
-
/>}
|
|
384
383
|
</>
|
|
385
384
|
);
|
|
386
385
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentRef, EntityCollection, EntityCustomViewParams } from "@rebasepro/types";
|
|
1
|
+
import type { ComponentRef, EntityCollection, EntityCustomViewParams, FormViewConfig } from "@rebasepro/types";
|
|
2
2
|
import type { FormContext } from "../types/fields";
|
|
3
3
|
import type { PluginFormActionProps } from "@rebasepro/types";
|
|
4
4
|
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -50,9 +50,8 @@ import { usePermissions } from "@rebasepro/core";
|
|
|
50
50
|
import { useUrlController } from "../index";
|
|
51
51
|
import { useNavigate } from "react-router-dom";
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
export
|
|
55
|
-
export const HISTORY_TAB_VALUE = "__rebase_history";
|
|
53
|
+
import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/entity_view_constants";
|
|
54
|
+
export { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE };
|
|
56
55
|
|
|
57
56
|
export type BarActionsParams = {
|
|
58
57
|
values: object,
|
|
@@ -225,15 +224,33 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
225
224
|
}
|
|
226
225
|
), [collection, status, entityId]);
|
|
227
226
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
// Track whether we've applied the defaultSelectedView already.
|
|
228
|
+
// On the initial render, selectedTabProp may be undefined because the URL
|
|
229
|
+
// has no tab segment — in that case we fall back to defaultSelectedView.
|
|
230
|
+
// But once the component is mounted, selectedTabProp=undefined means the
|
|
231
|
+
// user explicitly selected the form tab (which has no URL segment), so we
|
|
232
|
+
// must NOT re-apply defaultSelectedView.
|
|
233
|
+
const hasAppliedDefault = useRef(false);
|
|
234
|
+
|
|
235
|
+
const [selectedTab, setSelectedTab] = useState<string>(() => {
|
|
236
|
+
const val = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
|
|
237
|
+
hasAppliedDefault.current = true;
|
|
238
|
+
return val === "edit" ? MAIN_TAB_VALUE : val;
|
|
239
|
+
});
|
|
231
240
|
useEffect(() => {
|
|
232
|
-
|
|
241
|
+
// After initial mount, only sync from selectedTabProp — don't fall
|
|
242
|
+
// back to defaultSelectedView, because undefined now means "form tab".
|
|
243
|
+
const val = hasAppliedDefault.current
|
|
244
|
+
? (selectedTabProp ?? MAIN_TAB_VALUE)
|
|
245
|
+
: (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
|
|
246
|
+
const target = val === "edit" ? MAIN_TAB_VALUE : val;
|
|
247
|
+
if (!hasAppliedDefault.current) {
|
|
248
|
+
hasAppliedDefault.current = true;
|
|
249
|
+
}
|
|
233
250
|
if (target !== selectedTab) {
|
|
234
251
|
setSelectedTab(target);
|
|
235
252
|
}
|
|
236
|
-
}, [selectedTabProp
|
|
253
|
+
}, [selectedTabProp]);
|
|
237
254
|
|
|
238
255
|
const subcollections = getSubcollections(collection).filter(c => !c.hideFromNavigation);
|
|
239
256
|
const subcollectionsCount = subcollections?.length ?? 0;
|
|
@@ -268,6 +285,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
268
285
|
return {
|
|
269
286
|
entityId,
|
|
270
287
|
disabled: false,
|
|
288
|
+
readOnly: true,
|
|
271
289
|
openEntityMode: layout,
|
|
272
290
|
status: status,
|
|
273
291
|
values: usedEntity?.values ?? ({} as M),
|
|
@@ -413,6 +431,11 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
413
431
|
}
|
|
414
432
|
}, [status, onTabChange, path, entityId, collection]);
|
|
415
433
|
|
|
434
|
+
// Resolve formView.Builder if provided
|
|
435
|
+
const formViewConfig = (collection as EntityCollection<M> & { formView?: FormViewConfig<M> }).formView;
|
|
436
|
+
const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
|
|
437
|
+
const formViewIncludeActions = formViewConfig?.includeActions !== false;
|
|
438
|
+
|
|
416
439
|
const entityReadOnlyView = !canEdit && entity ? <div
|
|
417
440
|
className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", (canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : "")}>
|
|
418
441
|
<div
|
|
@@ -420,52 +443,86 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
420
443
|
<Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
|
|
421
444
|
{collection.singularName ?? collection.name}
|
|
422
445
|
</Typography>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
446
|
+
{FormViewBuilder && readOnlyFormContext ? (
|
|
447
|
+
<ErrorBoundary>
|
|
448
|
+
<Suspense fallback={<CircularProgressCenter />}>
|
|
449
|
+
<FormViewBuilder
|
|
450
|
+
collection={collection}
|
|
451
|
+
parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
|
|
452
|
+
entity={usedEntity}
|
|
453
|
+
modifiedValues={usedEntity?.values}
|
|
454
|
+
formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
|
|
455
|
+
/>
|
|
456
|
+
</Suspense>
|
|
457
|
+
</ErrorBoundary>
|
|
458
|
+
) : (
|
|
459
|
+
<EntityView
|
|
460
|
+
className={"px-8 h-full overflow-auto"}
|
|
461
|
+
entity={entity}
|
|
462
|
+
path={path}
|
|
463
|
+
collection={collection} />
|
|
464
|
+
)}
|
|
428
465
|
<div className="h-16" />
|
|
429
466
|
</div>
|
|
430
467
|
</div> : null;
|
|
431
468
|
|
|
432
|
-
const entityView =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
formProps
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
+
const entityView = FormViewBuilder ? (
|
|
470
|
+
// formView.Builder replaces the default form
|
|
471
|
+
<div className={cls(
|
|
472
|
+
"relative flex-1 w-full h-full overflow-auto",
|
|
473
|
+
(!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : ""
|
|
474
|
+
)}>
|
|
475
|
+
<ErrorBoundary>
|
|
476
|
+
<Suspense fallback={<CircularProgressCenter />}>
|
|
477
|
+
{(formContext ?? readOnlyFormContext) && <FormViewBuilder
|
|
478
|
+
collection={collection}
|
|
479
|
+
parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
|
|
480
|
+
entity={usedEntity}
|
|
481
|
+
modifiedValues={(formContext ?? readOnlyFormContext)?.formex?.values ?? usedEntity?.values}
|
|
482
|
+
formContext={(formContext ?? readOnlyFormContext) as FormContext<Record<string, unknown>>}
|
|
483
|
+
/>}
|
|
484
|
+
</Suspense>
|
|
485
|
+
</ErrorBoundary>
|
|
486
|
+
</div>
|
|
487
|
+
) : (
|
|
488
|
+
<EntityForm<M>
|
|
489
|
+
collection={collection}
|
|
490
|
+
path={path}
|
|
491
|
+
entityId={entityId ?? usedEntity?.id}
|
|
492
|
+
onValuesModified={onValuesModified}
|
|
493
|
+
entity={entity}
|
|
494
|
+
initialDirtyValues={initialDirtyValues}
|
|
495
|
+
openEntityMode={layout}
|
|
496
|
+
forceActionsAtTheBottom={actionsAtTheBottom}
|
|
497
|
+
initialStatus={status}
|
|
498
|
+
className={cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className)}
|
|
499
|
+
EntityFormActionsComponent={EntityEditViewFormActions}
|
|
500
|
+
disabled={!canEdit}
|
|
501
|
+
navigateBack={navigateBack}
|
|
502
|
+
{...formProps}
|
|
503
|
+
onEntityChange={(entity) => {
|
|
504
|
+
setUsedEntity(entity);
|
|
505
|
+
formProps?.onEntityChange?.(entity);
|
|
506
|
+
}}
|
|
507
|
+
onStatusChange={(status) => {
|
|
508
|
+
setStatus(status);
|
|
509
|
+
formProps?.onStatusChange?.(status);
|
|
510
|
+
}}
|
|
511
|
+
onFormContextReady={(formContext) => {
|
|
512
|
+
setFormContext(formContext);
|
|
513
|
+
formProps?.onFormContextReady?.(formContext);
|
|
514
|
+
}}
|
|
515
|
+
onSaved={(params) => {
|
|
516
|
+
const res = {
|
|
517
|
+
...params,
|
|
518
|
+
selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab
|
|
519
|
+
};
|
|
520
|
+
onSaved?.(res);
|
|
521
|
+
formProps?.onSaved?.(res);
|
|
522
|
+
}}
|
|
523
|
+
Builder={resolveComponentRef(selectedSecondaryForm?.Builder as ComponentRef<EntityCustomViewParams<M>> | undefined) as React.ComponentType<EntityCustomViewParams<M>> | undefined}
|
|
524
|
+
/>
|
|
525
|
+
);
|
|
469
526
|
|
|
470
527
|
const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
|
|
471
528
|
const icon = getIcon(subcollection.icon, undefined, undefined, "small");
|
|
@@ -162,7 +162,7 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
{formActions.length > 0 && <div className="grow flex overflow-auto no-scrollbar">
|
|
165
|
-
{formActions.map(action => {
|
|
165
|
+
{formActions.map((action, index) => {
|
|
166
166
|
|
|
167
167
|
const props = {
|
|
168
168
|
view: "form",
|
|
@@ -179,7 +179,7 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
179
179
|
const isEnabled = !action.isEnabled || action.isEnabled(props);
|
|
180
180
|
return (
|
|
181
181
|
<EntityActionButton
|
|
182
|
-
key={action.key}
|
|
182
|
+
key={action.key ?? action.name ?? index}
|
|
183
183
|
action={action}
|
|
184
184
|
enabled={isEnabled}
|
|
185
185
|
props={props}
|
|
@@ -282,7 +282,7 @@ function buildSideActions<M extends Record<string, unknown>>({
|
|
|
282
282
|
{pluginActions}
|
|
283
283
|
|
|
284
284
|
{formActions.length > 0 && <div className="flex flex-row flex-wrap mt-2">
|
|
285
|
-
{formActions.map(action => {
|
|
285
|
+
{formActions.map((action, index) => {
|
|
286
286
|
const props = {
|
|
287
287
|
view: "form",
|
|
288
288
|
entity,
|
|
@@ -296,7 +296,7 @@ function buildSideActions<M extends Record<string, unknown>>({
|
|
|
296
296
|
} satisfies EntityActionClickProps<any>;
|
|
297
297
|
const isEnabled = !action.isEnabled || action.isEnabled(props);
|
|
298
298
|
return (
|
|
299
|
-
<EntityActionButton key={action.key} action={action} enabled={isEnabled} props={props}/>
|
|
299
|
+
<EntityActionButton key={action.key ?? action.name ?? index} action={action} enabled={isEnabled} props={props}/>
|
|
300
300
|
);
|
|
301
301
|
})}
|
|
302
302
|
</div>}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityCollection } from "@rebasepro/types";
|
|
2
2
|
import type { EntitySidePanelProps } from "@rebasepro/types";
|
|
3
|
-
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import type { OnUpdateParams } from "../types/components/EntityFormProps";
|
|
6
6
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
@@ -8,11 +8,11 @@ import { IconButton, Maximize2Icon, XIcon } from "@rebasepro/ui";
|
|
|
8
8
|
import { EntityEditView } from "./EntityEditView";
|
|
9
9
|
import { EntityDetailView } from "./EntityDetailView";
|
|
10
10
|
import { useSideDialogContext } from "./SideDialogs";
|
|
11
|
-
import {
|
|
12
|
-
import { removeInitialAndTrailingSlashes } from "@rebasepro/common";
|
|
11
|
+
import { useNavigate } from "react-router-dom";
|
|
13
12
|
import { saveEntityToMemoryCache } from "@rebasepro/core";
|
|
14
13
|
import { useCollectionRegistryController, useSideEntityController } from "../index";
|
|
15
14
|
import { useUrlController } from "../index";
|
|
15
|
+
import { resolveDefaultSelectedView } from "@rebasepro/common";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* This is the component in charge of rendering the side dialog used
|
|
@@ -40,7 +40,6 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
40
40
|
} = useSideDialogContext();
|
|
41
41
|
|
|
42
42
|
const navigate = useNavigate();
|
|
43
|
-
const location = useLocation();
|
|
44
43
|
|
|
45
44
|
const sideEntityController = useSideEntityController();
|
|
46
45
|
const collectionRegistryController = useCollectionRegistryController();
|
|
@@ -96,6 +95,34 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
96
95
|
setShowEditInPanel(selectedTab === "edit");
|
|
97
96
|
}, [entityId, selectedTab]);
|
|
98
97
|
|
|
98
|
+
// One-time correction: when the side panel opens without the correct
|
|
99
|
+
// selectedTab but the resolved collection (from the registry) has a
|
|
100
|
+
// defaultSelectedView, update the panel width and URL to match.
|
|
101
|
+
// This handles cases where defaultSelectedView is set via the collection
|
|
102
|
+
// editor / Studio and is not available on the collection object the
|
|
103
|
+
// caller passed to sideEntityController.open().
|
|
104
|
+
const hasCorrectedDefaultView = useRef(false);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (hasCorrectedDefaultView.current) return;
|
|
107
|
+
if (selectedTab) return; // Already has a tab — no correction needed
|
|
108
|
+
if (!entityId || !collection?.defaultSelectedView) return;
|
|
109
|
+
|
|
110
|
+
const effectiveDefault = resolveDefaultSelectedView(
|
|
111
|
+
collection.defaultSelectedView,
|
|
112
|
+
{ status: "existing", entityId }
|
|
113
|
+
);
|
|
114
|
+
if (effectiveDefault && effectiveDefault !== "edit") {
|
|
115
|
+
hasCorrectedDefaultView.current = true;
|
|
116
|
+
sideEntityController.replace({
|
|
117
|
+
path,
|
|
118
|
+
entityId,
|
|
119
|
+
selectedTab: effectiveDefault,
|
|
120
|
+
updateUrl: collection.openEntityMode !== "dialog",
|
|
121
|
+
collection
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}, [selectedTab, entityId, collection, path, sideEntityController]);
|
|
125
|
+
|
|
99
126
|
// Note: beforeunload is handled by useUnsavedChangesDialog in SideDialogView,
|
|
100
127
|
// which listens to the same `blocked` state via SideDialogContext.
|
|
101
128
|
|
|
@@ -153,10 +180,13 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
153
180
|
return;
|
|
154
181
|
}
|
|
155
182
|
if (tabEntityId) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
sideEntityController.replace({
|
|
184
|
+
path,
|
|
185
|
+
entityId: tabEntityId,
|
|
186
|
+
selectedTab,
|
|
187
|
+
updateUrl: true,
|
|
188
|
+
collection: paramCollection ?? collection
|
|
189
|
+
});
|
|
160
190
|
}
|
|
161
191
|
}}
|
|
162
192
|
/>
|
|
@@ -198,18 +228,21 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
198
228
|
</IconButton>}
|
|
199
229
|
</div>}
|
|
200
230
|
onTabChange={({
|
|
201
|
-
entityId,
|
|
231
|
+
entityId: tabEntityId,
|
|
202
232
|
selectedTab,
|
|
203
233
|
collection: paramCollection
|
|
204
234
|
}) => {
|
|
205
235
|
if (collection?.openEntityMode === "dialog" || paramCollection?.openEntityMode === "dialog") {
|
|
206
236
|
return;
|
|
207
237
|
}
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
if (tabEntityId) {
|
|
239
|
+
sideEntityController.replace({
|
|
240
|
+
path,
|
|
241
|
+
entityId: tabEntityId,
|
|
242
|
+
selectedTab,
|
|
243
|
+
updateUrl: true,
|
|
244
|
+
collection: paramCollection ?? collection
|
|
245
|
+
});
|
|
213
246
|
}
|
|
214
247
|
}}
|
|
215
248
|
formProps={formProps}
|
|
@@ -258,6 +258,7 @@ export const PropertyCollectionView = ({
|
|
|
258
258
|
<div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
|
|
259
259
|
{Object.entries(data).map(([key, value]) => {
|
|
260
260
|
if (value === null || value === undefined) return null;
|
|
261
|
+
if (baseKey === "" && !properties[key]) return null;
|
|
261
262
|
const currentKey = baseKey ? `${baseKey}.${key}` : key;
|
|
262
263
|
return (
|
|
263
264
|
<PropertyCollectionView
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createSelectionStore } from "./SelectionStore";
|
|
2
2
|
import type { Property } from "@rebasepro/types";
|
|
3
3
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
|
-
import { CollectionSize, Entity, EntityTableController, FilterValues, SelectedCellProps } from "@rebasepro/types";
|
|
5
|
-
import { CellRendererParams, VirtualTable, VirtualTableColumn } from "@rebasepro/ui";
|
|
4
|
+
import { CollectionSize, Entity, EntityRelation, EntityTableController, FilterValues, SelectedCellProps } from "@rebasepro/types";
|
|
5
|
+
import { CellRendererParams, VirtualTable, VirtualTableColumn, VirtualTableFilterValues, OnRowClickParams, VirtualTableWhereFilterOp } from "@rebasepro/ui";
|
|
6
6
|
import { enumToObjectEntries } from "@rebasepro/common";
|
|
7
7
|
import { DEFAULT_PAGE_SIZE, EntityCollectionTableController, OnCellValueChange, OnColumnResizeParams } from "@rebasepro/core";
|
|
8
8
|
import { FilterFormFieldProps } from "@rebasepro/ui";
|
|
@@ -20,7 +20,7 @@ export type SelectableTableProps<M extends Record<string, unknown>> = {
|
|
|
20
20
|
/**
|
|
21
21
|
* Callback when a cell value changes.
|
|
22
22
|
*/
|
|
23
|
-
onValueChange?: OnCellValueChange<
|
|
23
|
+
onValueChange?: OnCellValueChange<unknown, M>;
|
|
24
24
|
|
|
25
25
|
columns: VirtualTableColumn[];
|
|
26
26
|
|
|
@@ -100,7 +100,7 @@ export type SelectableTableProps<M extends Record<string, unknown>> = {
|
|
|
100
100
|
* bust the cell memo comparator, triggering re-render of cells
|
|
101
101
|
* even when rowData hasn't changed.
|
|
102
102
|
*/
|
|
103
|
-
extraData?:
|
|
103
|
+
extraData?: unknown;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
@@ -184,12 +184,10 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
184
184
|
setItemCount?.(pageSize);
|
|
185
185
|
}, [pageSize]);
|
|
186
186
|
|
|
187
|
-
const onRowClick = useCallback(({ rowData }: {
|
|
188
|
-
rowData: Entity<M>
|
|
189
|
-
}) => {
|
|
187
|
+
const onRowClick = useCallback(({ rowData }: OnRowClickParams<Record<string, unknown>>) => {
|
|
190
188
|
if (inlineEditing)
|
|
191
189
|
return;
|
|
192
|
-
return onEntityClick && onEntityClick(rowData);
|
|
190
|
+
return onEntityClick && onEntityClick(rowData as unknown as Entity<M>);
|
|
193
191
|
}, [onEntityClick, inlineEditing]);
|
|
194
192
|
|
|
195
193
|
const select = useCallback((cell?: SelectedCellProps<M>) => {
|
|
@@ -223,18 +221,22 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
223
221
|
};
|
|
224
222
|
}, [unselect]);
|
|
225
223
|
|
|
226
|
-
const onFilterUpdate = useCallback((updatedFilterValues?:
|
|
227
|
-
setFilterValues
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
const onFilterUpdate = useCallback((updatedFilterValues?: VirtualTableFilterValues<string>) => {
|
|
225
|
+
if (setFilterValues) {
|
|
226
|
+
setFilterValues({
|
|
227
|
+
...updatedFilterValues,
|
|
228
|
+
...fixedFilter
|
|
229
|
+
} as FilterValues<Extract<keyof M, string> | (string & {})>);
|
|
230
|
+
}
|
|
231
|
+
}, [fixedFilter, setFilterValues]);
|
|
230
232
|
|
|
231
233
|
const contextValue = useMemo(() => ({
|
|
232
|
-
setPopupCell: setPopupCell as ((cell?: SelectedCellProps<M>) => void),
|
|
233
234
|
select,
|
|
234
235
|
onValueChange,
|
|
235
236
|
size: size ?? "m",
|
|
236
|
-
selectionStore
|
|
237
|
-
|
|
237
|
+
selectionStore,
|
|
238
|
+
setPopupCell
|
|
239
|
+
} as unknown as EntityCollectionTableController<Record<string, unknown>>), [setPopupCell, select, onValueChange, size, selectionStore]);
|
|
238
240
|
|
|
239
241
|
return (
|
|
240
242
|
<SelectableTableContext.Provider
|
|
@@ -243,11 +245,11 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
243
245
|
ref={ref}>
|
|
244
246
|
|
|
245
247
|
<VirtualTable
|
|
246
|
-
data={data as
|
|
248
|
+
data={data as unknown as Record<string, unknown>[]}
|
|
247
249
|
columns={columns}
|
|
248
250
|
// @ts-ignore
|
|
249
251
|
cellRenderer={cellRenderer}
|
|
250
|
-
onRowClick={inlineEditing ? undefined : (onEntityClick ?
|
|
252
|
+
onRowClick={inlineEditing ? undefined : (onEntityClick ? onRowClick : undefined)}
|
|
251
253
|
onEndReached={loadNextPage}
|
|
252
254
|
onResetPagination={resetPagination}
|
|
253
255
|
error={dataLoadingError}
|
|
@@ -262,9 +264,9 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
262
264
|
initialScroll={initialScroll}
|
|
263
265
|
onScroll={onScroll}
|
|
264
266
|
checkFilterCombination={checkFilterCombination}
|
|
265
|
-
createFilterField={filterable ?
|
|
266
|
-
rowClassName={useCallback((entity:
|
|
267
|
-
return highlightedRow?.(entity) ? "bg-surface-accent-50 dark:!bg-surface-accent-900" : "";
|
|
267
|
+
createFilterField={filterable ? createFilterField : undefined}
|
|
268
|
+
rowClassName={useCallback((entity: Record<string, unknown>) => {
|
|
269
|
+
return highlightedRow?.(entity as unknown as Entity<M>) ? "bg-surface-accent-50 dark:!bg-surface-accent-900" : "";
|
|
268
270
|
}, [highlightedRow])}
|
|
269
271
|
className="grow"
|
|
270
272
|
emptyComponent={emptyComponent}
|
|
@@ -287,7 +289,7 @@ function createFilterField({
|
|
|
287
289
|
column,
|
|
288
290
|
hidden,
|
|
289
291
|
setHidden
|
|
290
|
-
}: FilterFormFieldProps<
|
|
292
|
+
}: FilterFormFieldProps<unknown>): React.ReactNode {
|
|
291
293
|
|
|
292
294
|
if (!column.custom) {
|
|
293
295
|
return null;
|
|
@@ -313,7 +315,7 @@ function createFilterField({
|
|
|
313
315
|
hidden={hidden}
|
|
314
316
|
setHidden={setHidden}/>;
|
|
315
317
|
} else if (baseProperty.type === "relation" && baseProperty.relation) {
|
|
316
|
-
return <RelationFilterField value={filterValue as
|
|
318
|
+
return <RelationFilterField value={filterValue as [VirtualTableWhereFilterOp, EntityRelation | EntityRelation[] | null]}
|
|
317
319
|
setValue={setFilterValue}
|
|
318
320
|
name={id as string}
|
|
319
321
|
relation={baseProperty.relation}
|
|
@@ -3,4 +3,5 @@ import { EntityCollectionTableController } from "@rebasepro/core";
|
|
|
3
3
|
|
|
4
4
|
export const SelectableTableContext = React.createContext<EntityCollectionTableController<Record<string, unknown>>>(null! as EntityCollectionTableController<Record<string, unknown>>);
|
|
5
5
|
|
|
6
|
-
export const useSelectableTableController =
|
|
6
|
+
export const useSelectableTableController = <M extends Record<string, unknown> = Record<string, unknown>>() =>
|
|
7
|
+
useContext(SelectableTableContext) as unknown as EntityCollectionTableController<M>;
|
|
@@ -15,10 +15,10 @@ import { SelectedCellProps } from "@rebasepro/types";
|
|
|
15
15
|
* derivation actually changed will re-render.
|
|
16
16
|
*/
|
|
17
17
|
export function createSelectionStore() {
|
|
18
|
-
let selectedCell: SelectedCellProps
|
|
18
|
+
let selectedCell: SelectedCellProps | undefined = undefined;
|
|
19
19
|
const listeners = new Set<() => void>();
|
|
20
20
|
|
|
21
|
-
function getSnapshot(): SelectedCellProps
|
|
21
|
+
function getSnapshot(): SelectedCellProps | undefined {
|
|
22
22
|
return selectedCell;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ export function createSelectionStore() {
|
|
|
27
27
|
return () => listeners.delete(listener);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function select(cell: SelectedCellProps
|
|
30
|
+
function select(cell: SelectedCellProps | undefined) {
|
|
31
31
|
selectedCell = cell;
|
|
32
32
|
listeners.forEach(l => l());
|
|
33
33
|
}
|
|
@@ -43,7 +43,7 @@ export type SelectionStore = ReturnType<typeof createSelectionStore>;
|
|
|
43
43
|
* `selected` boolean actually changes (not on every store update).
|
|
44
44
|
*/
|
|
45
45
|
export function useCellSelected(
|
|
46
|
-
store: { getSnapshot: () => SelectedCellProps
|
|
46
|
+
store: { getSnapshot: () => SelectedCellProps | undefined; subscribe: (listener: () => void) => () => void },
|
|
47
47
|
propertyKey: string,
|
|
48
48
|
entityPath: string,
|
|
49
49
|
entityId: string | number
|