Fix for initial navigation

This commit is contained in:
francesco
2021-09-19 23:25:48 +02:00
parent 3c61ec69db
commit 08be184cce
13 changed files with 101 additions and 160 deletions

View File

@@ -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"
}
}

View File

@@ -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<Theme, { customColor: string }>(() => ({
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<string, CustomColorTextFieldProps>)
: ReactElement {
}: FieldProps<string, CustomColorTextFieldProps>) {
const classes = useStyles({ customColor: customProps.color });
return (
<>
<TextFieldWithStyles required={property.validation?.required}
error={!!error}
disabled={isSubmitting}
label={property.title}
value={value ?? ""}
onChange={(evt: any) => {
setValue(
evt.target.value
);
}}
helperText={error}
fullWidth
variant={"filled"}
customcolor={customProps.color}/>
<TextField required={property.validation?.required}
classes={{
root: classes.root
}}
error={!!error}
disabled={isSubmitting}
label={property.title}
value={value ?? ""}
onChange={(evt: any) => {
setValue(
evt.target.value
);
}}
helperText={error}
fullWidth
variant={"filled"}/>
<FieldDescription property={property}/>
</>

View File

@@ -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 (
<Box
@@ -62,9 +51,6 @@ export function SampleProductsView({ entity, modifiedValues }: {
{JSON.stringify(values, null, 2)}
</p>}
<small>
Note that "Related products" is intentionally excluded from this JSON preview
</small>
</Box>

View File

@@ -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<AuthController>({
@@ -89,8 +75,6 @@ export const AuthContext = React.createContext<AuthController>({
skipLogin: () => {
},
signOut: () => {
},
setExtra: (extra: any) => {
}
});

View File

@@ -113,7 +113,6 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
}) => {
const location = useLocation();
const navigate = useNavigate();
const initialised = useRef<boolean>(false);
const [sidePanels, setSidePanels] = useState<ExtendedPanelProps[]>([]);
@@ -137,13 +136,16 @@ export const SideEntityProvider: React.FC<SideEntityProviderProps> = ({
}
}, [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<SideEntityProviderProps> = ({
);
};
function buildSidePanelsFromUrl(path: string, allCollections: EntityCollection[], newFlag: boolean): ExtendedPanelProps[] {
function buildSidePanelsFromUrl(path: string, collections: EntityCollection[], newFlag: boolean): ExtendedPanelProps[] {
const navigationViewsForPath: NavigationViewEntry<any>[] = getNavigationEntriesFromPathInternal({
path,
allCollections
collections
});
console.log("navigationViewsForPath", navigationViewsForPath);
let sidePanels: ExtendedPanelProps[] = [];
let lastCollectionPath = "";

View File

@@ -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={
<BreadcrumbUpdater
path={addInitialSlash(path)}
key={`navigation_${path}`}
title={cmsView.name}>
{cmsView.view}
</BreadcrumbUpdater>}
@@ -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 (
<Route path={urlPath}
key={`navigation_${entityCollection.relativePath}`}
element={
<BreadcrumbUpdater
path={urlPath}
key={`navigation_${entityCollection.relativePath}`}
title={entityCollection.name}>
<EntityCollectionTable
path={entityCollection.relativePath}

View File

@@ -138,14 +138,14 @@ interface NavigationViewCustom<M> {
export function getNavigationEntriesFromPathInternal<M extends { [Key: string]: any }>(props: {
path: string,
allCollections: EntityCollection[],
collections: EntityCollection[],
customViews?: EntityCustomView<M>[],
currentFullPath?: string
}): NavigationViewEntry<M> [] {
const {
path,
allCollections,
collections,
currentFullPath
} = props;
@@ -156,26 +156,27 @@ export function getNavigationEntriesFromPathInternal<M extends { [Key: string]:
for (let i = 0; i < subpathCombinations.length; i++) {
const subpathCombination = subpathCombinations[i];
const collection = allCollections && allCollections.find((entry) => 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<M extends { [Key: string]:
result.push(...getNavigationEntriesFromPathInternal({
path: newPath,
customViews: customViews,
allCollections: collection.subcollections,
collections: collection.subcollections,
currentFullPath: fullPath
}));
}

View File

@@ -32,7 +32,6 @@ export const useFirebaseAuthController = (
const [authLoading, setAuthLoading] = React.useState(true);
const [loginSkipped, setLoginSkipped] = React.useState<boolean>(false);
const [notAllowedError, setNotAllowedError] = React.useState<boolean>(false);
const [extra, setExtra] = React.useState<any>();
useEffect(() => {
if (!firebaseApp) return;
@@ -95,8 +94,6 @@ export const useFirebaseAuthController = (
notAllowedError,
skipLogin,
signOut: onSignOut,
canAccessMainView,
extra,
setExtra
canAccessMainView
};
};

View File

@@ -73,7 +73,7 @@ export function resolveNavigationFrom<M>({
const navigationEntries = getNavigationEntriesFromPathInternal({
path,
allCollections: navigation.collections
collections: navigation.collections
});
const resultPromises: Promise<NavigationEntry<any>>[] = navigationEntries.map((entry) => {

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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<Theme, { customColor: string }>(() => ({
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<string, CustomColorTextFieldProps>)
: ReactElement {
}: FieldProps<string, CustomColorTextFieldProps>) {
const classes = useStyles({ customColor: customProps.color });
return (
<>
<TextFieldWithStyles required={property.validation?.required}
error={!!error}
disabled={isSubmitting}
label={property.title}
value={value ?? ""}
onChange={(evt: any) => {
setValue(
evt.target.value
);
}}
helperText={error}
fullWidth
variant={"filled"}
customcolor={customProps.color}/>
<TextField required={property.validation?.required}
classes={{
root: classes.root
}}
error={!!error}
disabled={isSubmitting}
label={property.title}
value={value ?? ""}
onChange={(evt: any) => {
setValue(
evt.target.value
);
}}
helperText={error}
fullWidth
variant={"filled"}/>
<FieldDescription property={property}/>
</>

View File

@@ -84,6 +84,8 @@ const productAdditionalColumn: AdditionalColumnDelegate<Product> = {
<div>{entity.values.title}</div>
};
```
- `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