diff --git a/.eslintrc b/.eslintrc index aa4c151..9540c26 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,6 @@ "extends": [ "standard", "standard-react", -// "plugin:prettier/recommended", "prettier/standard", "prettier/react" ], @@ -30,6 +29,7 @@ "react/no-unused-prop-types": 0, "import/export": 0, "no-unused-vars": "off", + "no-shadow": "error", "@typescript-eslint/no-unused-vars": "error" } } diff --git a/example/src/SampleApp/custom_field/CustomColorTextField.tsx b/example/src/SampleApp/custom_field/CustomColorTextField.tsx index 19c6ac5..d3e6d27 100644 --- a/example/src/SampleApp/custom_field/CustomColorTextField.tsx +++ b/example/src/SampleApp/custom_field/CustomColorTextField.tsx @@ -1,19 +1,17 @@ +import React from "react"; import { TextField, Theme } from "@mui/material"; -import withStyles from '@mui/styles/withStyles'; -import React, { ReactElement } from "react"; +import makeStyles from "@mui/styles/makeStyles"; import { FieldDescription, FieldProps } from "@camberi/firecms"; interface CustomColorTextFieldProps { color: string } -export const TextFieldWithStyles = withStyles((theme: Theme) => ({ - root: (props: any) => ({ - "& .MuiFilledInput-root": { - backgroundColor: props.customcolor - } +const useStyles = makeStyles(() => ({ + root: ({ customColor }) => ({ + backgroundColor: customColor }) -}))(TextField); +})); export default function CustomColorTextField({ property, @@ -25,25 +23,28 @@ export default function CustomColorTextField({ isSubmitting, context, // the rest of the entity values here ...props - }: FieldProps) - : ReactElement { + }: FieldProps) { + + const classes = useStyles({ customColor: customProps.color }); return ( <> - { - setValue( - evt.target.value - ); - }} - helperText={error} - fullWidth - variant={"filled"} - customcolor={customProps.color}/> + { + setValue( + evt.target.value + ); + }} + helperText={error} + fullWidth + variant={"filled"}/> diff --git a/example/src/SampleApp/custom_schema_view/SampleProductsView.tsx b/example/src/SampleApp/custom_schema_view/SampleProductsView.tsx index b2b4627..51c7f26 100644 --- a/example/src/SampleApp/custom_schema_view/SampleProductsView.tsx +++ b/example/src/SampleApp/custom_schema_view/SampleProductsView.tsx @@ -17,18 +17,7 @@ export function SampleProductsView({ entity, modifiedValues }: { }); }; - const includePropertyValue = (key:string, value: any ): boolean => { - if (key === "related_products") - return false; - return true; - }; - - const values = modifiedValues ? - Object.entries(modifiedValues) - .filter(([key, value]) => includePropertyValue(key, value)) - .map(([key, value]) => ({ [key]: value })) - .reduce((a, b) => ({ ...a, ...b })) - : {}; + const values = modifiedValues ? modifiedValues : {}; return ( } - - Note that "Related products" is intentionally excluded from this JSON preview - diff --git a/src/contexts/AuthController.tsx b/src/contexts/AuthController.tsx index 402099e..819b119 100644 --- a/src/contexts/AuthController.tsx +++ b/src/contexts/AuthController.tsx @@ -60,20 +60,6 @@ export interface AuthController { */ signOut: () => void; - /** - * Utility field you can use to store your custom data. - * e.g: Additional user data fetched from a Firestore document, or custom - * claims - */ - extra?: any; - - /** - * You can use this method to store any extra data you would like to - * associate your user to. - * e.g: Additional user data fetched from a Firestore document, or custom - * claims - */ - setExtra: (extra: any) => void; } export const AuthContext = React.createContext({ @@ -89,8 +75,6 @@ export const AuthContext = React.createContext({ skipLogin: () => { }, signOut: () => { - }, - setExtra: (extra: any) => { } }); diff --git a/src/contexts/SideEntityController.tsx b/src/contexts/SideEntityController.tsx index f03d363..ddff35a 100644 --- a/src/contexts/SideEntityController.tsx +++ b/src/contexts/SideEntityController.tsx @@ -113,7 +113,6 @@ export const SideEntityProvider: React.FC = ({ }) => { const location = useLocation(); - const navigate = useNavigate(); const initialised = useRef(false); const [sidePanels, setSidePanels] = useState([]); @@ -137,13 +136,16 @@ export const SideEntityProvider: React.FC = ({ } }, [location?.state, schemasRegistry.initialised]); - // only on initialisation useEffect(() => { + console.log("initialised.current", initialised.current, collections); if (collections && !initialised.current) { if (isCollectionPath(location.pathname)) { const newFlag = location.hash === "#new"; - const sidePanels = buildSidePanelsFromUrl(getEntityOrCollectionPath(location.pathname), collections, newFlag); + const entityOrCollectionPath = getEntityOrCollectionPath(location.pathname); + console.log("entityOrCollectionPath", entityOrCollectionPath); + const sidePanels = buildSidePanelsFromUrl(entityOrCollectionPath, collections, newFlag); + console.log("sidePanels", sidePanels, location.pathname); setSidePanels(sidePanels); } initialised.current = true; @@ -258,12 +260,13 @@ export const SideEntityProvider: React.FC = ({ ); }; -function buildSidePanelsFromUrl(path: string, allCollections: EntityCollection[], newFlag: boolean): ExtendedPanelProps[] { +function buildSidePanelsFromUrl(path: string, collections: EntityCollection[], newFlag: boolean): ExtendedPanelProps[] { const navigationViewsForPath: NavigationViewEntry[] = getNavigationEntriesFromPathInternal({ path, - allCollections + collections }); + console.log("navigationViewsForPath", navigationViewsForPath); let sidePanels: ExtendedPanelProps[] = []; let lastCollectionPath = ""; diff --git a/src/core/CMSRoutes.tsx b/src/core/CMSRoutes.tsx index 2dc2689..d5dddb7 100644 --- a/src/core/CMSRoutes.tsx +++ b/src/core/CMSRoutes.tsx @@ -2,7 +2,11 @@ import React from "react"; import { Route, Routes, useLocation } from "react-router-dom"; import { CMSView, Navigation } from "../models"; -import { addInitialSlash, buildCollectionUrl } from "./navigation"; +import { + addInitialSlash, + buildCollectionUrl, + removeTrailingSlash +} from "./navigation"; import { EntityCollectionTable } from "./components/EntityCollectionTable"; import BreadcrumbUpdater from "./components/BreadcrumbUpdater"; import CMSHome from "./components/CMSHome"; @@ -44,7 +48,6 @@ export function CMSRoutes({ HomePage }: { element={ {cmsView.view} } @@ -63,13 +66,13 @@ export function CMSRoutes({ HomePage }: { // we reorder collections so that nested paths are included first .sort((a, b) => b.relativePath.length - a.relativePath.length) .map(entityCollection => { - const urlPath = buildCollectionUrl(entityCollection.relativePath); + const urlPath = removeTrailingSlash(buildCollectionUrl(entityCollection.relativePath)) + "/*"; return ( { export function getNavigationEntriesFromPathInternal(props: { path: string, - allCollections: EntityCollection[], + collections: EntityCollection[], customViews?: EntityCustomView[], currentFullPath?: string }): NavigationViewEntry [] { const { path, - allCollections, + collections, currentFullPath } = props; @@ -156,26 +156,27 @@ export function getNavigationEntriesFromPathInternal entry.relativePath === subpathCombination); + const collection = collections && collections.find((entry) => entry.relativePath === subpathCombination); + if (collection) { - const path = currentFullPath && currentFullPath.length > 0 + const collectionPath = currentFullPath && currentFullPath.length > 0 ? (currentFullPath + "/" + collection.relativePath) : collection.relativePath; result.push({ type: "collection", - path, + path: collectionPath, collection }); const restOfThePath = removeInitialAndTrailingSlashes(path.replace(subpathCombination, "")); const nextSegments = restOfThePath.length > 0 ? restOfThePath.split("/") : []; if (nextSegments.length > 0) { const entityId = nextSegments[0]; - const fullPath = path + "/" + entityId; + const fullPath = collectionPath + "/" + entityId; result.push({ type: "entity", entityId: entityId, - relativePath: path, + relativePath: collectionPath, path: fullPath, parentCollection: collection }); @@ -196,7 +197,7 @@ export function getNavigationEntriesFromPathInternal(false); const [notAllowedError, setNotAllowedError] = React.useState(false); - const [extra, setExtra] = React.useState(); useEffect(() => { if (!firebaseApp) return; @@ -95,8 +94,6 @@ export const useFirebaseAuthController = ( notAllowedError, skipLogin, signOut: onSignOut, - canAccessMainView, - extra, - setExtra + canAccessMainView }; }; diff --git a/src/hooks/useResolvedNavigationFrom.tsx b/src/hooks/useResolvedNavigationFrom.tsx index 4f7c28d..ba29f3f 100644 --- a/src/hooks/useResolvedNavigationFrom.tsx +++ b/src/hooks/useResolvedNavigationFrom.tsx @@ -73,7 +73,7 @@ export function resolveNavigationFrom({ const navigationEntries = getNavigationEntriesFromPathInternal({ path, - allCollections: navigation.collections + collections: navigation.collections }); const resultPromises: Promise>[] = navigationEntries.map((entry) => { diff --git a/src/models/user.ts b/src/models/user.ts index aaa3b3b..7b34a01 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -47,7 +47,7 @@ export interface User { readonly providerData: any; /** - * Additional data you can use to store any information relevat to you, + * Additional data you can use to store any information relevant to you, * such as roles. */ extra:any; diff --git a/src/test/navigation.test.ts b/src/test/navigation.test.ts index ed209b0..77b8b9e 100644 --- a/src/test/navigation.test.ts +++ b/src/test/navigation.test.ts @@ -28,12 +28,6 @@ it("collection view matches ok", () => { collectionViewFromPath3 && collectionViewFromPath3.relativePath ).toEqual("locales"); - expect( - getCollectionViewFromPath("products/pid/not_existing", collectionViews) - ).toEqual( - undefined - ); - expect( () => getCollectionViewFromPath("products/pid", collectionViews) ).toThrow( @@ -42,9 +36,8 @@ it("collection view matches ok", () => { expect( getCollectionViewFromPath("products", []) - ).toThrow( - undefined - ); + ).toEqual(undefined); + const collectionViewFromPath10 = getCollectionViewFromPath("products/id/subcollection_inline", collectionViews); expect( collectionViewFromPath10 && collectionViewFromPath10.relativePath @@ -56,7 +49,7 @@ it("build entity collection array", () => { const collections = getNavigationEntriesFromPathInternal({ path: "products/pid", - allCollections: collectionViews + collections: collectionViews }); console.log(collections); // expect( @@ -66,24 +59,20 @@ it("build entity collection array", () => { it("Custom view internal", () => { - const collections = getNavigationEntriesFromPathInternal({ + const navigationEntries = getNavigationEntriesFromPathInternal({ path: "products/pid/custom_view", - allCollections: collectionViews + collections: collectionViews }); - console.log(collections); - // expect( - // collections.map((collection) => collection.relativePath) - // ).toEqual(["products", "locales"]); + console.log(navigationEntries); + expect(navigationEntries.length).toEqual(3); }); it("build entity collection array 2", () => { - const collections = getNavigationEntriesFromPathInternal({ + const navigationEntries = getNavigationEntriesFromPathInternal({ path: "products/pid/locales/yep", - allCollections: collectionViews + collections: collectionViews }); - console.log(collections); - // expect( - // collections.map((collection) => collection.relativePath) - // ).toEqual(["products", "locales"]); + console.log(navigationEntries); + expect(navigationEntries.length).toEqual(4); }); diff --git a/website/docs/custom_fields.md b/website/docs/custom_fields.md index 591c33d..cb89879 100644 --- a/website/docs/custom_fields.md +++ b/website/docs/custom_fields.md @@ -5,66 +5,38 @@ sidebar_label: Custom fields --- If you need a custom field for your property you can do it by passing a React -component to the `field` prop of a property `config`. The React component must -accept the props of type `CMSFieldProps`. The bare minimum you need to implement is a field that displays the -received `value` and uses the `setValue` callback. - -You can also specify your own props that are passed to your component in `customProps` - -## Custom field props - CMSFieldProps - -* `name` The name of the property, such as `age`. You can use nested and array - indexed such as `address.street` or `people[3]` - -* `property` The CMS property you are binding this field to - -* `context` The context where this field is being rendered. You get a - context as a prop when creating a custom field. - -* `includeDescription` Should the description be included in this field - -* `underlyingValueHasChanged` Has the value of this property been updated - in the database while this field is being edited - -* `tableMode` Is this field being rendered in a table - -* `partOfArray` Is this field part of an array - -* `autoFocus` Should the field take focus when rendered. When opening the - popup view in table mode, it makes sense to put the focus on the only - field rendered. - -* `disabled` Should this field be disabled - -* `dependsOnOtherProperties` This flag is used to avoid using Formik - FastField internally, which prevents being updated from the values +component to the `Field` prop of a property `config`. The React component must +accept the props of type `[FieldProps](api/interfaces/fieldprops.md)`. +The bare minimum you need to implement +is a field that displays the received `value` and uses the `setValue` callback. You can also pass custom props to your custom field, which you then receive in -the `customProps`. +the `customProps` prop. If you are developing a custom field and need to access the values of the -entity, you can use the `context` field in CMSFieldProps. +entity, you can use the `context` field in FieldProps. + +You can check all the props `[FieldProps](api/interfaces/fieldprops.md)` ## Example This is an example of a custom TextField that takes the background color as a prop ```tsx -import { TextField, Theme, withStyles } from "@mui/material"; -import React, { ReactElement } from "react"; +import React from "react"; +import { TextField, Theme } from "@mui/material"; +import makeStyles from "@mui/styles/makeStyles"; import { FieldDescription, FieldProps } from "@camberi/firecms"; interface CustomColorTextFieldProps { color: string } -export const TextFieldWithStyles = withStyles((theme: Theme) => ({ - root: (props: any) => ({ - "& .MuiFilledInput-root": { - backgroundColor: props.customcolor - } +const useStyles = makeStyles(() => ({ + root: ({ customColor }) => ({ + backgroundColor: customColor }) -}))(TextField); +})); export default function CustomColorTextField({ property, @@ -76,25 +48,28 @@ export default function CustomColorTextField({ isSubmitting, context, // the rest of the entity values here ...props - }: FieldProps) - : ReactElement { + }: FieldProps) { + + const classes = useStyles({ customColor: customProps.color }); return ( <> - { - setValue( - evt.target.value - ); - }} - helperText={error} - fullWidth - variant={"filled"} - customcolor={customProps.color}/> + { + setValue( + evt.target.value + ); + }} + helperText={error} + fullWidth + variant={"filled"}/> diff --git a/website/docs/updating_from_alpha_versions.md b/website/docs/updating_from_alpha_versions.md index f512ee1..80ebc69 100644 --- a/website/docs/updating_from_alpha_versions.md +++ b/website/docs/updating_from_alpha_versions.md @@ -84,6 +84,8 @@ const productAdditionalColumn: AdditionalColumnDelegate = {
{entity.values.title}
}; ``` +- `AuthController` no longer has `extra` and `setExtra` props. If you need that + functionality, you can use the `extra` field in the `User` - `PermissionsBuilder` no longer has an `authController` prop, but it can still be accessed through the `context` prop. Related, the new `User` type includes