feat(DataSource): support create component datasource

This commit is contained in:
Bowen Tan 2022-12-06 16:32:42 +08:00
parent 9418334d9e
commit 84b11f115a
14 changed files with 166 additions and 265 deletions

View File

@ -39,7 +39,7 @@ export function createTrait(options: CreateTraitOptions): RuntimeTrait {
kind: 'Trait' as any,
parsedVersion: parseVersion(options.version),
metadata: {
name: options.metadata.name,
...options.metadata,
description: options.metadata.description || '',
annotations: options.metadata.annotations || {},
},

View File

@ -22,7 +22,7 @@ declare module '../../types/widget' {
export const FetchWidget: React.FC<WidgetProps<FetchWidgetType>> = props => {
const { value, onChange, component, services } = props;
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(false);
return (
<Box>
<Button onClick={() => setIsOpen(true)}>Edit In Modal</Button>

View File

@ -1,25 +1,22 @@
import React, { useState, useMemo, useEffect } from 'react';
import React from 'react';
import { ComponentSchema } from '@sunmao-ui/core';
import {
Box,
Text,
Input,
AccordionItem,
AccordionButton,
AccordionIcon,
AccordionPanel,
Tag,
HStack,
} from '@chakra-ui/react';
import { EditorServices } from '../../types';
import { ComponentSchema } from '@sunmao-ui/core';
import { watch } from '@sunmao-ui/runtime';
import { ComponentNode } from '../StructureTree/ComponentNode';
interface Props {
datas: ComponentSchema[];
dataSources: ComponentSchema[];
title: string;
filterPlaceholder: string;
emptyPlaceholder: string;
services: EditorServices;
type: string;
}
const COLOR_MAP = {
@ -37,51 +34,37 @@ const STATE_MAP: Record<string, string> = {
object: 'Object',
};
export const DataSourceNode: React.FC<Props> = props => {
const { datas = [], filterPlaceholder, emptyPlaceholder, title, services } = props;
export const DataSourceGroup: React.FC<Props> = props => {
const { dataSources = [], title, services, type } = props;
const { stateManager, editorStore } = services;
const { store } = stateManager;
const [search, setSearch] = useState('');
const [reactiveStore, setReactiveStore] = useState<Record<string, any>>({ ...store });
const list = useMemo(
() => datas.filter(({ id }) => id.includes(search)),
[search, datas]
);
useEffect(() => {
const stop = watch(store, newValue => {
setReactiveStore({ ...newValue });
});
return stop;
}, [store]);
const StateItems = () => (
<>
{list.map(state => {
{dataSources.map(dataSource => {
let tag = '';
const trait = state.traits.find(({ type }) => type === `core/v1/fetch`);
const trait = dataSource.traits.find(({ type }) => type === `core/v1/fetch`);
if (trait?.properties) {
tag = (trait.properties.method as string).toUpperCase();
tag = ((trait.properties.config as any).method as string).toUpperCase();
} else {
tag = Array.isArray(reactiveStore[state.id]?.value)
tag = Array.isArray(store[dataSource.id]?.value)
? 'Array'
: STATE_MAP[typeof reactiveStore[state.id]?.value] ?? 'Any';
: STATE_MAP[typeof store[dataSource.id]?.value] ?? 'Any';
}
return (
<ComponentNode
id={state.id}
key={state.id}
component={state}
id={dataSource.id}
key={dataSource.id}
component={dataSource}
parentId={null}
slot={null}
onSelectComponent={editorStore.setSelectedComponentId}
services={services}
droppable={false}
depth={0}
isSelected={editorStore.selectedComponent?.id === state.id}
isSelected={editorStore.selectedComponent?.id === dataSource.id}
isExpanded={false}
onToggleExpand={() => undefined}
shouldShowSelfSlotName={false}
@ -108,23 +91,15 @@ export const DataSourceNode: React.FC<Props> = props => {
return (
<AccordionItem>
<h2>
<AccordionButton borderBottom="solid" borderColor="inherit">
<Box flex="1" textAlign="left">
{title}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb="4" padding="0">
<Input
placeholder={filterPlaceholder}
value={search}
onChange={e => {
setSearch(e.target.value);
}}
/>
{list.length ? <StateItems /> : <Text padding="2">{emptyPlaceholder}</Text>}
<AccordionButton justifyContent="space-between">
<HStack>
{type === 'component' ? <Tag colorScheme="blue">C</Tag> : undefined}
<Text fontWeight="bold">{title}</Text>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel padding="0">
{dataSources.length ? <StateItems /> : <Text padding="2">Empty</Text>}
</AccordionPanel>
</AccordionItem>
);

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import {
VStack,
Flex,
@ -10,65 +10,114 @@ import {
MenuList,
IconButton,
Accordion,
MenuGroup,
} from '@chakra-ui/react';
import { AddIcon, ChevronDownIcon } from '@chakra-ui/icons';
import { DataSourceNode } from './DataSourceNode';
import { DataSourceGroup } from './DataSourceGroup';
import { EditorServices } from '../../types';
import { DataSourceType } from '../../constants/dataSource';
import { groupBy } from 'lodash';
import { genOperation } from '../../operations';
import { generateDefaultValueFromSpec } from '@sunmao-ui/shared';
import { JSONSchema7 } from 'json-schema';
import { ToolMenuTabs } from '../../constants/enum';
interface Props {
active: string;
services: EditorServices;
}
const DATASOURCE_TYPES = Object.values(DataSourceType);
export const DataSourceList: React.FC<Props> = props => {
const { services } = props;
const { editorStore } = services;
const group = groupBy(editorStore.dataSources, c => c.traits[0]?.type);
// const NORMAL_DATASOURCES = DATA_DATASOURCES.map(item => ({
// ...item,
// title: item.type,
// datas: editorStore.dataSources[item.type],
// }));
const NORMAL_DATASOURCES = Object.keys(group).map(type => {
const { editorStore, eventBus, registry } = services;
const { dataSources, setSelectedComponentId, setToolMenuTab } = editorStore;
const tDataSources = dataSources.filter(ds => ds.type === 'core/v1/dummy');
const cDataSources = dataSources.filter(ds => ds.type !== 'core/v1/dummy');
const cdsMap = groupBy(cDataSources, c => c.type);
const tdsMap = groupBy(tDataSources, c => c.traits[0]?.type);
const cdsGroups = Object.keys(cdsMap).map(type => {
return {
title: type,
datas: group[type],
dataSources: cdsMap[type],
type: 'component',
};
});
const onMenuItemClick = (type: DataSourceType) => {
editorStore.createDataSource(
type,
type === DataSourceType.API ? {} : { key: 'value' }
);
editorStore.setSelectedComponentId('');
};
// const onApiItemClick = (api: ComponentSchema) => {
// editorStore.setActiveDataSourceId(api.id);
// editorStore.setSelectedComponentId(api.id);
// };
// const onDataSourceItemClick = (dataSource: ComponentSchema) => {
// editorStore.setActiveDataSourceId(dataSource.id);
// editorStore.setToolMenuTab(ToolMenuTabs.INSPECT);
// editorStore.setSelectedComponentId(dataSource.id);
// };
// const onApiItemRemove = (api: ComponentSchema) => {
// editorStore.removeDataSource(api);
// };
// const onStateItemRemove = (state: ComponentSchema) => {
// editorStore.removeDataSource(state);
// };
const MenuItems = () => (
<>
{DATASOURCE_TYPES.map(type => (
<MenuItem key={type} onClick={() => onMenuItemClick(type)}>
{type}
</MenuItem>
))}
</>
const tdsGroups = Object.keys(tdsMap).map(type => {
return {
title: type,
dataSources: tdsMap[type],
type: 'trait',
};
});
const dsGroups = cdsGroups.concat(tdsGroups);
// cdsTypes: component data source types
// tdsTypes: trait data source types
const { cdsTypes, tdsTypes } = useMemo(() => {
const cdsTypes = registry
.getAllComponents()
.filter(c => c.metadata.isDataSource && c.metadata.name !== 'dummy')
.map(c => `${c.version}/${c.metadata.name}`);
const tdsTypes = registry
.getAllTraits()
.filter(t => t.metadata.isDataSource)
.map(t => `${t.version}/${t.metadata.name}`);
return { cdsTypes, tdsTypes };
}, [registry]);
const getNewId = useCallback(
(name: string): string => {
let count = dataSources.length;
let id = `${name}${count}`;
const ids = dataSources.map(({ id }) => id);
while (ids.includes(id)) {
id = `${name}${++count}`;
}
return `${name}${count}`;
},
[dataSources]
);
const onCreateDSFromComponent = useCallback(
(type: string) => {
const name = type.split('/')[2];
const id = getNewId(name);
eventBus.send(
'operation',
genOperation(registry, 'createComponent', {
componentType: type,
componentId: id,
})
);
setSelectedComponentId(id);
setToolMenuTab(ToolMenuTabs.INSPECT);
},
[eventBus, getNewId, registry, setSelectedComponentId, setToolMenuTab]
);
const onCreateDSFromTrait = useCallback(
(type: string) => {
const propertiesSpec = registry.getTraitByType(type).spec.properties;
const defaultProperties = generateDefaultValueFromSpec(propertiesSpec, {
genArrayItemDefaults: true,
});
const name = type.split('/')[2];
const id = getNewId(name);
eventBus.send(
'operation',
genOperation(registry, 'createDataSource', {
id,
type,
defaultProperties: defaultProperties as JSONSchema7,
})
);
setSelectedComponentId(id);
setToolMenuTab(ToolMenuTabs.INSPECT);
},
[eventBus, getNewId, registry, setSelectedComponentId, setToolMenuTab]
);
return (
@ -89,22 +138,36 @@ export const DataSourceList: React.FC<Props> = props => {
rightIcon={<ChevronDownIcon />}
/>
<MenuList>
<MenuItems />
{cdsTypes.length ? (
<MenuGroup title="From Component">
{cdsTypes.map(type => (
<MenuItem key={type} onClick={() => onCreateDSFromComponent(type)}>
{type}
</MenuItem>
))}
</MenuGroup>
) : undefined}
<MenuGroup title="From Trait">
{tdsTypes.map(type => (
<MenuItem key={type} onClick={() => onCreateDSFromTrait(type)}>
{type}
</MenuItem>
))}
</MenuGroup>
</MenuList>
</Menu>
</Flex>
<Accordion
reduceMotion
defaultIndex={[0].concat(NORMAL_DATASOURCES.map((_, i) => i + 1))}
defaultIndex={[0].concat(dsGroups.map((_, i) => i + 1))}
allowMultiple
>
{NORMAL_DATASOURCES.map(dataSourceItem => (
<DataSourceNode
key={dataSourceItem.title}
title={dataSourceItem.title}
filterPlaceholder={'filter'}
emptyPlaceholder={'Empty'}
datas={dataSourceItem.datas}
{dsGroups.map(group => (
<DataSourceGroup
key={group.title}
title={group.title}
type={group.type}
dataSources={group.dataSources}
services={services}
/>
))}

View File

@ -39,7 +39,6 @@ export const Editor: React.FC<Props> = observer(
components,
selectedComponentId,
modules,
activeDataSource,
toolMenuTab,
explorerMenuTab,
setToolMenuTab,
@ -158,10 +157,7 @@ export const Editor: React.FC<Props> = observer(
<StructureTree services={services} />
</TabPanel>
<TabPanel height="full" overflow="auto" p={0}>
<DataSourceList
active={activeDataSource?.id ?? ''}
services={services}
/>
<DataSourceList services={services} />
</TabPanel>
<TabPanel overflow="auto" p={0} height="100%">
<StateViewer store={stateStore} />
@ -212,15 +208,6 @@ export const Editor: React.FC<Props> = observer(
</Tabs>
</Box>
</Resizable>
{/* {selectedComponent && activeDataSourceType === 'core/v1/fetch' ? (
<ApiForm
key={selectedComponentId}
api={selectedComponent}
services={services}
store={stateStore}
className={ApiFormStyle}
/>
) : null} */}
</Flex>
</>
);

View File

@ -1,44 +0,0 @@
import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
export enum DataSourceType {
API = 'API',
STATE = 'State',
LOCALSTORAGE = 'LocalStorage',
TRANSFORMER = 'Transformer',
}
export const DATASOURCE_NAME_MAP = {
[DataSourceType.API]: 'api',
[DataSourceType.STATE]: 'state',
[DataSourceType.LOCALSTORAGE]: 'localStorage',
[DataSourceType.TRANSFORMER]: 'transformer',
};
export const DATASOURCE_TRAIT_TYPE_MAP = {
[DataSourceType.API]: `${CORE_VERSION}/${CoreTraitName.Fetch}`,
[DataSourceType.STATE]: `${CORE_VERSION}/${CoreTraitName.State}`,
[DataSourceType.LOCALSTORAGE]: `${CORE_VERSION}/${CoreTraitName.LocalStorage}`,
[DataSourceType.TRANSFORMER]: `${CORE_VERSION}/${CoreTraitName.Transformer}`,
};
export const DATA_DATASOURCES = [
{
type: DataSourceType.STATE,
traitType: DATASOURCE_TRAIT_TYPE_MAP[DataSourceType.STATE],
filterPlaceholder: 'filter the states',
emptyPlaceholder: 'No States.',
},
{
type: DataSourceType.LOCALSTORAGE,
traitType: DATASOURCE_TRAIT_TYPE_MAP[DataSourceType.LOCALSTORAGE],
filterPlaceholder: 'filter the localStorages',
emptyPlaceholder: 'No LocalStorages.',
},
{
type: DataSourceType.TRANSFORMER,
traitType: DATASOURCE_TRAIT_TYPE_MAP[DataSourceType.TRANSFORMER],
filterPlaceholder: 'filter the transformers',
emptyPlaceholder: 'No Transformers.',
},
];

View File

@ -7,14 +7,12 @@ export const unremovableTraits = [`${CORE_VERSION}/${CoreTraitName.Slot}`];
export const hideCreateTraitsList = [
`${CORE_VERSION}/${CoreTraitName.Event}`,
`${CORE_VERSION}/${CoreTraitName.Style}`,
// `${CORE_VERSION}/${CoreTraitName.Fetch}`,
`${CORE_VERSION}/${CoreTraitName.Slot}`,
];
export const hasSpecialFormTraitList = [
`${CORE_VERSION}/${CoreTraitName.Event}`,
`${CORE_VERSION}/${CoreTraitName.Style}`,
// `${CORE_VERSION}/${CoreTraitName.Fetch}`,
];
export const RootId = '__root__';

View File

@ -2,7 +2,6 @@ import { AppModel } from '../../../AppModel/AppModel';
import { BaseBranchOperation } from '../../type';
import { CreateComponentBranchOperation } from '../index';
import { CreateTraitLeafOperation } from '../../leaf';
import { DataSourceType, DATASOURCE_TRAIT_TYPE_MAP } from '../../../constants/dataSource';
import {
generateDefaultValueFromSpec,
CORE_VERSION,
@ -12,18 +11,17 @@ import { JSONSchema7Object } from 'json-schema';
export type CreateDataSourceBranchOperationContext = {
id: string;
type: DataSourceType;
defaultProperties: Record<string, any>;
type: string;
defaultProperties?: Record<string, any>;
};
export class CreateDataSourceBranchOperation extends BaseBranchOperation<CreateDataSourceBranchOperationContext> {
do(prev: AppModel): AppModel {
const { id, type, defaultProperties = {} } = this.context;
const traitType = DATASOURCE_TRAIT_TYPE_MAP[type];
const traitSpec = this.registry.getTraitByType(traitType).spec;
const initProperties = generateDefaultValueFromSpec(
traitSpec.properties
) as JSONSchema7Object;
const { id, type, defaultProperties } = this.context;
const traitSpec = this.registry.getTraitByType(type).spec;
const initProperties = generateDefaultValueFromSpec(traitSpec.properties, {
genArrayItemDefaults: true,
}) as JSONSchema7Object;
this.operationStack.insert(
new CreateComponentBranchOperation(this.registry, {
@ -34,14 +32,8 @@ export class CreateDataSourceBranchOperation extends BaseBranchOperation<CreateD
this.operationStack.insert(
new CreateTraitLeafOperation(this.registry, {
componentId: id,
traitType,
properties:
type === DataSourceType.API
? {
...initProperties,
method: 'get',
}
: { ...initProperties, ...defaultProperties },
traitType: type,
properties: defaultProperties || initProperties,
})
);

View File

@ -1,4 +1,3 @@
import { AppModel } from '../../../AppModel/AppModel';
import { ComponentId, TraitId, TraitType } from '../../../AppModel/IAppModel';
import { BaseLeafOperation } from '../../type';
@ -15,11 +14,14 @@ export class CreateTraitLeafOperation extends BaseLeafOperation<CreateTraitLeafO
do(prev: AppModel): AppModel {
const component = prev.getComponentById(this.context.componentId as ComponentId);
if (!component) {
return prev
return prev;
}
const trait = component.addTrait(this.context.traitType as TraitType, this.context.properties);
const trait = component.addTrait(
this.context.traitType as TraitType,
this.context.properties
);
this.traitId = trait.id;
return prev
return prev;
}
redo(prev: AppModel): AppModel {
@ -29,9 +31,9 @@ export class CreateTraitLeafOperation extends BaseLeafOperation<CreateTraitLeafO
undo(prev: AppModel): AppModel {
const component = prev.getComponentById(this.context.componentId as ComponentId);
if (!component) {
return prev
return prev;
}
component.removeTrait(this.traitId);
return prev
return prev;
}
}

View File

@ -5,8 +5,6 @@ import { RegistryInterface, StateManagerInterface } from '@sunmao-ui/runtime';
import { EventBusType } from './eventBus';
import { AppStorage } from './AppStorage';
import type { SchemaValidator, ValidateErrorResult } from '../validator';
import { DataSourceType, DATASOURCE_NAME_MAP } from '../constants/dataSource';
import { genOperation } from '../operations';
import { ExplorerMenuTabs, ToolMenuTabs } from '../constants/enum';
import { isEqual } from 'lodash';
@ -47,9 +45,6 @@ export class EditorStore {
lastSavedComponentsVersion = 0;
schemaValidator?: SchemaValidator;
// data source
activeDataSourceId: string | null = null;
private isDataSourceTypeCache: Record<string, boolean> = {};
constructor(
@ -115,7 +110,6 @@ export class EditorStore {
() => {
if (this.selectedComponentId) {
this.setToolMenuTab(ToolMenuTabs.INSPECT);
this.setActiveDataSourceId(null);
}
}
);
@ -208,18 +202,6 @@ export class EditorStore {
});
}
get activeDataSource(): ComponentSchema | null {
return (
this.components.find(component => component.id === this.activeDataSourceId) || null
);
}
get activeDataSourceType(): string | null {
if (!this.selectedComponent) return null;
const isDataSource = this.isDataSourceTypeCache[this.selectedComponent.type];
return isDataSource ? this.selectedComponent.traits[0].type : null;
}
clearSunmaoGlobalState() {
this.stateManager.clear();
this.setSelectedComponentId('');
@ -282,68 +264,10 @@ export class EditorStore {
this.lastSavedComponentsVersion = val;
};
setActiveDataSourceId = (dataSourceId: string | null) => {
this.activeDataSourceId = dataSourceId;
};
setValidateResult = (validateResult: ValidateErrorResult[]) => {
this.validateResult = validateResult;
};
createDataSource = (
type: DataSourceType,
defaultProperties: Record<string, any> = {}
) => {
const getCount = (
dataSources: ComponentSchema[] = [],
dataSourceName = ''
): number => {
let count = dataSources.length;
let id = `${dataSourceName}${count}`;
const ids = dataSources.map(({ id }) => id);
while (ids.includes(id)) {
id = `${dataSourceName}${++count}`;
}
return count;
};
const id = `${DATASOURCE_NAME_MAP[type]}${getCount(
this.dataSources,
DATASOURCE_NAME_MAP[type]
)}`;
this.eventBus.send(
'operation',
genOperation(this.registry, 'createDataSource', {
id,
type,
defaultProperties,
})
);
const component = this.components.find(({ id: componentId }) => id === componentId);
this.setActiveDataSourceId(component!.id);
if (type === DataSourceType.STATE || type === DataSourceType.LOCALSTORAGE) {
this.setToolMenuTab(ToolMenuTabs.INSPECT);
}
};
removeDataSource = (dataSource: ComponentSchema) => {
this.eventBus.send(
'operation',
genOperation(this.registry, 'removeComponent', {
componentId: dataSource.id,
})
);
if (this.activeDataSource?.id === dataSource.id) {
this.setActiveDataSourceId(null);
}
};
setExplorerMenuTab = (val: ExplorerMenuTabs) => {
this.explorerMenuTab = val;
};

View File

@ -60,6 +60,7 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.Fetch,
description: 'fetch data to store',
isDataSource: true,
},
spec: {
properties: Type.Object({

View File

@ -28,6 +28,7 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.LocalStorage,
description: 'localStorage trait',
isDataSource: true,
},
spec: {
properties: LocalStorageTraitPropertiesSpec,

View File

@ -18,6 +18,7 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.State,
description: 'add state to component',
isDataSource: true,
},
spec: {
properties: StateTraitPropertiesSpec,

View File

@ -16,6 +16,7 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.Transformer,
description: 'transform the value',
isDataSource: true,
},
spec: {
properties: TransformerTraitPropertiesSpec,