Merge pull request #21 from toeverything/feat/block-pendant

Feat/block pendant
This commit is contained in:
Qi
2022-08-02 15:06:29 +08:00
committed by GitHub
5 changed files with 355 additions and 255 deletions

View File

@@ -26,7 +26,6 @@ export const PendantHistoryPanel = ({
const { getProperties } = useRecastBlockMeta(); const { getProperties } = useRecastBlockMeta();
const { getProperty } = useRecastBlockMeta(); const { getProperty } = useRecastBlockMeta();
const { getAllValue } = getRecastItemValue(block);
const recastBlock = useRecastBlock(); const recastBlock = useRecastBlock();
const [history, setHistory] = useState<RecastBlockValue[]>([]); const [history, setHistory] = useState<RecastBlockValue[]>([]);
@@ -34,7 +33,7 @@ export const PendantHistoryPanel = ({
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
const currentBlockValues = getAllValue(); const currentBlockValues = getRecastItemValue(block).getAllValue();
const allProperties = getProperties(); const allProperties = getProperties();
const missProperties = allProperties.filter( const missProperties = allProperties.filter(
property => !currentBlockValues.find(v => v.id === property.id) property => !currentBlockValues.find(v => v.id === property.id)
@@ -52,24 +51,26 @@ export const PendantHistoryPanel = ({
return history; return history;
}, {}); }, {});
const blockHistory = await Promise.all( const blockHistory = (
Object.entries(historyMap).map( await Promise.all(
async ([propertyId, blockId]) => { Object.entries(historyMap).map(
const latestValueBlock = ( async ([propertyId, blockId]) => {
await groupBlock.children() const latestValueBlock = (
).find((block: AsyncBlock) => block.id === blockId); await groupBlock.children()
).find((block: AsyncBlock) => block.id === blockId);
return getRecastItemValue(latestValueBlock).getValue( return getRecastItemValue(
propertyId as RecastPropertyId latestValueBlock
); ).getValue(propertyId as RecastPropertyId);
} }
)
) )
); ).filter(v => v);
setHistory(blockHistory); setHistory(blockHistory);
}; };
init(); init();
}, [getAllValue, getProperties, groupBlock, recastBlock]); }, [block, getProperties, groupBlock, recastBlock]);
return ( return (
<StyledPendantHistoryPanel> <StyledPendantHistoryPanel>

View File

@@ -1,11 +1,11 @@
import React, { CSSProperties, useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Input, Option, Select, Tooltip } from '@toeverything/components/ui'; import { Input, Option, Select, Tooltip } from '@toeverything/components/ui';
import { HelpCenterIcon } from '@toeverything/components/icons'; import { HelpCenterIcon } from '@toeverything/components/icons';
import { AsyncBlock } from '../../editor'; import { AsyncBlock } from '../../editor';
import { IconMap, pendantOptions } from '../config'; import { IconMap, pendantOptions } from '../config';
import { OptionType, PendantOptions, PendantTypes } from '../types'; import { PendantOptions } from '../types';
import { PendantModifyPanel } from '../pendant-modify-panel'; import { PendantModifyPanel } from '../pendant-modify-panel';
import { import {
StyledDivider, StyledDivider,
@@ -15,23 +15,13 @@ import {
StyledPopoverSubTitle, StyledPopoverSubTitle,
StyledPopoverWrapper, StyledPopoverWrapper,
} from '../StyledComponent'; } from '../StyledComponent';
import { import { genInitialOptions, getPendantConfigByType } from '../utils';
genSelectOptionId, import { useOnCreateSure } from './hooks';
InformationProperty,
useRecastBlock,
useRecastBlockMeta,
useSelectProperty,
} from '../../recast-block';
import {
genInitialOptions,
getOfficialSelected,
getPendantConfigByType,
} from '../utils';
import { usePendant } from '../use-pendant';
const upperFirst = (str: string) => { const upperFirst = (str: string) => {
return `${str[0].toUpperCase()}${str.slice(1)}`; return `${str[0].toUpperCase()}${str.slice(1)}`;
}; };
export const CreatePendantPanel = ({ export const CreatePendantPanel = ({
block, block,
onSure, onSure,
@@ -41,9 +31,7 @@ export const CreatePendantPanel = ({
}) => { }) => {
const [selectedOption, setSelectedOption] = useState<PendantOptions>(); const [selectedOption, setSelectedOption] = useState<PendantOptions>();
const [fieldName, setFieldName] = useState<string>(''); const [fieldName, setFieldName] = useState<string>('');
const { addProperty, removeProperty } = useRecastBlockMeta(); const onCreateSure = useOnCreateSure({ block });
const { createSelect } = useSelectProperty();
const { setPendant } = usePendant(block);
useEffect(() => { useEffect(() => {
selectedOption && selectedOption &&
@@ -110,91 +98,13 @@ export const CreatePendantPanel = ({
getPendantConfigByType(selectedOption.type) getPendantConfigByType(selectedOption.type)
)} )}
iconConfig={getPendantConfigByType(selectedOption.type)} iconConfig={getPendantConfigByType(selectedOption.type)}
// isStatusSelect={selectedOption.name === 'Status'}
onSure={async (type, newPropertyItem, newValue) => { onSure={async (type, newPropertyItem, newValue) => {
if (!fieldName) { await onCreateSure({
return; type,
} newPropertyItem,
newValue,
if ( fieldName,
type === PendantTypes.MultiSelect || });
type === PendantTypes.Select ||
type === PendantTypes.Status
) {
const newProperty = await createSelect({
name: fieldName,
options: newPropertyItem,
type,
});
const selectedId = getOfficialSelected({
isMulti: type === PendantTypes.MultiSelect,
options: newProperty.options,
tempOptions: newPropertyItem,
tempSelectedId: newValue,
});
await setPendant(newProperty, selectedId);
} else if (type === PendantTypes.Information) {
const emailOptions = genOptionWithId(
newPropertyItem.emailOptions
);
const phoneOptions = genOptionWithId(
newPropertyItem.phoneOptions
);
const locationOptions = genOptionWithId(
newPropertyItem.locationOptions
);
const newProperty = await addProperty({
type,
name: fieldName,
emailOptions,
phoneOptions,
locationOptions,
} as Omit<InformationProperty, 'id'>);
await setPendant(newProperty, {
email: getOfficialSelected({
isMulti: true,
options: emailOptions,
tempOptions:
newPropertyItem.emailOptions,
tempSelectedId: newValue.email,
}),
phone: getOfficialSelected({
isMulti: true,
options: phoneOptions,
tempOptions:
newPropertyItem.phoneOptions,
tempSelectedId: newValue.phone,
}),
location: getOfficialSelected({
isMulti: true,
options: locationOptions,
tempOptions:
newPropertyItem.locationOptions,
tempSelectedId: newValue.location,
}),
});
} else {
// TODO: Color and background should use pendant config, but ui is not design now
const iconConfig = getPendantConfigByType(type);
// TODO: Color and background should be choose by user in the future
const newProperty = await addProperty({
type: type,
name: fieldName,
background:
iconConfig.background as CSSProperties['background'],
color: iconConfig.color as CSSProperties['color'],
iconName: iconConfig.iconName,
});
await setPendant(newProperty, newValue);
}
onSure?.(); onSure?.();
}} }}
/> />
@@ -203,10 +113,3 @@ export const CreatePendantPanel = ({
</StyledPopoverWrapper> </StyledPopoverWrapper>
); );
}; };
const genOptionWithId = (options: OptionType[] = []) => {
return options.map((option: OptionType) => ({
...option,
id: genSelectOptionId(),
}));
};

View File

@@ -1,27 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { Input, Tooltip } from '@toeverything/components/ui';
import { HelpCenterIcon } from '@toeverything/components/icons';
import { PendantModifyPanel } from '../pendant-modify-panel'; import { PendantModifyPanel } from '../pendant-modify-panel';
import type { AsyncBlock } from '../../editor'; import type { AsyncBlock } from '../../editor';
import { import {
genSelectOptionId,
InformationProperty,
type MultiSelectProperty,
type RecastBlockValue, type RecastBlockValue,
type RecastMetaProperty, type RecastMetaProperty,
type SelectOption,
type SelectProperty,
useRecastBlockMeta,
useSelectProperty,
} from '../../recast-block'; } from '../../recast-block';
import { OptionType, PendantTypes, TempInformationType } from '../types'; import { getPendantConfigByType } from '../utils';
import {
getOfficialSelected,
getPendantConfigByType,
// getPendantIconsConfigByNameOrType,
} from '../utils';
import { usePendant } from '../use-pendant'; import { usePendant } from '../use-pendant';
import { import {
StyledPopoverWrapper, StyledPopoverWrapper,
StyledOperationTitle,
StyledOperationLabel, StyledOperationLabel,
StyledInputEndAdornment, StyledInputEndAdornment,
StyledDivider, StyledDivider,
@@ -29,10 +18,8 @@ import {
StyledPopoverSubTitle, StyledPopoverSubTitle,
} from '../StyledComponent'; } from '../StyledComponent';
import { IconMap, pendantOptions } from '../config'; import { IconMap, pendantOptions } from '../config';
import { Input, Tooltip } from '@toeverything/components/ui';
import { HelpCenterIcon } from '@toeverything/components/icons';
type SelectPropertyType = MultiSelectProperty | SelectProperty; import { useOnUpdateSure } from './hooks';
type Props = { type Props = {
value: RecastBlockValue; value: RecastBlockValue;
@@ -53,13 +40,12 @@ export const UpdatePendantPanel = ({
onCancel, onCancel,
titleEditable = false, titleEditable = false,
}: Props) => { }: Props) => {
const { updateSelect } = useSelectProperty();
const { setPendant, removePendant } = usePendant(block);
const pendantOption = pendantOptions.find(v => v.type === property.type); const pendantOption = pendantOptions.find(v => v.type === property.type);
const iconConfig = getPendantConfigByType(property.type); const iconConfig = getPendantConfigByType(property.type);
const { removePendant } = usePendant(block);
const Icon = IconMap[iconConfig.iconName]; const Icon = IconMap[iconConfig.iconName];
const { updateProperty } = useRecastBlockMeta(); const [fieldName, setFieldName] = useState(property.name);
const [fieldTitle, setFieldTitle] = useState(property.name); const onUpdateSure = useOnUpdateSure({ block, property });
return ( return (
<StyledPopoverWrapper> <StyledPopoverWrapper>
@@ -77,10 +63,10 @@ export const UpdatePendantPanel = ({
<StyledOperationLabel>Field Title</StyledOperationLabel> <StyledOperationLabel>Field Title</StyledOperationLabel>
{titleEditable ? ( {titleEditable ? (
<Input <Input
value={fieldTitle} value={fieldName}
placeholder="Input your field name here" placeholder="Input your field name here"
onChange={e => { onChange={e => {
setFieldTitle(e.target.value); setFieldName(e.target.value);
}} }}
endAdornment={ endAdornment={
<Tooltip content="Help info here"> <Tooltip content="Help info here">
@@ -111,114 +97,12 @@ export const UpdatePendantPanel = ({
property={property} property={property}
type={property.type} type={property.type}
onSure={async (type, newPropertyItem, newValue) => { onSure={async (type, newPropertyItem, newValue) => {
if ( await onUpdateSure({
type === PendantTypes.MultiSelect || type,
type === PendantTypes.Select || newPropertyItem,
type === PendantTypes.Status newValue,
) { fieldName,
const newOptions = newPropertyItem as OptionType[]; });
let selectProperty = property as SelectPropertyType;
const deleteOptionIds = selectProperty.options
.filter(o => {
return !newOptions.find(no => no.id === o.id);
})
.map(o => o.id);
const addOptions = newOptions.filter(
o => typeof o.id === 'number'
);
const { addSelectOptions, removeSelectOptions } =
updateSelect(selectProperty);
deleteOptionIds.length &&
(selectProperty = (await removeSelectOptions(
...deleteOptionIds
)) as SelectPropertyType);
addOptions.length &&
(selectProperty = (await addSelectOptions(
...(addOptions as unknown as Omit<
SelectOption,
'id'
>[])
)) as SelectPropertyType);
const selectedId = getOfficialSelected({
isMulti: type === PendantTypes.MultiSelect,
options: selectProperty.options,
tempOptions: newPropertyItem,
tempSelectedId: newValue,
});
await setPendant(selectProperty, selectedId);
} else if (type === PendantTypes.Information) {
// const { emailOptions, phoneOptions, locationOptions } =
// property as InformationProperty;
const optionGroup =
newPropertyItem as TempInformationType;
const emailOptions = optionGroup.emailOptions.map(
option => {
if (typeof option.id === 'number') {
option.id = genSelectOptionId();
}
return option;
}
);
const phoneOptions = optionGroup.phoneOptions.map(
option => {
if (typeof option.id === 'number') {
option.id = genSelectOptionId();
}
return option;
}
);
const locationOptions = optionGroup.locationOptions.map(
option => {
if (typeof option.id === 'number') {
option.id = genSelectOptionId();
}
return option;
}
);
const newProperty = await updateProperty({
...property,
emailOptions,
phoneOptions,
locationOptions,
} as InformationProperty);
await setPendant(newProperty, {
email: getOfficialSelected({
isMulti: true,
options: emailOptions as SelectOption[],
tempOptions: newPropertyItem.emailOptions,
tempSelectedId: newValue.email,
}),
phone: getOfficialSelected({
isMulti: true,
options: phoneOptions as SelectOption[],
tempOptions: newPropertyItem.phoneOptions,
tempSelectedId: newValue.phone,
}),
location: getOfficialSelected({
isMulti: true,
options: locationOptions as SelectOption[],
tempOptions: newPropertyItem.locationOptions,
tempSelectedId: newValue.location,
}),
});
} else {
await setPendant(property, newValue);
}
if (fieldTitle !== property.name) {
await updateProperty({
...property,
name: fieldTitle,
});
}
onSure?.(); onSure?.();
}} }}
onDelete={ onDelete={

View File

@@ -0,0 +1,265 @@
import type { CSSProperties } from 'react';
import {
genSelectOptionId,
type InformationProperty,
type MultiSelectProperty,
type RecastMetaProperty,
type SelectOption,
type SelectProperty,
useRecastBlockMeta,
useSelectProperty,
} from '../../recast-block';
import { type AsyncBlock } from '../../editor';
import { usePendant } from '../use-pendant';
import {
type OptionType,
PendantTypes,
type TempInformationType,
} from '../types';
import {
checkPendantForm,
getOfficialSelected,
getPendantConfigByType,
} from '../utils';
import { message } from '@toeverything/components/ui';
type SelectPropertyType = MultiSelectProperty | SelectProperty;
type SureParams = {
fieldName: string;
type: PendantTypes;
newPropertyItem: any;
newValue: any;
};
const genOptionWithId = (options: OptionType[] = []) => {
return options.map((option: OptionType) => ({
...option,
id: genSelectOptionId(),
}));
};
// Callback function for pendant create
export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
const { addProperty } = useRecastBlockMeta();
const { createSelect } = useSelectProperty();
const { setPendant } = usePendant(block);
return async ({
type,
fieldName,
newPropertyItem,
newValue,
}: SureParams) => {
const checkResult = checkPendantForm(
type,
fieldName,
newPropertyItem,
newValue
);
if (!checkResult.passed) {
await message.error(checkResult.message);
return;
}
if (
type === PendantTypes.MultiSelect ||
type === PendantTypes.Select ||
type === PendantTypes.Status
) {
const newProperty = await createSelect({
name: fieldName,
options: newPropertyItem,
type,
});
const selectedId = getOfficialSelected({
isMulti: type === PendantTypes.MultiSelect,
options: newProperty.options,
tempOptions: newPropertyItem,
tempSelectedId: newValue,
});
await setPendant(newProperty, selectedId);
} else if (type === PendantTypes.Information) {
const emailOptions = genOptionWithId(newPropertyItem.emailOptions);
const phoneOptions = genOptionWithId(newPropertyItem.phoneOptions);
const locationOptions = genOptionWithId(
newPropertyItem.locationOptions
);
const newProperty = await addProperty({
type,
name: fieldName,
emailOptions,
phoneOptions,
locationOptions,
} as Omit<InformationProperty, 'id'>);
await setPendant(newProperty, {
email: getOfficialSelected({
isMulti: true,
options: emailOptions,
tempOptions: newPropertyItem.emailOptions,
tempSelectedId: newValue.email,
}),
phone: getOfficialSelected({
isMulti: true,
options: phoneOptions,
tempOptions: newPropertyItem.phoneOptions,
tempSelectedId: newValue.phone,
}),
location: getOfficialSelected({
isMulti: true,
options: locationOptions,
tempOptions: newPropertyItem.locationOptions,
tempSelectedId: newValue.location,
}),
});
} else {
// TODO: Color and background should use pendant config, but ui is not design now
const iconConfig = getPendantConfigByType(type);
// TODO: Color and background should be choose by user in the future
const newProperty = await addProperty({
type: type,
name: fieldName,
background:
iconConfig.background as CSSProperties['background'],
color: iconConfig.color as CSSProperties['color'],
iconName: iconConfig.iconName,
});
await setPendant(newProperty, newValue);
}
};
};
// Callback function for pendant update
export const useOnUpdateSure = ({
block,
property,
}: {
block: AsyncBlock;
property: RecastMetaProperty;
}) => {
const { updateSelect } = useSelectProperty();
const { setPendant } = usePendant(block);
const { updateProperty } = useRecastBlockMeta();
return async ({
type,
fieldName,
newPropertyItem,
newValue,
}: SureParams) => {
const checkResult = checkPendantForm(
type,
fieldName,
newPropertyItem,
newValue
);
if (!checkResult.passed) {
await message.error(checkResult.message);
return;
}
if (
type === PendantTypes.MultiSelect ||
type === PendantTypes.Select ||
type === PendantTypes.Status
) {
const newOptions = newPropertyItem as OptionType[];
let selectProperty = property as SelectPropertyType;
const deleteOptionIds = selectProperty.options
.filter(o => {
return !newOptions.find(no => no.id === o.id);
})
.map(o => o.id);
const addOptions = newOptions.filter(o => typeof o.id === 'number');
const { addSelectOptions, removeSelectOptions } =
updateSelect(selectProperty);
deleteOptionIds.length &&
(selectProperty = (await removeSelectOptions(
...deleteOptionIds
)) as SelectPropertyType);
addOptions.length &&
(selectProperty = (await addSelectOptions(
...(addOptions as unknown as Omit<SelectOption, 'id'>[])
)) as SelectPropertyType);
const selectedId = getOfficialSelected({
isMulti: type === PendantTypes.MultiSelect,
options: selectProperty.options,
tempOptions: newPropertyItem,
tempSelectedId: newValue,
});
await setPendant(selectProperty, selectedId);
} else if (type === PendantTypes.Information) {
// const { emailOptions, phoneOptions, locationOptions } =
// property as InformationProperty;
const optionGroup = newPropertyItem as TempInformationType;
const emailOptions = optionGroup.emailOptions.map(option => {
if (typeof option.id === 'number') {
option.id = genSelectOptionId();
}
return option;
});
const phoneOptions = optionGroup.phoneOptions.map(option => {
if (typeof option.id === 'number') {
option.id = genSelectOptionId();
}
return option;
});
const locationOptions = optionGroup.locationOptions.map(option => {
if (typeof option.id === 'number') {
option.id = genSelectOptionId();
}
return option;
});
const newProperty = await updateProperty({
...property,
emailOptions,
phoneOptions,
locationOptions,
} as InformationProperty);
await setPendant(newProperty, {
email: getOfficialSelected({
isMulti: true,
options: emailOptions as SelectOption[],
tempOptions: newPropertyItem.emailOptions,
tempSelectedId: newValue.email,
}),
phone: getOfficialSelected({
isMulti: true,
options: phoneOptions as SelectOption[],
tempOptions: newPropertyItem.phoneOptions,
tempSelectedId: newValue.phone,
}),
location: getOfficialSelected({
isMulti: true,
options: locationOptions as SelectOption[],
tempOptions: newPropertyItem.locationOptions,
tempSelectedId: newValue.location,
}),
});
} else {
await setPendant(property, newValue);
}
if (fieldName !== property.name) {
await updateProperty({
...property,
name: fieldName,
});
}
};
};

View File

@@ -1,4 +1,5 @@
import { import {
PropertyType,
RecastBlockValue, RecastBlockValue,
RecastPropertyId, RecastPropertyId,
SelectOption, SelectOption,
@@ -175,3 +176,49 @@ export const genInitialOptions = (
} }
return [genBasicOption({ index: 0, iconConfig })]; return [genBasicOption({ index: 0, iconConfig })];
}; };
export const checkPendantForm = (
type: PropertyType,
fieldTitle: string,
newProperty: any,
newValue: any
): { passed: boolean; message: string } => {
if (!fieldTitle) {
return { passed: false, message: 'The field title cannot be empty !' };
}
if (
type === PendantTypes.MultiSelect ||
type === PendantTypes.Select ||
type === PendantTypes.Status
) {
if (!newProperty) {
return {
passed: false,
message: 'Ensure at least one non-empty option !',
};
}
}
if (type === PendantTypes.Information) {
if (!newProperty) {
return {
passed: false,
message: 'Ensure at least one non-empty option !',
};
}
}
if (
type === PendantTypes.Text ||
type === PendantTypes.Date ||
type === PendantTypes.Mention
) {
if (!newValue) {
return {
passed: false,
message: `The content of the input must not be empty !`,
};
}
}
return { passed: true, message: 'Check passed !' };
};