mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-02-17 17:40:31 +08:00
add ExplorerForm
This commit is contained in:
parent
85752079f6
commit
78946ff767
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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} />
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
@ -0,0 +1 @@
|
||||
export * from './ExplorerForm';
|
137
packages/editor/src/components/Explorer/ExplorerTree.tsx
Normal file
137
packages/editor/src/components/Explorer/ExplorerTree.tsx
Normal 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>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user