Enabled overriding of entity schemas side panel

This commit is contained in:
francesco
2021-02-10 00:43:27 +01:00
parent 0d6b7569a0
commit f2df253be3
13 changed files with 292 additions and 107 deletions

View File

@@ -1,13 +1,25 @@
import React from "react";
import { Box, Button } from "@material-ui/core";
import { useSnackbarController, useAuthContext } from "@camberi/firecms";
import { useSnackbarController, useAuthContext, useSideEntityController, buildSchema } from "@camberi/firecms";
export function ExampleAdditionalView() {
const snackbarController = useSnackbarController();
const sideEntityController = useSideEntityController();
const authContext = useAuthContext();
const customProductSchema = buildSchema({
name: "Product",
properties: {
name: {
title: "Name",
validation: { required: true },
dataType: "string"
},
}
});
return (
<Box
display="flex"
@@ -30,10 +42,20 @@ export function ExampleAdditionalView() {
<Button
onClick={() => snackbarController.open({
type: "success",
message: "Test snackbar"
message: "This is pretty cool"
})}
color="primary">
Click me
Test snackbar
</Button>
<Button
onClick={() => sideEntityController.open({
entityId: "B003WT1622",
collectionPath: "/products",
schema: customProductSchema
})}
color="primary">
Open entity with custom schema
</Button>
</Box>

View File

@@ -36,7 +36,8 @@ import { CMSDrawer } from "./CMSDrawer";
import { CMSRouterSwitch } from "./CMSRouterSwitch";
import { CMSAppBar } from "./components/CMSAppBar";
import { EntitySideDialogs } from "./side_dialog/EntitySideDialogs";
import { SideEntityProvider } from "./side_dialog/SideEntityContext";
import { SideEntityProvider } from "./side_dialog/SideEntityPanelsController";
import { SchemaOverrideRegistryProvider } from "./side_dialog/SchemaOverrideRegistry";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -252,39 +253,41 @@ export function CMSApp(props: CMSAppProps) {
function renderMainView() {
return (
<Router>
<SideEntityProvider navigation={navigation}>
<BreadcrumbsProvider>
<MuiPickersUtilsProvider
utils={DateFnsUtils}>
<DndProvider backend={HTML5Backend}>
<SchemaOverrideRegistryProvider navigation={navigation}>
<SideEntityProvider navigation={navigation}>
<BreadcrumbsProvider>
<MuiPickersUtilsProvider
utils={DateFnsUtils}>
<DndProvider backend={HTML5Backend}>
<nav>
<CMSDrawer logo={logo}
drawerOpen={drawerOpen}
navigation={navigation}
closeDrawer={closeDrawer}
additionalViews={additionalViews}/>
</nav>
<nav>
<CMSDrawer logo={logo}
drawerOpen={drawerOpen}
navigation={navigation}
closeDrawer={closeDrawer}
additionalViews={additionalViews}/>
</nav>
<div className={classes.main}>
<CMSAppBar title={name}
handleDrawerToggle={handleDrawerToggle}
toolbarExtraWidget={toolbarExtraWidget}/>
<div className={classes.main}>
<CMSAppBar title={name}
handleDrawerToggle={handleDrawerToggle}
toolbarExtraWidget={toolbarExtraWidget}/>
<main
className={classes.content}>
<CMSRouterSwitch
navigation={navigation}
additionalViews={additionalViews}/>
</main>
</div>
<main
className={classes.content}>
<CMSRouterSwitch
navigation={navigation}
additionalViews={additionalViews}/>
</main>
</div>
<EntitySideDialogs navigation={navigation}/>
<EntitySideDialogs/>
</DndProvider>
</MuiPickersUtilsProvider>
</BreadcrumbsProvider>
</SideEntityProvider>
</DndProvider>
</MuiPickersUtilsProvider>
</BreadcrumbsProvider>
</SideEntityProvider>
</SchemaOverrideRegistryProvider>
</Router>
);
}

View File

@@ -15,7 +15,7 @@ import {
Typography
} from "@material-ui/core";
import { Delete, FileCopy, KeyboardTab, MoreVert } from "@material-ui/icons";
import { useSideEntityController } from "../side_dialog/SideEntityContext";
import { useSideEntityController } from "../side_dialog/SideEntityPanelsController";
export function CollectionRowActions<S extends EntitySchema>({
entity,

View File

@@ -13,7 +13,7 @@ import { Add, Delete } from "@material-ui/icons";
import { CollectionRowActions } from "./CollectionRowActions";
import DeleteEntityDialog from "./DeleteEntityDialog";
import { getSubcollectionColumnId, useColumnIds } from "./common";
import { useSideEntityController } from "../side_dialog/SideEntityContext";
import { useSideEntityController } from "../side_dialog/SideEntityPanelsController";
type EntitySubCollectionProps<S extends EntitySchema> = {
collectionPath: string;

View File

@@ -28,7 +28,7 @@ import ClearIcon from "@material-ui/icons/Clear";
import { listenEntityFromRef } from "../../models/firestore";
import KeyboardTabIcon from "@material-ui/icons/KeyboardTab";
import { CollectionTable } from "../../collection/CollectionTable";
import { useSideEntityController } from "../../side_dialog/SideEntityContext";
import { useSideEntityController } from "../../side_dialog/SideEntityPanelsController";
export const useStyles = makeStyles(theme => createStyles({
root: {

View File

@@ -118,6 +118,17 @@ export type {
AuthContextController,
BreadcrumbsStatus
} from "./contexts";
export {
useSideEntityController
} from "./side_dialog/SideEntityPanelsController";
export type {
SideEntityPanelsController
} from "./side_dialog/SideEntityPanelsController";
export type {
EntitySidePanelProps, SchemaSidePanelProps
} from "./side_dialog/model";
export {
useSnackbarController,
useBreadcrumbsContext,

View File

@@ -18,7 +18,7 @@ import CloseIcon from "@material-ui/icons/Close";
import EditIcon from "@material-ui/icons/Edit";
import { getCMSPathFrom, removeInitialSlash } from "../routes/navigation";
import { EntityCollectionTable } from "../collection/EntityCollectionTable";
import { useSideEntityController } from "../side_dialog/SideEntityContext";
import { useSideEntityController } from "../side_dialog/SideEntityPanelsController";
export const useStyles = makeStyles(theme => createStyles({

View File

@@ -32,7 +32,7 @@ import { useAppConfigContext } from "../../contexts";
import firebase from "firebase/app";
import "firebase/firestore";
import { PreviewComponent } from "../PreviewComponent";
import { useSideEntityController } from "../../side_dialog/SideEntityContext";
import { useSideEntityController } from "../../side_dialog/SideEntityPanelsController";
import { PreviewError } from "./PreviewError";
import { Skeleton } from "@material-ui/lab";

View File

@@ -1,10 +1,12 @@
import React from "react";
import { EntityCollection, EntitySchema } from "../models";
import { EntitySchema } from "../models";
import { createStyles, makeStyles } from "@material-ui/core";
import { EntityDrawer } from "./EntityDrawer";
import EntityView from "./EntityView";
import { SidePanelProps, useSideEntityController } from "./SideEntityContext";
import { getCollectionViewFromPath } from "../routes/navigation";
import { useSideEntityController } from "./SideEntityPanelsController";
import { useSchemasRegistryController } from "./SchemaOverrideRegistry";
import { EntitySidePanelProps, SchemaSidePanelProps } from "./model";
import { ErrorBoundary } from "../components";
export const useStyles = makeStyles(theme => createStyles({
root: {
@@ -40,12 +42,14 @@ export const useStyles = makeStyles(theme => createStyles({
}));
export function EntitySideDialogs<S extends EntitySchema>({ navigation }: { navigation: EntityCollection[] }) {
export function EntitySideDialogs<S extends EntitySchema>() {
const sideEntityController = useSideEntityController();
const schemasRegistry = useSchemasRegistryController();
const sideEntityContext = useSideEntityController();
const classes = useStyles();
const sidePanels = sideEntityContext.sidePanels;
const sidePanels = sideEntityController.sidePanels;
// const [sidePanelBeingClosed, setSidePanelBeingClosed] = useState<SidePanelProps | undefined>();
const onExitAnimation = () => {
@@ -54,33 +58,35 @@ export function EntitySideDialogs<S extends EntitySchema>({ navigation }: { navi
const allPanels = [...sidePanels, undefined];
function buildEntityView(panel: SidePanelProps) {
function buildEntityView(panel: EntitySidePanelProps) {
const entityCollection = getCollectionViewFromPath(panel.collectionPath, navigation);
const editable = entityCollection.editEnabled == undefined || entityCollection.editEnabled;
const schema = entityCollection.schema;
const subcollections = entityCollection.subcollections;
const extendedProps: SchemaSidePanelProps | null = schemasRegistry.get(panel.collectionPath);
return <EntityView
key={`side-form-route-${panel.entityId}`}
editable={editable}
schema={schema}
subcollections={subcollections}
{...panel}/>;
if (!extendedProps)
return null;
return (
<ErrorBoundary>
<EntityView
key={`side-form-route-${panel.entityId}`}
{...extendedProps}
{...panel}/>
</ErrorBoundary>
);
}
return (
<>
{/* we add an extra closed drawer, that it is used to maintain the transition when a drawer is removed */}
{
allPanels.map((panel: SidePanelProps | undefined, index) => {
allPanels.map((panel: EntitySidePanelProps | undefined, index) => {
return (
<EntityDrawer
key={`side_menu_${index}`}
open={panel !== undefined}
onClose={() => {
sideEntityContext.close();
sideEntityController.close();
}}
offsetPosition={sidePanels.length - index - 1}
onExitAnimation={onExitAnimation}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
import EntityForm from "../form/EntityForm";
import {
Entity,
@@ -7,7 +7,7 @@ import {
EntityStatus,
EntityValues
} from "../models";
import { listenEntity, saveEntity } from "../models/firestore";
import { listenEntity, saveEntity } from "../models";
import {
Box,
CircularProgress,
@@ -24,11 +24,8 @@ import { Prompt } from "react-router-dom";
import CloseIcon from "@material-ui/icons/Close";
import { EntityPreview } from "../preview";
import { EntityCollectionTable } from "../collection/EntityCollectionTable";
import {
getCollectionViewFromPath,
removeInitialSlash
} from "../routes/navigation";
import { useSideEntityController } from "./SideEntityContext";
import { removeInitialSlash } from "../routes/navigation";
import { useSideEntityController } from "./SideEntityPanelsController";
import CircularProgressCenter from "../components/CircularProgressCenter";
@@ -85,9 +82,9 @@ const useStylesSide = makeStyles((theme: Theme) =>
export interface EntitySideViewProps {
collectionPath: string;
entityId?: string;
copy: boolean;
copy?: boolean;
selectedSubcollection?: string;
editable:boolean;
editEnabled?:boolean;
schema: EntitySchema<any>;
subcollections?: EntityCollection[];
}
@@ -97,7 +94,7 @@ function EntitySideView({
entityId,
selectedSubcollection,
copy,
editable,
editEnabled = true,
schema,
subcollections
}: EntitySideViewProps) {
@@ -230,7 +227,7 @@ function EntitySideView({
const containerRef = React.useRef<HTMLDivElement>(null);
const form = editable ? (
const form = editEnabled ? (
<EntityForm
status={status}
collectionPath={collectionPath}
@@ -324,7 +321,7 @@ function EntitySideView({
scrollButtons="auto"
>
<Tab
label={`${editable ? (existingEntity ? "Edit" : `Add New`) : ""} ${schema.name}`
label={`${editEnabled ? (existingEntity ? "Edit" : `Add New`) : ""} ${schema.name}`
}/>
{subcollections && subcollections.map(

View File

@@ -0,0 +1,88 @@
import React, { useContext, useRef } from "react";
import { EntityCollection, EntitySchema } from "../models";
import { SchemaSidePanelProps } from "./model";
import { getCollectionViewFromPath } from "../routes/navigation";
const DEFAULT_SIDE_ENTITY = {
/**
* Get
*/
get: (collectionPath: string) => null,
/**
* Set
*/
set: (
collectionPath: string,
sidePanelProps: SchemaSidePanelProps | null
) => {
}
};
export type SchemasRegistryPanelsController<S extends EntitySchema> = {
/**
* Get props for path
*/
get: (collectionPath: string) => SchemaSidePanelProps | null;
/**
* Set props for path
*/
set: (
collectionPath: string,
sidePanelProps: SchemaSidePanelProps | null
) => void;
};
export const SchemasRegistryContext = React.createContext<SchemasRegistryPanelsController<any>>(DEFAULT_SIDE_ENTITY);
export const useSchemasRegistryController = () => useContext(SchemasRegistryContext);
interface ViewRegistryProviderProps {
children: React.ReactNode;
navigation: EntityCollection[];
}
export const SchemaOverrideRegistryProvider: React.FC<ViewRegistryProviderProps> = ({
children,
navigation
}) => {
const viewsRef = useRef<Record<string, SchemaSidePanelProps>>({});
const get = (collectionPath: string): SchemaSidePanelProps | null => {
let props: SchemaSidePanelProps | null = viewsRef.current[collectionPath];
if (!props) {
const entityCollection: EntityCollection = getCollectionViewFromPath(collectionPath, navigation);
const editEnabled = entityCollection.editEnabled == undefined || entityCollection.editEnabled;
const schema = entityCollection.schema;
const subcollections = entityCollection.subcollections;
props = {
editEnabled, schema, subcollections
};
}
return props;
};
const set = (
collectionPath: string,
sidePanelProps: SchemaSidePanelProps | null
) => {
if (!sidePanelProps) {
delete viewsRef.current[collectionPath];
} else {
viewsRef.current[collectionPath] = sidePanelProps;
}
};
return (
<SchemasRegistryContext.Provider
value={{
get,
set
}}
>
{children}
</SchemasRegistryContext.Provider>
);
};

View File

@@ -10,33 +10,43 @@ import {
isCollectionPath,
NavigationEntry
} from "../routes/navigation";
import { EntitySidePanelProps, SchemaSidePanelProps } from "./model";
import { useSchemasRegistryController } from "./SchemaOverrideRegistry";
const DEFAULT_SIDE_ENTITY = {
sidePanels: [],
close: () => {
},
open: (props: {
collectionPath: string,
entityId?: string,
selectedSubcollection?: string,
copy?: boolean
}) => {
open: (props: EntitySidePanelProps & Partial<SchemaSidePanelProps>) => {
}
};
export type SideEntityPanelsController<S extends EntitySchema> = {
/**
* Close the last panel
*/
close: () => void;
sidePanels: SidePanelProps[];
open: (props: {
collectionPath: string,
entityId?: string,
selectedSubcollection?: string,
copy?: boolean
}) => void;
/**
* List of side entity panels currently open
*/
sidePanels: EntitySidePanelProps[];
/**
* Open a new entity sideDialog
* @param props
*/
open: (props: EntitySidePanelProps & Partial<SchemaSidePanelProps>) => void;
};
export const SideEntityContext = React.createContext<SideEntityPanelsController<any>>(DEFAULT_SIDE_ENTITY);
export const useSideEntityController = () => useContext(SideEntityContext);
export const SideEntityPanelsController = React.createContext<SideEntityPanelsController<any>>(DEFAULT_SIDE_ENTITY);
/**
* Get a reference to the controller used to open side dialogs for entity
* edition.
*/
export const useSideEntityController = () => useContext(SideEntityPanelsController);
interface SideEntityProviderProps {
children: React.ReactNode;
@@ -51,7 +61,9 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
const location: any = useLocation();
const history = useHistory();
const initialised = useRef<boolean>(false);
const [sidePanels, setSidePanels] = useState<SidePanelProps[]>([]);
const [sidePanels, setSidePanels] = useState<EntitySidePanelProps[]>([]);
const viewRegistry = useSchemasRegistryController();
const mainLocation = location.state && location.state["main_location"] ? location.state["main_location"] : location;
@@ -89,21 +101,26 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
const newPath = getCMSPathFrom(lastSidePanel.collectionPath);
history.replace(newPath);
}
viewRegistry.set(lastSidePanel.collectionPath, null);
};
const open = (props: {
collectionPath: string,
entityId?: string,
selectedSubcollection?: string,
copy?: boolean
}) => {
const { collectionPath, entityId, selectedSubcollection, copy } = props;
const open = ({
collectionPath,
entityId,
selectedSubcollection,
copy,
...schemaProps
}: EntitySidePanelProps & Partial<SchemaSidePanelProps>) => {
if (copy && !entityId) {
throw Error("If you want to copy an entity you need to provide an entityId");
}
if (schemaProps && Object.keys(schemaProps).length > 0) {
viewRegistry.set(collectionPath, schemaProps as SchemaSidePanelProps);
}
const newPath = entityId
? getEntityPath(entityId, collectionPath, selectedSubcollection)
: getRouterNewEntityPath(collectionPath);
@@ -112,7 +129,7 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
// If the side dialog is open currently, we update it
if (entityId && lastSidePanel && lastSidePanel?.entityId === entityId) {
const updatedPanel: SidePanelProps = {
const updatedPanel: EntitySidePanelProps = {
...lastSidePanel,
selectedSubcollection
};
@@ -125,7 +142,7 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
);
} else {
const newPanel: SidePanelProps = {
const newPanel: EntitySidePanelProps = {
collectionPath,
entityId,
copy: copy !== undefined && copy,
@@ -142,7 +159,7 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
};
return (
<SideEntityContext.Provider
<SideEntityPanelsController.Provider
value={{
sidePanels,
close,
@@ -150,24 +167,16 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
}}
>
{children}
</SideEntityContext.Provider>
</SideEntityPanelsController.Provider>
);
};
export interface SidePanelProps {
collectionPath: string;
entityId?: string;
copy: boolean;
selectedSubcollection?: string;
}
function buildSidePanelsFromUrl(path: string, allCollections: EntityCollection[], newFlag: boolean): SidePanelProps[] {
function buildSidePanelsFromUrl(path: string, allCollections: EntityCollection[], newFlag: boolean): EntitySidePanelProps[] {
const navigationViewsForPath: NavigationEntry[] = getCollectionViewsFromPath(path, allCollections);
let fullPath: string = "";
let sidePanels: SidePanelProps[] = [];
let sidePanels: EntitySidePanelProps[] = [];
for (let i = 0; i < navigationViewsForPath.length; i++) {
const navigationEntry = navigationViewsForPath[i];
@@ -186,7 +195,7 @@ function buildSidePanelsFromUrl(path: string, allCollections: EntityCollection[]
);
}
} else if (navigationEntry.type === "collection") {
const lastSidePanel: SidePanelProps = sidePanels[sidePanels.length - 1];
const lastSidePanel: EntitySidePanelProps = sidePanels[sidePanels.length - 1];
if (lastSidePanel)
lastSidePanel.selectedSubcollection = navigationEntry.collection.relativePath;
}

49
src/side_dialog/model.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { EntityCollection, EntitySchema } from "../models";
export interface EntitySidePanelProps {
/**
* Absolute path of the entity
*/
collectionPath: string;
/**
* Id of the entity, if not set, it means we are creating a new entity
*/
entityId?: string;
/**
* Set this flag to true if you want to make a copy of an existing entity
*/
copy?: boolean;
/**
* Open the entity with a selected subcollection view. If the panel for this
* entity was already open, it is replaced.
*/
selectedSubcollection?: string;
}
/**
* You can add these additional props to override properties
*/
export interface SchemaSidePanelProps {
/**
* Can the elements in this collection be added and edited. Defaults to `true`
*/
editEnabled?: boolean;
/**
* Schema representing the entities of this view
*/
schema: EntitySchema<any>;
/**
* Following the Firestore document and collection schema, you can add
* subcollections to your entity in the same way you define the root
* collections.
*/
subcollections?: EntityCollection[];
}