Performance updates

This commit is contained in:
francesco
2021-12-16 19:07:23 +01:00
parent 69c145a4e1
commit 3e061a0363
9 changed files with 225 additions and 147 deletions

View File

@@ -82,6 +82,13 @@ export const testEntitySchema = buildSchema({
properties: properties
});
},
product: {
title: "Product",
dataType: "reference",
path: "products",
previewProperties: ["name", "main_image"]
},
// gallery: {
// title: 'Gallery',
// dataType: 'array',
@@ -223,12 +230,6 @@ export const testEntitySchema = buildSchema({
// unique: true
// }
// },
// product: {
// title: "Product",
// dataType: "reference",
// path: "products",
// previewProperties: ["name", "main_image"]
// },
// disabled_product: {
// title: "Disabled product",
// dataType: "reference",

View File

@@ -68,7 +68,17 @@ export const useStyles = makeStyles<Theme>(theme => createStyles({
* @see Table
* @category Components
*/
export const CollectionTable = React.memo<CollectionTableProps<any, any>>(CollectionTableInternal) as React.FunctionComponent<CollectionTableProps<any, any>>;
export const CollectionTable = React.memo<CollectionTableProps<any, any>>(CollectionTableInternal, areEqual) as React.FunctionComponent<CollectionTableProps<any, any>>;
function areEqual(prevProps: CollectionTableProps<any, any>, nextProps: CollectionTableProps<any, any>) {
return prevProps.path === nextProps.path
&& prevProps.collection === nextProps.collection
&& prevProps.title === nextProps.title
&& prevProps.toolbarActionsBuilder === nextProps.toolbarActionsBuilder
&& prevProps.tableRowActionsBuilder === nextProps.tableRowActionsBuilder
&& prevProps.inlineEditing === nextProps.inlineEditing
;
}
export function CollectionTableInternal<M extends { [Key: string]: any },
@@ -89,7 +99,6 @@ export function CollectionTableInternal<M extends { [Key: string]: any },
hoverRow = true
}: CollectionTableProps<M, AdditionalKey>) {
const context = useFireCMSContext();
const dataSource = useDataSource();
const sideEntityController = useSideEntityController();

View File

@@ -56,7 +56,7 @@ export function useSelectionController<M = any>(): SelectionController {
const [selectedEntities, setSelectedEntities] = useState<Entity<M>[]>([]);
const toggleEntitySelection = (entity: Entity<M>) => {
const toggleEntitySelection = useCallback((entity: Entity<M>) => {
let newValue;
if (selectedEntities.map(e => e.id).includes(entity.id)) {
newValue = selectedEntities.filter((item: Entity<M>) => item.id !== entity.id);
@@ -64,9 +64,9 @@ export function useSelectionController<M = any>(): SelectionController {
newValue = [...selectedEntities, entity];
}
setSelectedEntities(newValue);
};
}, [selectedEntities]);
const isEntitySelected = (entity: Entity<M>) => selectedEntities.map(e => e.id).includes(entity.id);
const isEntitySelected = useCallback((entity: Entity<M>) => selectedEntities.map(e => e.id).includes(entity.id), [selectedEntities]);
return {
selectedEntities,
@@ -269,10 +269,10 @@ export function EntityCollectionView<M extends { [Key: string]: any }>({
</div>
), [path, collection]);
const tableRowActionsBuilder = ({
entity,
size
}: { entity: Entity<any>, size: CollectionSize }) => {
const tableRowActionsBuilder = useCallback(({
entity,
size
}: { entity: Entity<any>, size: CollectionSize }) => {
const isSelected = isEntitySelected(entity);
@@ -322,9 +322,9 @@ export function EntityCollectionView<M extends { [Key: string]: any }>({
/>
);
};
}, [selectionController, sideEntityController, collection.permissions, authController, path,]);
const toolbarActionsBuilder = (_: { size: CollectionSize, data: Entity<any>[] }) => {
const toolbarActionsBuilder = useCallback((_: { size: CollectionSize, data: Entity<any>[] }) => {
const addButton = canCreate(collection.permissions, authController, path, context) && onNewClick && (largeLayout ?
<Button
@@ -395,7 +395,7 @@ export function EntityCollectionView<M extends { [Key: string]: any }>({
{addButton}
</>
);
};
}, [selectionController, path, collection, largeLayout]);
return (
<>

View File

@@ -1,4 +1,4 @@
import React, { useRef } from "react";
import React, { useCallback, useEffect, useRef } from "react";
import BaseTable, { Column, ColumnShape } from "react-base-table";
import Measure, { ContentRect } from "react-measure";
import { Box, Paper, Typography } from "@mui/material";
@@ -61,7 +61,17 @@ export function Table<T>({
const tableRef = useRef<BaseTable>(null);
// these refs are a workaround to prevent the scroll jump caused by Firestore
// firing listeners with incomplete data
const scrollRef = useRef<number>(0);
const endReachedTimestampRef = useRef<number>(0);
const classes = useTableStyles();
useEffect(() => {
if (tableRef.current && data) {
tableRef.current.scrollToTop(scrollRef.current);
}
}, [data?.length]);
const onColumnSort = (key: string) => {
@@ -96,11 +106,31 @@ export function Table<T>({
const scrollToTop = () => {
if (tableRef.current) {
scrollRef.current = 0;
tableRef.current.scrollToTop(0);
}
};
const clickRow = (props: { rowData: T; rowIndex: number; rowKey: string ; event: React.SyntheticEvent }) => {
const onScroll = ({ scrollTop, scrollUpdateWasRequested }: {
scrollLeft: number;
scrollTop: number;
horizontalScrollDirection: 'forward' | 'backward';
verticalScrollDirection: 'forward' | 'backward';
scrollUpdateWasRequested: boolean;
}) => {
const prudentTime = Date.now() - endReachedTimestampRef.current > 3000;
if (!scrollUpdateWasRequested && prudentTime) {
scrollRef.current = scrollTop;
}
};
const onEndReachedInternal = () => {
endReachedTimestampRef.current = Date.now();
if (onEndReached)
onEndReached();
};
const clickRow = (props: { rowData: T; rowIndex: number; rowKey: string; event: React.SyntheticEvent }) => {
if (!onRowClick)
return;
onRowClick(props);
@@ -211,10 +241,10 @@ export function Table<T>({
);
}
const onBaseTableColumnResize = ({
column,
width
}: { column: ColumnShape; width: number }) => {
const onBaseTableColumnResize = useCallback(({
column,
width
}: { column: ColumnShape; width: number }) => {
if (onColumnResize) {
onColumnResize({
width,
@@ -222,7 +252,7 @@ export function Table<T>({
column: column as TableColumn<any>
});
}
};
}, [onColumnResize]);
return (
@@ -232,6 +262,7 @@ export function Table<T>({
bounds
onResize={setTableSize}>
{({ measureRef }) => {
return (
<div ref={measureRef}
className={classes.tableContainer}
@@ -249,9 +280,10 @@ export function Table<T>({
ignoreFunctionInColumnCompare={false}
rowHeight={getRowHeight(size)}
ref={tableRef}
onScroll={onScroll}
overscanRowCount={2}
onEndReachedThreshold={PIXEL_NEXT_PAGE_OFFSET}
onEndReached={onEndReached}
onEndReached={onEndReachedInternal}
rowEventHandlers={
{ onClick: clickRow as any }
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
AuthController,
ConfigurationPersistence,
@@ -20,7 +20,7 @@ import {
User
} from "../../models";
import {
getCollectionFromCollections,
getCollectionByPath,
removeInitialAndTrailingSlashes
} from "../util/navigation_utils";
import { getValueInPath, mergeDeep } from "../util/objects";
@@ -77,19 +77,51 @@ export function useBuildNavigationContext<UserType>({
locale,
dataSource,
storageSource
})
.then((result: Navigation) => {
setNavigation(result);
setNavigationLoading(false);
}).catch(setNavigationLoadingError);
}).then((result: Navigation) => {
setNavigation(result);
setNavigationLoading(false);
}).catch(setNavigationLoadingError);
}, [authController.user, authController.canAccessMainView, navigationOrBuilder]);
const getCollectionResolver = <M extends { [Key: string]: any }>(path: string, entityId?: string, collection?:EntityCollection<M>): EntityCollectionResolver<M> => {
const getSchemaOverride = useCallback(<M extends any>(path: string): PartialEntitySchema<M> | undefined => {
if (!userConfigPersistence)
return undefined
const collectionOverride = userConfigPersistence.getCollectionConfig<M>(path);
return collectionOverride?.schema;
}, [userConfigPersistence]);
const buildSchemaResolver = useCallback(<M extends { [Key: string]: any } = any>({
schema,
path
}: { schema: EntitySchema<M>, path: string }): EntitySchemaResolver<M> => ({
entityId,
values,
}: EntitySchemaResolverProps<M>) => {
const schemaOverride = getSchemaOverride<M>(path);
const storedProperties: PartialProperties<M> | undefined = getValueInPath(schemaOverride, "properties");
const properties = computeProperties({
propertiesOrBuilder: schema.properties,
path,
entityId,
values: values ?? schema.defaultValues
});
return {
...schema,
properties: mergeDeep(properties, storedProperties),
originalSchema: schema
};
}, [getSchemaOverride]);
const getCollectionResolver = useCallback(<M extends { [Key: string]: any }>(path: string, entityId?: string, collection?: EntityCollection<M>): EntityCollectionResolver<M> => {
const collections = navigation?.collections;
const baseCollection = collection ?? (collections && getCollectionFromCollections<M>(removeInitialAndTrailingSlashes(path), collections));
const baseCollection = collection ?? (collections && getCollectionByPath<M>(removeInitialAndTrailingSlashes(path), collections));
const collectionOverride = getCollectionOverride(path);
@@ -158,19 +190,26 @@ export function useBuildNavigationContext<UserType>({
return { ...resolvedCollection, ...(result as EntityCollectionResolver<M>) };
};
}, [
navigation,
basePath,
baseCollectionPath,
schemaOverrideHandler,
schemaConfigRecord.current,
buildSchemaResolver
]);
const setOverride = ({
path,
entityId,
schemaConfig,
overrideSchemaRegistry
}: {
path: string,
entityId?: string,
schemaConfig?: Partial<EntityCollectionResolver>
overrideSchemaRegistry?: boolean
}
const setOverride = useCallback(({
path,
entityId,
schemaConfig,
overrideSchemaRegistry
}: {
path: string,
entityId?: string,
schemaConfig?: Partial<EntityCollectionResolver>
overrideSchemaRegistry?: boolean
}
) => {
const key = getSidePanelKey(path, entityId);
@@ -185,9 +224,9 @@ export function useBuildNavigationContext<UserType>({
};
return key;
}
};
}, [schemaConfigRecord.current]);
const removeAllOverridesExcept = (entityRefs: {
const removeAllOverridesExcept = useCallback((entityRefs: {
path: string, entityId?: string
}[]) => {
const keys = entityRefs.map(({
@@ -198,98 +237,38 @@ export function useBuildNavigationContext<UserType>({
if (!keys.includes(currentKey))
delete schemaConfigRecord.current[currentKey];
});
};
}, [schemaConfigRecord.current]);
function buildSchemaResolver<M>({
schema,
path
}: { schema: EntitySchema<M>, path: string }): EntitySchemaResolver<M> {
const isUrlCollectionPath = useCallback(
(path: string): boolean => removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/"),
[fullCollectionPath]);
return ({
entityId,
values,
}: EntitySchemaResolverProps) => {
const schemaOverride = getSchemaOverride<M>(path);
const storedProperties: PartialProperties<M> | undefined = getValueInPath(schemaOverride, "properties");
const properties = computeProperties({
propertiesOrBuilder: schema.properties,
path,
entityId,
values: values ?? schema.defaultValues
});
return {
...schema,
properties: mergeDeep(properties, storedProperties),
originalSchema: schema
};
};
}
async function getNavigation<UserType>({ navigationOrCollections, user, authController, dateTimeFormat, locale, dataSource, storageSource }:
{
navigationOrCollections: Navigation | NavigationBuilder<UserType> | EntityCollection[],
user: User | null,
authController: AuthController<UserType>,
dateTimeFormat?: string,
locale?: Locale,
dataSource: DataSource,
storageSource: StorageSource
}
): Promise<Navigation> {
if (Array.isArray(navigationOrCollections)) {
return {
collections: navigationOrCollections
};
} else if (typeof navigationOrCollections === "function") {
return navigationOrCollections({ user, authController, dateTimeFormat,locale, dataSource, storageSource });
} else {
return navigationOrCollections;
}
}
function isUrlCollectionPath(path: string): boolean {
return removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/");
}
function urlPathToDataPath(path: string): string {
const urlPathToDataPath = useCallback((path: string): string => {
if (path.startsWith(fullCollectionPath))
return path.replace(fullCollectionPath, "");
throw Error("Expected path starting with " + fullCollectionPath);
}
}, [fullCollectionPath]);
function buildUrlCollectionPath(path: string): string {
return `${baseCollectionPath}/${removeInitialAndTrailingSlashes(path)}`;
}
const buildUrlCollectionPath = useCallback((path: string): string => `${baseCollectionPath}/${removeInitialAndTrailingSlashes(path)}`,
[baseCollectionPath]);
function buildCMSUrlPath(path: string): string {
return cleanBasePath ? `/${cleanBasePath}/${removeInitialAndTrailingSlashes(path)}` : `/${path}`;
}
const buildCMSUrlPath = useCallback((path: string): string => cleanBasePath ? `/${cleanBasePath}/${removeInitialAndTrailingSlashes(path)}` : `/${path}`,
[cleanBasePath]);
const onCollectionModifiedForUser = <M extends any>(path: string, partialCollection: PartialEntityCollection<M>) => {
const onCollectionModifiedForUser = useCallback(<M extends any>(path: string, partialCollection: PartialEntityCollection<M>) => {
if (userConfigPersistence) {
const currentStoredConfig = userConfigPersistence.getCollectionConfig(path);
userConfigPersistence.onCollectionModified(path, mergeDeep(currentStoredConfig, partialCollection));
}
}
}, [userConfigPersistence]);
const getCollectionOverride = <M extends any>(path: string): PartialEntityCollection<M> | undefined => {
const getCollectionOverride = useCallback(<M extends any>(path: string): PartialEntityCollection<M> | undefined => {
if (!userConfigPersistence)
return undefined
const dynamicCollectionConfig = { ...userConfigPersistence.getCollectionConfig<M>(path) };
delete dynamicCollectionConfig["schema"];
return dynamicCollectionConfig;
}
const getSchemaOverride = <M extends any>(path: string): PartialEntitySchema<M> | undefined => {
if (!userConfigPersistence)
return undefined
const collectionOverride = userConfigPersistence.getCollectionConfig<M>(path);
return collectionOverride?.schema;
}
}, [userConfigPersistence]);
return {
navigation,
@@ -310,10 +289,48 @@ export function useBuildNavigationContext<UserType>({
};
}
const getNavigation = async <UserType extends any>({
navigationOrCollections,
user,
authController,
dateTimeFormat,
locale,
dataSource,
storageSource
}:
{
navigationOrCollections: Navigation | NavigationBuilder<UserType> | EntityCollection[],
user: User | null,
authController: AuthController<UserType>,
dateTimeFormat?: string,
locale?: Locale,
dataSource: DataSource,
storageSource: StorageSource
}
): Promise<Navigation> => {
if (Array.isArray(navigationOrCollections)) {
return {
collections: navigationOrCollections
};
} else if (typeof navigationOrCollections === "function") {
return navigationOrCollections({
user,
authController,
dateTimeFormat,
locale,
dataSource,
storageSource
});
} else {
return navigationOrCollections;
}
};
export function getSidePanelKey(path: string, entityId?: string) {
if (entityId)
return `${removeInitialAndTrailingSlashes(path)}/${removeInitialAndTrailingSlashes(entityId)}`;
else
return removeInitialAndTrailingSlashes(path);
}
}

View File

@@ -38,7 +38,7 @@ export function getLastSegment(path: string) {
* @param path
* @param collections
*/
export function getCollectionFromCollections<M>(path: string, collections?: EntityCollection[]): EntityCollection<M> | undefined {
export function getCollectionByPath<M>(path: string, collections?: EntityCollection[]): EntityCollection<M> | undefined {
if (!collections)
return undefined;

View File

@@ -16,6 +16,7 @@ import {
ArrayOfReferencesPreview,
ArrayOfStorageComponentsPreview,
ArrayOfStringsPreview,
ArrayOneOfPreview,
ArrayPreview,
ArrayPropertyEnumPreview,
BooleanPreview,
@@ -26,21 +27,14 @@ import {
StorageThumbnail,
StringPreview,
TimestampPreview,
UrlComponentPreview,
ArrayOneOfPreview
UrlComponentPreview
} from "./internal";
import { ErrorView } from "../core/components";
import { PreviewComponentProps } from "./PreviewComponentProps";
import { Markdown } from "./components/Markdown";
/**
* @category Preview components
*/
export function PreviewComponent<T extends CMSType>(props: PreviewComponentProps<T>) {
return <MemoPreviewComponent {...props} />;
}
import deepEqual from "deep-equal";
export function PreviewComponentInternal<T extends CMSType>(props: PreviewComponentProps<T>) {
let content: JSX.Element | any;
@@ -198,5 +192,17 @@ function buildWrongValueType(name: string | undefined, dataType: string, value:
);
}
const MemoPreviewComponent = React.memo<PreviewComponentProps<any>>(PreviewComponentInternal) as React.FunctionComponent<PreviewComponentProps<any>>;
/**
* @category Preview components
*/
export const PreviewComponent = React.memo<PreviewComponentProps<any>>(PreviewComponentInternal, areEqual) as React.FunctionComponent<PreviewComponentProps<any>>;
function areEqual(prevProps: PreviewComponentProps<any>, nextProps: PreviewComponentProps<any>) {
return prevProps.name === nextProps.name
&& prevProps.size === nextProps.size
&& prevProps.height === nextProps.height
&& prevProps.width === nextProps.width
&& deepEqual(prevProps.value, nextProps.value)
;
}

View File

@@ -35,10 +35,6 @@ export type ReferencePreviewProps =
PreviewComponentProps<EntityReference>
& { onHover?: boolean };
/**
* @category Preview components
*/
export const ReferencePreview = React.memo<ReferencePreviewProps>(ReferencePreviewComponent) as React.FunctionComponent<ReferencePreviewProps>;
const useReferenceStyles = makeStyles<Theme, { size: PreviewSize, onHover?: boolean }>((theme: Theme) =>
createStyles({
@@ -90,6 +86,23 @@ const useReferenceStyles = makeStyles<Theme, { size: PreviewSize, onHover?: bool
}
}));
/**
* @category Preview components
*/
export const ReferencePreview = React.memo<ReferencePreviewProps>(ReferencePreviewComponent, areEqual) as React.FunctionComponent<ReferencePreviewProps>;
function areEqual(prevProps: ReferencePreviewProps, nextProps: ReferencePreviewProps) {
return prevProps.name === nextProps.name
&& prevProps.size === nextProps.size
&& prevProps.height === nextProps.height
&& prevProps.width === nextProps.width
&& prevProps.onHover === nextProps.onHover
&& prevProps.value?.id === nextProps.value?.id
&& prevProps.value?.path === nextProps.value?.path
;
}
function ReferencePreviewComponent<M extends { [Key: string]: any }>(
{
value,

View File

@@ -1,4 +1,4 @@
import { getCollectionFromCollections } from "../core/util/navigation_utils";
import { getCollectionByPath } from "../core/util/navigation_utils";
import { siteConfig } from "./test_site_config";
import { EntityCollection } from "../models";
import { getNavigationEntriesFromPathInternal } from "../core/util/navigation_from_path";
@@ -6,37 +6,37 @@ import { getNavigationEntriesFromPathInternal } from "../core/util/navigation_fr
const collectionViews = siteConfig.navigation as EntityCollection[];
it("collection view matches ok", () => {
const collectionViewFromPath = getCollectionFromCollections("products", collectionViews);
const collectionViewFromPath = getCollectionByPath("products", collectionViews);
expect(
collectionViewFromPath && collectionViewFromPath.path
).toEqual("products");
const collectionViewFromPath1 = getCollectionFromCollections("products/pid/locales", collectionViews);
const collectionViewFromPath1 = getCollectionByPath("products/pid/locales", collectionViews);
expect(
collectionViewFromPath1 && collectionViewFromPath1.path
).toEqual("locales");
const collectionViewFromPath2 = getCollectionFromCollections("sites/es/products", collectionViews);
const collectionViewFromPath2 = getCollectionByPath("sites/es/products", collectionViews);
expect(
collectionViewFromPath2 && collectionViewFromPath2.path
).toEqual("sites/es/products");
const collectionViewFromPath3 = getCollectionFromCollections("sites/es/products/pid/locales", collectionViews);
const collectionViewFromPath3 = getCollectionByPath("sites/es/products/pid/locales", collectionViews);
expect(
collectionViewFromPath3 && collectionViewFromPath3.path
).toEqual("locales");
expect(
() => getCollectionFromCollections("products/pid", collectionViews)
() => getCollectionByPath("products/pid", collectionViews)
).toThrow(
"Collection paths must have an odd number of segments: products/pid"
);
expect(
getCollectionFromCollections("products", [])
getCollectionByPath("products", [])
).toEqual(undefined);
const collectionViewFromPath10 = getCollectionFromCollections("products/id/subcollection_inline", collectionViews);
const collectionViewFromPath10 = getCollectionByPath("products/id/subcollection_inline", collectionViews);
expect(
collectionViewFromPath10 && collectionViewFromPath10.path
).toEqual("products/id/subcollection_inline");