Merge branch 'main' into yz-patch

* main:
  fix(editorMask): mask does'nt work because onHTMLElementsUpdated  doesn't run in init
  feat(PreviewModal): fix the length of the component in the PreviewModal exceeds the modal
  fix(ArrayTable): fix the problem of length exceeding the table
  feat(Select): add filter by text
  chore: fix typo
  feat(utilMethod): add spec to utilMethod

# Conflicts:
#	packages/core/src/index.ts
#	packages/editor-sdk/src/components/Widgets/EventWidget.tsx
This commit is contained in:
Bowen Tan 2022-05-31 17:05:58 +08:00
commit 744e75cd03
13 changed files with 214 additions and 101 deletions

View File

@ -106,6 +106,10 @@ export const Select = implementRuntimeComponent({
value={value}
{...cProps}
showSearch={showSearch}
filterOption={(inputValue, option) =>
option.props.value.toLowerCase().indexOf(inputValue.toLowerCase()) >= 0 ||
option.props.children.toLowerCase().indexOf(inputValue.toLowerCase()) >= 0
}
dropdownRender={menu => {
return (
<div className={css(customStyle?.dropdownRenderWrap)}>

View File

@ -1,6 +1,6 @@
import { Type, Static, TProperties, TObject } from '@sinclair/typebox';
import { createStandaloneToast } from '@chakra-ui/react';
import { UtilMethod } from '@sunmao-ui/runtime';
import { implementUtilMethod } from '@sunmao-ui/runtime';
const ToastPosition = Type.KeyOf(
Type.Object({
@ -49,7 +49,7 @@ export const ToastOpenParameterSpec = Type.Object({
export const ToastCloseParameterSpec = Type.Object({
id: Type.String(),
positions: Type.Array(ToastPosition, {
defaultValue: []
defaultValue: [],
}),
});
@ -70,37 +70,46 @@ const pickProperty = <T, U extends Record<string, any>>(
export default function ToastUtilMethodFactory() {
let toast: ReturnType<typeof createStandaloneToast> | undefined;
const toastOpen: UtilMethod<typeof ToastOpenParameterSpec> = {
name: 'toast.open',
method(parameters) {
if (!toast) {
toast = createStandaloneToast();
}
if (parameters) {
toast(pickProperty(ToastOpenParameterSpec, parameters));
}
const toastOpen = implementUtilMethod({
version: 'chakra_ui/v1',
metadata: {
name: 'openToast',
},
parameters: ToastOpenParameterSpec,
};
spec: {
parameters: ToastOpenParameterSpec,
},
})(parameters => {
if (!toast) {
toast = createStandaloneToast();
}
if (parameters) {
toast(pickProperty(ToastOpenParameterSpec, parameters));
}
});
const toastClose: UtilMethod<typeof ToastCloseParameterSpec> = {
name: 'toast.close',
method(parameters) {
if (!toast) {
return;
}
if (!parameters) {
toast.closeAll();
} else {
const closeParameters = pickProperty(ToastCloseParameterSpec, parameters);
if (closeParameters.id !== undefined) {
toast.close(closeParameters.id);
} else {
toast.closeAll(closeParameters);
}
}
const toastClose = implementUtilMethod({
version: 'chakra_ui/v1',
metadata: {
name: 'closeToast',
},
parameters: ToastCloseParameterSpec,
};
spec: {
parameters: ToastCloseParameterSpec,
},
})(parameters => {
if (!toast) {
return;
}
if (!parameters) {
toast.closeAll();
} else {
const closeParameters = pickProperty(ToastCloseParameterSpec, parameters);
if (closeParameters.id !== undefined) {
toast.close(closeParameters.id);
} else {
toast.closeAll(closeParameters);
}
}
});
return [toastOpen, toastClose];
}

View File

@ -6,3 +6,4 @@ export * from './method';
export * from './module';
export * from './version';
export * from './slot';
export * from './utilMethod';

View File

@ -0,0 +1,28 @@
import { JSONSchema7 } from 'json-schema';
import { Metadata } from './metadata';
import { parseVersion, type Version } from './version';
export type UtilMethodSpec = {
parameters: JSONSchema7;
};
export type UtilMethod = {
version: string;
kind: 'UtilMethod';
metadata: Metadata;
spec: UtilMethodSpec;
};
export type RuntimeUtilMethod = UtilMethod & {
parsedVersion: Version;
};
export type CreateUtilMethodOptions = Omit<UtilMethod, 'kind'>;
export function createUtilMethod(options: CreateUtilMethodOptions): RuntimeUtilMethod {
return {
...options,
kind: 'UtilMethod',
parsedVersion: parseVersion(options.version),
};
}

View File

@ -24,6 +24,12 @@ const TableRowStyle = css`
padding-bottom: var(--chakra-space-1);
border-bottom-width: 1px;
border-color: var(--chakra-colors-gray-100);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
& > th:last-child {
width: 76px;
}
`;
@ -99,7 +105,7 @@ export const ArrayTable: React.FC<ArrayTableProps> = props => {
return (
<div className={TableWrapperStyle}>
<Table size="sm">
<Table size="sm" sx={{ tableLayout: 'fixed' }}>
<Thead>
<Tr className={TableRowStyle}>
<Th width="24px" />
@ -109,7 +115,7 @@ export const ArrayTable: React.FC<ArrayTableProps> = props => {
return <Th key={key}>{title}</Th>;
})}
<Th key="button" display="flex" justifyContent="end">
<Th key="button">
<IconButton
aria-label="add"
icon={<AddIcon />}

View File

@ -19,8 +19,8 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
props => {
const { value, path, level, component, spec, services, onChange } = props;
const { registry, editorStore, appModelManager } = services;
const { utilMethods } = registry;
const { components } = editorStore;
const utilMethods = useMemo(() => registry.getAllUtilMethods(), [registry]);
const [methods, setMethods] = useState<string[]>([]);
const formik = useFormik({
@ -29,23 +29,26 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
onChange(values);
},
});
const findMethodsByComponent = (component?: ComponentSchema) => {
if (!component) {
return [];
}
const findMethodsByComponent = useCallback(
(component?: ComponentSchema) => {
if (!component) {
return [];
}
const componentMethods = Object.entries(
registry.getComponentByType(component.type).spec.methods
).map(([name, parameters]) => ({
name,
parameters,
}));
const traitMethods = component.traits
.map(trait => registry.getTraitByType(trait.type).spec.methods)
.flat();
const componentMethods = Object.entries(
registry.getComponentByType(component.type).spec.methods
).map(([name, parameters]) => ({
name,
parameters,
}));
const traitMethods = component.traits
.map(trait => registry.getTraitByType(trait.type).spec.methods)
.flat();
return ([] as any[]).concat(componentMethods, traitMethods);
};
return ([] as any[]).concat(componentMethods, traitMethods);
},
[registry]
);
const eventTypes = useMemo(() => {
return registry.getComponentByType(component.type).spec.events;
@ -55,15 +58,14 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
[formik.values.method.parameters]
);
const paramsSpec = useMemo(() => {
const { values } = formik;
const methodName = values.method.name;
const methodType = formik.values.method.name;
let spec: WidgetProps['spec'] = Type.Record(Type.String(), Type.String());
if (methodName) {
if (methodType) {
if (value.componentId === GLOBAL_UTIL_METHOD_ID) {
const targetMethod = utilMethods.get(methodName);
const targetMethod = registry.getUtilMethodByType(methodType)!;
spec = targetMethod?.parameters;
spec = targetMethod.spec.parameters;
} else {
const targetComponent = appModelManager.appModel.getComponentById(
value.componentId
@ -79,20 +81,26 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
}
return spec;
}, [formik.values.method]);
}, [
formik.values.method.name,
registry,
appModelManager,
value.componentId,
findMethodsByComponent,
]);
const params = useMemo(() => {
const params: Record<string, string> = {};
const { values } = formik;
const parameters = formik.values.method.parameters;
for (const key in paramsSpec?.properties ?? {}) {
const defaultValue = (paramsSpec?.properties?.[key] as WidgetProps['spec'])
.defaultValue;
params[key] = values.method.parameters?.[key] ?? defaultValue ?? '';
params[key] = parameters?.[key] ?? defaultValue ?? '';
}
return params;
}, [formik.values.method.name]);
}, [formik.values.method.parameters, paramsSpec?.properties]);
const parametersPath = useMemo(() => path.concat('method', 'parameters'), [path]);
const parametersSpec = useMemo(
() => mergeWidgetOptionsIntoSpec(paramsSpec, { onlySetValue: true }),
@ -107,7 +115,11 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
const updateMethods = useCallback(
(componentId: string) => {
if (componentId === GLOBAL_UTIL_METHOD_ID) {
setMethods(Array.from(utilMethods.keys()));
setMethods(
utilMethods.map(
utilMethod => `${utilMethod.version}/${utilMethod.metadata.name}`
)
);
} else {
const component = components.find(c => c.id === componentId);
@ -120,7 +132,7 @@ export const EventWidget: React.FC<WidgetProps<EventWidgetOptionsType>> = observ
}
}
},
[components, registry]
[components, utilMethods, findMethodsByComponent]
);
useEffect(() => {

View File

@ -54,6 +54,7 @@ export class EditorMaskManager {
this.observeIntersection();
this.observeResize();
this.refreshElementIdMap();
// listen to the DOM elements' mount and unmount events
// TODO: This is not very accurate, because sunmao runtime 'didDOMUpdate' hook is not accurate.
// We will refactor the 'didDOMUpdate' hook with components' life cycle in the future.
@ -125,14 +126,7 @@ export class EditorMaskManager {
private onHTMLElementsUpdated = () => {
this.observeIntersection();
this.observeResize();
// generate elementIdMap, this only aim to improving the performance of refreshHoverElement method
const elementIdMap = new Map<Element, string>();
this.eleMap.forEach((ele, id) => {
elementIdMap.set(ele, id);
});
this.elementIdMap = elementIdMap;
this.refreshElementIdMap();
this.refreshHoverElement();
this.refreshMaskPosition();
};
@ -156,6 +150,16 @@ export class EditorMaskManager {
};
}
private refreshElementIdMap() {
// generate elementIdMap, this only aim to improving the performance of refreshHoverElement method
const elementIdMap = new Map<Element, string>();
this.eleMap.forEach((ele, id) => {
elementIdMap.set(ele, id);
});
this.elementIdMap = elementIdMap;
}
private refreshHoverElement() {
const hoverElement = document.elementFromPoint(...this.mousePosition);
if (!hoverElement) return;

View File

@ -12,9 +12,10 @@ export const GeneralModal: React.FC<{
onClose: () => void;
title: string;
size?: string;
}> = ({ title, onClose, size = 'full', children }) => {
scrollBehavior?: 'inside' | 'outside';
}> = ({ title, onClose, size = 'full', children, scrollBehavior = 'inside' }) => {
return (
<Modal onClose={onClose} size={size} isOpen>
<Modal onClose={onClose} scrollBehavior={scrollBehavior} size={size} isOpen>
<ModalOverlay />
<ModalContent>
<ModalHeader>{title}</ModalHeader>

View File

@ -28,9 +28,10 @@ import {
ImplementedRuntimeTraitFactory,
ImplementedRuntimeTrait,
ImplementedRuntimeModule,
UtilMethodFactory,
UIServices,
} from '../types';
import { UtilMethod, UtilMethodFactory } from '../types/utilMethod';
import { ImplementedUtilMethod } from '../types/utilMethod';
import { UtilMethodManager } from './UtilMethodManager';
export type SunmaoLib = {
@ -53,7 +54,7 @@ export class Registry {
components = new Map<string, Map<string, AnyImplementedRuntimeComponent>>();
traits = new Map<string, Map<string, ImplementedRuntimeTrait>>();
modules = new Map<string, Map<string, ImplementedRuntimeModule>>();
utilMethods = new Map<string, UtilMethod<any>>();
utilMethods = new Map<string, Map<string, ImplementedUtilMethod>>();
private services: UIServices;
constructor(
@ -180,14 +181,42 @@ export class Registry {
this.traits = new Map<string, Map<string, ImplementedRuntimeTrait>>();
}
registerUtilMethod<T>(m: UtilMethod<T>) {
if (this.utilMethods.get(m.name)) {
throw new Error(`Already has utilMethod ${m.name} in this registry.`);
registerUtilMethod<T>(m: ImplementedUtilMethod<T>) {
if (this.utilMethods.get(m.version)?.get(m.metadata.name)) {
throw new Error(
`Already has utilMethod ${m.version}/${m.metadata.name} in this registry.`
);
}
this.utilMethods.set(m.name, m);
if (!this.utilMethods.has(m.version)) {
this.utilMethods.set(m.version, new Map());
}
this.utilMethods.get(m.version)!.set(m.metadata.name, m);
this.utilMethodManager.listenUtilMethod(m, this.services);
}
getUtilMethod(version: string, name: string): ImplementedUtilMethod<any> | null {
return this.utilMethods.get(version)?.get(name) || null;
}
getUtilMethodByType(type: string): ImplementedUtilMethod<any> | null {
const { version, name } = parseType(type);
return this.getUtilMethod(version, name);
}
getAllUtilMethods(): ImplementedUtilMethod[] {
const res: ImplementedUtilMethod[] = [];
for (const version of this.utilMethods.values()) {
for (const utilMethod of version.values()) {
res.push(utilMethod);
}
}
return res;
}
installLib(lib: SunmaoLib) {
lib.components?.forEach(c => this.registerComponent(c));
lib.traits?.forEach(t => this.registerTrait(t));

View File

@ -1,16 +1,19 @@
import { GLOBAL_MODULE_ID, GLOBAL_UTIL_METHOD_ID } from '../constants';
import { ApiService } from './apiService';
import { UtilMethod, UIServices } from '../types';
import { ImplementedUtilMethod, UIServices } from '../types';
export class UtilMethodManager {
constructor(private apiService: ApiService) {
this.listenSystemMethods();
}
listenUtilMethod<T>(utilMethod: UtilMethod<T>, services: UIServices) {
listenUtilMethod<T>(utilMethod: ImplementedUtilMethod<T>, services: UIServices) {
this.apiService.on('uiMethod', ({ componentId, name, parameters }) => {
if (componentId === GLOBAL_UTIL_METHOD_ID && name === utilMethod.name) {
utilMethod.method(parameters, services);
if (
componentId === GLOBAL_UTIL_METHOD_ID &&
name === `${utilMethod.version}/${utilMethod.metadata.name}`
) {
utilMethod.impl(parameters, services);
}
});
}

View File

@ -1,11 +1,10 @@
import { Static } from '@sinclair/typebox';
import { JSONSchema7 } from 'json-schema';
import { UIServices } from './application';
import { RuntimeUtilMethod } from '@sunmao-ui/core';
export interface UtilMethod<T extends JSONSchema7> {
name: string;
method: (parameters: Static<T>, services: UIServices) => void;
parameters: T;
}
export type UtilMethodImpl<T = any> = (parameters: T, services: UIServices) => void;
export type UtilMethodFactory = () => UtilMethod<any>[];
export type ImplementedUtilMethod<T = any> = RuntimeUtilMethod & {
impl: UtilMethodImpl<T>;
};
export type UtilMethodFactory = () => ImplementedUtilMethod<any>[];

View File

@ -1,20 +1,22 @@
import { Type } from '@sinclair/typebox';
import { UtilMethod } from '../types/utilMethod';
import { implementUtilMethod } from '../utils/buildKit';
const ScrollToComponentMethodParameters = Type.Object({
componentId: Type.String(),
});
const ScrollToComponentMethod: UtilMethod<typeof ScrollToComponentMethodParameters> = {
name: 'scrollToComponent',
method(parameters, services) {
if (!parameters) return;
const ele = services.eleMap.get(parameters?.componentId);
if (ele) {
ele.scrollIntoView({ behavior: 'smooth' });
}
export default implementUtilMethod({
version: 'core/v1',
metadata: {
name: 'scrollToComponent',
},
parameters: ScrollToComponentMethodParameters,
};
export default ScrollToComponentMethod;
spec: {
parameters: ScrollToComponentMethodParameters,
},
})((parameters, services) => {
if (!parameters) return;
const ele = services.eleMap.get(parameters?.componentId);
if (ele) {
ele.scrollIntoView({ behavior: 'smooth' });
}
});

View File

@ -5,12 +5,16 @@ import {
createTrait,
CreateTraitOptions,
TraitSpec,
createUtilMethod,
CreateUtilMethodOptions,
} from '@sunmao-ui/core';
import {
ComponentImpl,
ImplementedRuntimeComponent,
TraitImplFactory,
ImplementedRuntimeTraitFactory,
UtilMethodImpl,
ImplementedUtilMethod,
} from '../types';
type ToMap<U> = {
@ -55,3 +59,14 @@ export function implementRuntimeTrait<T extends CreateTraitOptions>(
factory,
});
}
export function implementUtilMethod<T extends CreateUtilMethodOptions>(
options: T
): (
impl: UtilMethodImpl<Static<T['spec']['parameters']>>
) => ImplementedUtilMethod<Static<T['spec']['parameters']>> {
return impl => ({
...createUtilMethod(options),
impl,
});
}