mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-06 21:40:23 +08:00
feat(editor-sdk): breadcrumb widget
This commit is contained in:
parent
2f4205bc8d
commit
46e8bf43e4
@ -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}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
245
packages/editor-sdk/src/components/Widgets/BreadcrumbWidget.tsx
Normal file
245
packages/editor-sdk/src/components/Widgets/BreadcrumbWidget.tsx
Normal 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);
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -38,6 +38,7 @@ export enum CoreWidgetName {
|
||||
Spec = 'spec',
|
||||
StringField = 'string',
|
||||
UnsupportedField = 'unsupported',
|
||||
Breadcrumb = 'breadcrumb',
|
||||
}
|
||||
|
||||
export enum StyleWidgetName {
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user