mirror of
https://github.com/zhigang1992/firecms.git
synced 2026-06-16 10:33:46 +08:00
Fix for initial navigation
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}/>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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}/>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user