add ExplorerForm

This commit is contained in:
Bowen Tan 2021-12-01 15:14:14 +08:00
parent 85752079f6
commit 78946ff767
8 changed files with 270 additions and 151 deletions

View File

@ -15,7 +15,7 @@ export class AppStorage {
app: observable.shallow,
modules: observable.shallow,
setApp: action,
setModules: action
setModules: action,
});
}
@ -57,9 +57,9 @@ export class AppStorage {
this.saveModulesInLS();
}
// name is `${module.version}/${module.metadata.name}`
saveComponentsInLS(
type: 'app' | 'module',
version: string,
name: string,
components: ApplicationComponent[]
) {
@ -72,7 +72,9 @@ export class AppStorage {
this.saveAppInLS();
break;
case 'module':
const i = this.modules.findIndex(m => m.metadata.name === name);
const i = this.modules.findIndex(
m => m.version === version && m.metadata.name === name
);
const newModules = produce(this.modules, draft => {
draft[i].components = components;
});
@ -82,8 +84,6 @@ export class AppStorage {
}
}
private saveAppInLS() {
localStorage.setItem(AppStorage.AppLSKey, JSON.stringify(this.app));
}

View File

@ -1,18 +1,26 @@
import { makeAutoObservable, autorun, observable } from 'mobx';
import { makeAutoObservable, autorun, observable, action } from 'mobx';
import { ApplicationComponent } from '@sunmao-ui/core';
import { eventBus } from './eventBus';
import { AppStorage } from './AppStorage';
import { registry } from './setup';
type EditingTarget = {
kind: 'app' | 'module';
version: string;
name: string;
};
class EditorStore {
components: ApplicationComponent[] = [];
// currentEditingComponents, it could be app's or module's components
selectedComponentId = '';
selectedComponentId = this.components[0]?.id;
hoverComponentId = '';
// it could be app or module's name
// name is `${module.version}/${module.metadata.name}`
currentEditingName = '';
currentEditingType: 'app' | 'module' = 'app';
// current editor editing target(app or module)
currentEditingTarget: EditingTarget = {
kind: 'app',
version: '',
name: '',
};
appStorage = new AppStorage();
@ -24,11 +32,18 @@ class EditorStore {
return this.appStorage.modules;
}
get selectedComponent() {
return this.components.find(c => c.id === this.selectedComponentId);
}
constructor() {
makeAutoObservable(this, {
components: observable.shallow,
setComponents: action,
});
this.updateCurrentEditingTarget('app', this.app.version, this.app.metadata.name);
eventBus.on('selectComponent', id => {
this.setSelectedComponentId(id);
});
@ -36,11 +51,12 @@ class EditorStore {
eventBus.on('componentsChange', components => {
this.setComponents(components);
this.appStorage.saveComponentsInLS(
this.currentEditingType,
this.currentEditingName,
this.currentEditingTarget.kind,
this.currentEditingTarget.version,
this.currentEditingTarget.name,
components
);
if (this.currentEditingType === 'module') {
if (this.currentEditingTarget.kind === 'module') {
// reregister modules to activate immediately
this.modules.forEach(m => registry.registerModule(m, true));
}
@ -55,10 +71,13 @@ class EditorStore {
// origin components of app of module
// when switch app or module, components should refresh
get originComponents(): ApplicationComponent[] {
switch (this.currentEditingType) {
switch (this.currentEditingTarget.kind) {
case 'module':
console.log('this.currentEditingName', this.currentEditingTarget);
const module = this.modules.find(
m => m.metadata.name === this.currentEditingName
m =>
m.version === this.currentEditingTarget.version &&
m.metadata.name === this.currentEditingTarget.name
);
return module?.components || [];
case 'app':
@ -66,9 +85,16 @@ class EditorStore {
}
}
updateCurrentEditingTarget = (type: 'app' | 'module', name: string) => {
this.currentEditingType = type;
this.currentEditingName = name;
updateCurrentEditingTarget = (
kind: 'app' | 'module',
version: string,
name: string
) => {
this.currentEditingTarget = {
kind,
name,
version,
};
};
setSelectedComponentId = (val: string) => {
this.selectedComponentId = val;

View File

@ -1,9 +1,8 @@
import React, { useMemo } from 'react';
import React from 'react';
import { flatten } from 'lodash-es';
import { FormControl, FormLabel, Input, Textarea, VStack } from '@chakra-ui/react';
import { EmotionJSX } from '@emotion/react/types/jsx-namespace';
import { TSchema } from '@sinclair/typebox';
import { Application } from '@sunmao-ui/core';
import { parseType, parseTypeBox } from '@sunmao-ui/runtime';
import { eventBus } from '../../eventBus';
import { EventTraitForm } from './EventTraitForm';
@ -12,11 +11,11 @@ import { FetchTraitForm } from './FetchTraitForm';
import { Registry } from '@sunmao-ui/runtime/lib/services/registry';
import SchemaField from './JsonSchemaForm/SchemaField';
import { genOperation } from '../../operations';
import { editorStore } from '../../EditorStore';
import { observer } from 'mobx-react-lite';
type Props = {
registry: Registry;
selectedId: string;
app: Application;
};
export const renderField = (properties: {
@ -24,23 +23,23 @@ export const renderField = (properties: {
value: unknown;
type?: string;
fullKey: string;
selectedId: string;
selectedComponentId: string;
index: number;
}) => {
const { value, type, fullKey, selectedId, index } = properties;
const { value, type, fullKey, selectedComponentId, index } = properties;
if (typeof value !== 'object') {
const ref = React.createRef<HTMLTextAreaElement>();
const onBlur = () => {
const operation = type
? genOperation('modifyTraitProperty', {
componentId: selectedId,
componentId: selectedComponentId,
traitIndex: index,
properties: {
[fullKey]: ref.current?.value,
},
})
: genOperation('modifyComponentProperty', {
componentId: selectedId,
componentId: selectedComponentId,
properties: {
[fullKey]: ref.current?.value,
},
@ -48,7 +47,7 @@ export const renderField = (properties: {
eventBus.send('operation', operation);
};
return (
<FormControl key={`${selectedId}-${fullKey}`}>
<FormControl key={`${selectedComponentId}-${fullKey}`}>
<FormLabel>{fullKey}</FormLabel>
<Textarea ref={ref} onBlur={onBlur} defaultValue={value as string} />
</FormControl>
@ -63,7 +62,7 @@ export const renderField = (properties: {
value: childValue,
type: type,
fullKey: `${fullKey}.${childKey}`,
selectedId,
selectedComponentId,
});
})
);
@ -71,16 +70,12 @@ export const renderField = (properties: {
}
};
export const ComponentForm: React.FC<Props> = props => {
const { selectedId, app, registry } = props;
const selectedComponent = useMemo(
() => app.spec.components.find(c => c.id === selectedId),
[selectedId, app]
);
export const ComponentForm: React.FC<Props> = observer(props => {
const { registry } = props;
const { selectedComponent, selectedComponentId } = editorStore;
if (!selectedComponent) {
return <div>cannot find component with id: {selectedId}</div>;
return <div>cannot find component with id: {selectedComponentId}</div>;
}
const { version, name } = parseType(selectedComponent.type);
const cImpl = registry.getComponent(version, name);
@ -89,18 +84,18 @@ export const ComponentForm: React.FC<Props> = props => {
selectedComponent.properties
);
const changeComponentId = (selectedId: string, value: string) => {
const changeComponentId = (selectedComponentId: string, value: string) => {
eventBus.send(
'operation',
genOperation('modifyComponentId', {
componentId: selectedId,
componentId: selectedComponentId,
newId: value,
})
);
};
return !selectedComponent ? (
<div>cannot find component with id: {selectedId}</div>
<div>cannot find component with id: {selectedComponentId}</div>
) : (
<VStack p={4} spacing="4" background="gray.50">
<FormControl>
@ -132,7 +127,7 @@ export const ComponentForm: React.FC<Props> = props => {
eventBus.send(
'operation',
genOperation('modifyComponentProperty', {
componentId: selectedId,
componentId: selectedComponentId,
properties: newFormData,
})
);
@ -145,4 +140,4 @@ export const ComponentForm: React.FC<Props> = props => {
<GeneralTraitFormList component={selectedComponent} registry={registry} />
</VStack>
);
};
});

View File

@ -5,7 +5,6 @@ import { Box, Tabs, TabList, Tab, TabPanels, TabPanel, Flex } from '@chakra-ui/r
import { observer } from 'mobx-react-lite';
import { StructureTree } from './StructureTree';
import { eventBus } from '../eventBus';
import { ComponentForm } from './ComponentForm';
import { ComponentList } from './ComponentsList';
import { EditorHeader } from './EditorHeader';
import { PreviewModal } from './PreviewModal';
@ -15,6 +14,7 @@ import { StateEditor, SchemaEditor } from './CodeEditor';
import { Explorer } from './Explorer';
import { editorStore } from '../EditorStore';
import { genOperation } from '../operations';
import { ComponentForm } from './ComponentForm';
type ReturnOfInit = ReturnType<typeof initSunmaoUI>;
@ -159,11 +159,7 @@ export const Editor: React.FC<Props> = observer(({ App, registry, stateStore })
</TabList>
<TabPanels flex="1" overflow="auto">
<TabPanel p={0}>
<ComponentForm
app={app}
selectedId={selectedComponentId}
registry={registry}
/>
<ComponentForm registry={registry} />
</TabPanel>
<TabPanel p={0}>
<ComponentList registry={registry} />

View File

@ -1,110 +1,30 @@
import { Divider, HStack, IconButton, Text, VStack } from '@chakra-ui/react';
import React from 'react';
import { observer } from 'mobx-react-lite';
import { AddIcon, DeleteIcon } from '@chakra-ui/icons';
import { ImplementedRuntimeModule } from '@sunmao-ui/runtime';
import { editorStore } from '../../EditorStore';
import { ExplorerForm } from './ExplorerForm/ExplorerForm';
import { ExplorerTree } from './ExplorerTree';
export const Explorer: React.FC = observer(() => {
const { app, modules, updateCurrentEditingTarget } = editorStore;
const appItemId = `app_${app.metadata.name}`;
const [selectedItem, setSelectedItem] = React.useState<string | undefined>(appItemId);
const onClickApp = (id: string) => {
setSelectedItem(id);
updateCurrentEditingTarget('app', app.metadata.name);
export const Explorer: React.FC = () => {
const [isEditingMode, setIsEditingMode] = React.useState(false);
const [formType, setFormType] = React.useState<'app' | 'module'>('app');
const [currentVersion, setCurrentVersion] = React.useState<string>('');
const [currentName, setCurrentName] = React.useState<string>('');
const onEdit = (type: 'app' | 'module', version: string, name: string) => {
setFormType(type);
setCurrentVersion(version);
setCurrentName(name);
setIsEditingMode(true);
};
const appItem = (
<ExplorerItem
key={app.metadata.name}
id={appItemId}
title={app.metadata.name}
onClick={onClickApp}
isActive={selectedItem === appItemId}
/>
);
const moduleItems = modules.map((module: ImplementedRuntimeModule) => {
const moduleItemId = `module_${module.metadata.name}`;
const onClickModule = (id: string) => {
setSelectedItem(id);
updateCurrentEditingTarget('module', module.metadata.name);
};
const onRemove = () => {
editorStore.appStorage.removeModule(module.version, module.metadata.name);
};
const onBack = () => {
setIsEditingMode(false);
};
if (isEditingMode) {
return (
<ExplorerItem
key={module.metadata.name}
id={moduleItemId}
title={`${module.version}/${module.metadata.name}`}
onClick={onClickModule}
onRemove={onRemove}
isActive={selectedItem === moduleItemId}
<ExplorerForm
formType={formType}
version={currentVersion}
name={currentName}
onBack={onBack}
/>
);
});
return (
<VStack alignItems="start">
<Text fontSize="lg" fontWeight="bold">
Applications
</Text>
{appItem}
<Divider />
<HStack width="full" justifyContent="space-between">
<Text fontSize="lg" fontWeight="bold">
Modules
</Text>
<IconButton
aria-label="create module"
size="xs"
icon={<AddIcon />}
onClick={() => editorStore.appStorage.createModule()}
/>
</HStack>
{moduleItems}
</VStack>
);
});
type ExplorerItemProps = {
id: string;
title: string;
isActive: boolean;
onClick: (id: string) => void;
onRemove?: () => void;
};
const ExplorerItem: React.FC<ExplorerItemProps> = ({
id,
title,
isActive,
onClick,
onRemove,
}) => {
return (
<HStack
width="full"
justify="space-between"
cursor="pointer"
borderRadius="5"
padding="2"
backgroundColor={isActive ? 'gray.100' : 'white'}
>
<Text fontSize="lg" onClick={() => onClick(id)}>
{title}
</Text>
{onRemove ? (
<IconButton
variant="ghost"
size="smx"
aria-label="remove"
icon={<DeleteIcon />}
onClick={() => onRemove()}
/>
) : null}
</HStack>
);
}
return <ExplorerTree onEdit={onEdit} />;
};

View File

@ -0,0 +1,44 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { IconButton, VStack } from '@chakra-ui/react';
import { ArrowLeftIcon } from '@chakra-ui/icons';
type Props = {
formType: 'app' | 'module';
version: string;
name: string;
onBack: () => void;
};
export const ExplorerForm: React.FC<Props> = observer(
({ formType, version, name, onBack }) => {
let form;
switch (formType) {
case 'app':
form = (
<span>
App Form: {version}/{name}
</span>
);
break;
case 'module':
form = (
<span>
module Form: {version}/{name}
</span>
);
break;
}
return (
<VStack alignItems='start'>
<IconButton
aria-label="go back to tree"
size="xs"
icon={<ArrowLeftIcon />}
variant="ghost"
onClick={onBack}
/>
{form}
</VStack>
);
}
);

View File

@ -0,0 +1 @@
export * from './ExplorerForm';

View File

@ -0,0 +1,137 @@
import { Divider, HStack, IconButton, Text, VStack } from '@chakra-ui/react';
import React from 'react';
import { observer } from 'mobx-react-lite';
import { AddIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { ImplementedRuntimeModule } from '@sunmao-ui/runtime';
import { editorStore } from '../../EditorStore';
type ExplorerTreeProps = {
onEdit: (type: 'app' | 'module', version: string, name: string) => void;
};
export const ExplorerTree: React.FC<ExplorerTreeProps> = observer(({ onEdit }) => {
const { app, modules, updateCurrentEditingTarget } = editorStore;
const appItemId = `app_${app.metadata.name}`;
const [selectedItem, setSelectedItem] = React.useState<string | undefined>(appItemId);
const onClickApp = () => {
setSelectedItem(appItemId);
updateCurrentEditingTarget('app', app.version, app.metadata.name);
};
const onEditApp = () => {
onEdit('app', app.version, app.metadata.name);
};
const appItem = (
<ExplorerTreeItem
key={app.metadata.name}
title={app.metadata.name}
onClick={onClickApp}
isActive={selectedItem === appItemId}
onEdit={onEditApp}
/>
);
const moduleItems = modules.map((module: ImplementedRuntimeModule) => {
const moduleItemId = `module_${module.metadata.name}`;
const onClickModule = () => {
setSelectedItem(moduleItemId);
updateCurrentEditingTarget('module', module.version, module.metadata.name);
};
const onEditModule = () => {
onEdit('module', module.version, module.metadata.name);
};
const onRemove = () => {
editorStore.appStorage.removeModule(module.version, module.metadata.name);
};
return (
<ExplorerTreeItem
key={module.metadata.name}
title={`${module.version}/${module.metadata.name}`}
onClick={onClickModule}
onRemove={onRemove}
isActive={selectedItem === moduleItemId}
onEdit={onEditModule}
/>
);
});
return (
<VStack alignItems="start">
<Text fontSize="lg" fontWeight="bold">
Applications
</Text>
{appItem}
<Divider />
<HStack width="full" justifyContent="space-between">
<Text fontSize="lg" fontWeight="bold">
Modules
</Text>
<IconButton
aria-label="create module"
size="xs"
icon={<AddIcon />}
onClick={() => editorStore.appStorage.createModule()}
/>
</HStack>
{moduleItems}
</VStack>
);
});
type ExplorerTreeItemProps = {
title: string;
isActive: boolean;
onClick: () => void;
onEdit: () => void;
onRemove?: () => void;
};
const ExplorerTreeItem: React.FC<ExplorerTreeItemProps> = ({
title,
isActive,
onClick,
onRemove,
onEdit,
}) => {
const _onEdit = (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
onEdit();
};
return (
<HStack
width="full"
justify="space-between"
cursor="pointer"
borderRadius="5"
padding="2"
backgroundColor={isActive ? 'gray.100' : 'white'}
>
<Text
fontSize="lg"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
onClick={onClick}
>
{title}
</Text>
<IconButton
variant="ghost"
size="smx"
aria-label="edit"
icon={<EditIcon />}
onClick={_onEdit}
/>
{onRemove ? (
<IconButton
variant="ghost"
size="smx"
aria-label="remove"
icon={<DeleteIcon />}
onClick={onRemove}
/>
) : null}
</HStack>
);
};