feat(editor-sdk): breadcrumb widget

This commit is contained in:
Bowen Tan 2023-02-10 14:37:17 +08:00
parent 2f4205bc8d
commit 46e8bf43e4
6 changed files with 294 additions and 19 deletions

View File

@ -1,7 +1,7 @@
import React, { useMemo, useCallback } from 'react';
import { css } from '@emotion/css';
import { IconButton, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { AddIcon, SettingsIcon } from '@chakra-ui/icons';
import { generateDefaultValueFromSpec, isJSONSchema } from '@sunmao-ui/shared';
import { JSONSchema7 } from 'json-schema';
import { ArrayButtonGroup } from './ArrayButtonGroup';
@ -35,6 +35,9 @@ const TableRowStyle = css`
type ArrayTableProps = WidgetProps<'core/v1/array'> & {
itemSpec: JSONSchema7;
// These 2 properties are for BreadcrumbWidget
disablePopover?: boolean;
onClickSettings?: (i: number) => void;
};
type RowProps = ArrayTableProps & {
itemValue: any;
@ -44,8 +47,19 @@ type RowProps = ArrayTableProps & {
const DEFAULT_KEYS = ['index'];
const TableRow: React.FC<RowProps> = props => {
const { value, itemSpec, spec, level, path, children, itemValue, itemIndex, onChange } =
props;
const {
value,
itemSpec,
spec,
level,
path,
children,
itemValue,
itemIndex,
onChange,
disablePopover,
onClickSettings,
} = props;
const {
expressionOptions,
displayedKeys = [],
@ -78,20 +92,31 @@ const TableRow: React.FC<RowProps> = props => {
[itemIndex, onChange, value]
);
const popoverWidget = (
<PopoverWidget
{...props}
value={itemValue}
spec={mergedSpec as WidgetProps<PopoverWidgetType>}
path={nextPath}
level={level + 1}
onChange={onPopoverWidgetChange}
>
{typeof children === 'function' ? children(props, itemValue, itemIndex) : null}
</PopoverWidget>
);
const settingsButton = (
<IconButton
size="xs"
variant="ghost"
aria-label="Setting"
icon={<SettingsIcon />}
onClick={() => onClickSettings?.(itemIndex)}
/>
);
return (
<Tr className={TableRowStyle}>
<Td key="setting">
<PopoverWidget
{...props}
value={itemValue}
spec={mergedSpec as WidgetProps<PopoverWidgetType>}
path={nextPath}
level={level + 1}
onChange={onPopoverWidgetChange}
>
{typeof children === 'function' ? children(props, itemValue, itemIndex) : null}
</PopoverWidget>
</Td>
<Td key="setting">{disablePopover ? settingsButton : popoverWidget}</Td>
{keys.map((key: string) => {
const keyValue = get(itemValue, key);
const propertyValue = key === 'index' ? keyValue ?? itemIndex : keyValue;
@ -114,7 +139,7 @@ const TableRow: React.FC<RowProps> = props => {
};
export const ArrayTable: React.FC<ArrayTableProps> = props => {
const { value, itemSpec, spec, onChange } = props;
const { value, itemSpec, spec, onChange, disablePopover, onClickSettings } = props;
const { displayedKeys = [] } = spec.widgetOptions || {};
const keys = displayedKeys.length ? displayedKeys : ['index'];
@ -151,6 +176,8 @@ export const ArrayTable: React.FC<ArrayTableProps> = props => {
key={itemIndex}
itemValue={itemValue}
itemIndex={itemIndex}
disablePopover={disablePopover}
onClickSettings={onClickSettings}
/>
))
) : (

View File

@ -0,0 +1,245 @@
import React, { useEffect, useRef, useState } from 'react';
import {
CORE_VERSION,
CoreWidgetName,
generateDefaultValueFromSpec,
} from '@sunmao-ui/shared';
import { Static, Type } from '@sinclair/typebox';
import { JSONSchema7 } from 'json-schema';
import { get, set } from 'lodash';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
Button,
VStack,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
Portal,
Divider,
} from '@chakra-ui/react';
import { ArrayTable } from '../Form/ArrayTable';
import { implementWidget } from '../../utils/widget';
import { WidgetProps } from '../../types/widget';
import { ComponentFormElementId } from '../../constants';
import { SpecWidget, DefaultTemplate } from './SpecWidget';
export type BreadcrumbWidgetType = `${typeof CORE_VERSION}/${CoreWidgetName.Breadcrumb}`;
const BreadcrumbWidgetOption = Type.Object({
appendToBody: Type.Optional(Type.Boolean()),
appendToParent: Type.Optional(Type.Boolean()),
});
declare module '../../types/widget' {
interface WidgetOptionsMap {
'core/v1/breadcrumb': Static<typeof BreadcrumbWidgetOption>;
}
}
export const BreadcrumbWidget: React.FC<WidgetProps<BreadcrumbWidgetType>> = props => {
const { spec, value, onChange } = props;
const [currentPath, setCurrentPath] = useState<string[]>([]);
const currentSpec = getJSONSchemaByPath(spec as JSONSchema7, currentPath.join('.'));
let currentValue = currentPath.length ? get(value, currentPath.join('.')) : value;
const containerRef = useRef(document.getElementById(ComponentFormElementId));
useEffect(() => {
containerRef.current = document.getElementById(ComponentFormElementId);
}, []);
if (!currentSpec) {
return <span>Fail to find spec of: {currentPath.join('.')}.</span>;
}
if (!currentValue) {
// Gengerate a default value if it is undefined.
currentValue = generateDefaultValueFromSpec(currentSpec);
}
let content;
if (currentSpec.type === 'array' && currentSpec.items) {
// Only when breadcrumbWidget is add to array type directly, render this
const itemSpec = Array.isArray(currentSpec.items)
? currentSpec.items[0]
: currentSpec.items;
content = (
<ArrayTable
{...props}
value={currentValue}
itemSpec={itemSpec as JSONSchema7}
disablePopover
onClickSettings={i => {
setCurrentPath(prev => [...prev, `${i}`]);
}}
onChange={v => {
onChange(v);
}}
/>
);
} else if (currentSpec.type === 'object' && currentSpec.properties) {
// render object fields
content = Object.keys(currentSpec.properties).map(key => {
const childSpec = currentSpec.properties![key] as JSONSchema7;
switch (childSpec.type) {
case 'object':
const onClickEdit = () => {
setCurrentPath(prev => [...prev, key]);
};
return (
<DefaultTemplate
key={key}
label={childSpec.title || key}
description={childSpec.description}
displayLabel={true}
codeMode={false}
isExpression={false}
>
{{
content: (
<Button size="xs" onClick={onClickEdit}>
Edit
</Button>
),
}}
</DefaultTemplate>
);
case 'array':
const itemSpec = Array.isArray(childSpec.items)
? childSpec.items[0]
: childSpec.items;
return (
<DefaultTemplate
key={key}
label={childSpec.title || key}
description={childSpec.description}
displayLabel={true}
codeMode={false}
>
{{
content: (
<ArrayTable
{...props}
value={currentValue[key]}
itemSpec={itemSpec as JSONSchema7}
disablePopover
onClickSettings={i => {
setCurrentPath(prev => [...prev, key, `${i}`]);
}}
onChange={v => {
const newValue = set(value, [...currentPath, key].join('.'), v);
onChange(newValue);
}}
/>
),
}}
</DefaultTemplate>
);
default:
return (
<SpecWidget
key={key}
{...props}
value={currentValue[key]}
spec={{ ...childSpec, title: childSpec.title ?? key }}
onChange={v => {
const newValue = set(value, [...currentPath, key].join('.'), v);
onChange(newValue);
}}
/>
);
}
});
} else {
return (
<span>
Fail to find fields spec of: {currentPath.join('.')}. It is not an object or array
type.
</span>
);
}
const bread = (
<>
<Breadcrumb marginBottom="2" separator=">" width="full">
<BreadcrumbItem key="root">
<BreadcrumbLink onClick={() => setCurrentPath([])}>/</BreadcrumbLink>
</BreadcrumbItem>
{currentPath.map((path, i) => {
return (
<BreadcrumbItem key={`${path}-${i}`}>
<BreadcrumbLink onClick={() => setCurrentPath(currentPath.slice(0, i + 1))}>
{path}
</BreadcrumbLink>
</BreadcrumbItem>
);
})}
</Breadcrumb>
<Divider />
</>
);
const popoverContent = (
<PopoverContent>
<PopoverBody maxHeight="75vh" overflow="auto" paddingBottom="96px">
<VStack justifyContent="start" alignItems="start" spacing="0">
{currentPath.length > 0 ? bread : undefined}
{content}
</VStack>
</PopoverBody>
</PopoverContent>
);
return (
<Popover isLazy placement="left">
<PopoverTrigger>
<Button size="xs">Edit</Button>
</PopoverTrigger>
{spec.widgetOptions?.appendToParent ? (
popoverContent
) : (
<Portal
containerRef={spec.widgetOptions?.appendToBody ? undefined : containerRef}
>
{popoverContent}
</Portal>
)}
</Popover>
);
};
function getJSONSchemaByPath(schema: JSONSchema7, path: string) {
const keys = path.split('.');
let result: JSONSchema7 | undefined = schema;
function getChild(key: string, s: JSONSchema7) {
if (!key) return s;
switch (s.type) {
case 'object':
return s.properties ? s.properties[key] : undefined;
case 'array':
return Array.isArray(s.items) ? s.items[0] : s.items;
}
}
for (const k of keys) {
if (!result) break;
result = getChild(k, result) as JSONSchema7;
}
return result;
}
export default implementWidget<BreadcrumbWidgetType>({
version: CORE_VERSION,
metadata: {
name: CoreWidgetName.Breadcrumb,
},
spec: {
options: BreadcrumbWidgetOption,
},
})(BreadcrumbWidget);

View File

@ -109,7 +109,7 @@ const descriptionStyle = css`
}
`;
const DefaultTemplate: React.FC<TemplateProps> = props => {
export const DefaultTemplate: React.FC<TemplateProps> = props => {
const {
id,
label,
@ -261,7 +261,6 @@ export const SpecWidget: React.FC<Props> = props => {
} else {
Component = ExpressionWidget;
}
return (
<DefaultTemplate
label={label}

View File

@ -14,6 +14,7 @@ import moduleWidgetSpec from './ModuleWidget';
import recordWidgetSpec from './RecordField';
import eventWidgetSpec from './EventWidget';
import popoverWidgetSpec from './PopoverWidget';
import breadcrumbWidgetSpec from './BreadcrumbWidget';
import fetchWidgetSpec from './FetchWidget';
import sizeWidgetSpec from './StyleWidgets/SizeWidget';
import fontWidgetSpec from './StyleWidgets/FontWidget';
@ -35,6 +36,7 @@ export * from './ModuleWidget';
export * from './RecordField';
export * from './EventWidget';
export * from './PopoverWidget';
export * from './BreadcrumbWidget';
export * from './FetchWidget';
export * from './StyleWidgets/SizeWidget';
export * from './StyleWidgets/FontWidget';
@ -58,6 +60,7 @@ export const widgets: ImplementedWidget<any>[] = [
recordWidgetSpec,
eventWidgetSpec,
popoverWidgetSpec,
breadcrumbWidgetSpec,
fetchWidgetSpec,
sizeWidgetSpec,
fontWidgetSpec,

View File

@ -38,6 +38,7 @@ export enum CoreWidgetName {
Spec = 'spec',
StringField = 'string',
UnsupportedField = 'unsupported',
Breadcrumb = 'breadcrumb',
}
export enum StyleWidgetName {

View File

@ -2833,7 +2833,7 @@
"@emotion/styled@^11.0.0", "@emotion/styled@^11.8.1":
version "11.10.5"
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79"
resolved "https://registry.npmmirror.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79"
integrity sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw==
dependencies:
"@babel/runtime" "^7.18.3"