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

Feat/block pendant
This commit is contained in:
Qi
2022-08-04 16:23:00 +08:00
committed by GitHub
19 changed files with 382 additions and 279 deletions

View File

@@ -41,6 +41,7 @@ const getKanbanColor = (
return DEFAULT_COLOR; return DEFAULT_COLOR;
} }
if ( if (
group.type === PropertyType.Status ||
group.type === PropertyType.Select || group.type === PropertyType.Select ||
group.type === PropertyType.MultiSelect || group.type === PropertyType.MultiSelect ||
group.type === DEFAULT_GROUP_ID group.type === DEFAULT_GROUP_ID

View File

@@ -1,5 +1,4 @@
import React, { ReactNode, useRef, useEffect, useState } from 'react'; import React, { ReactNode, useRef, useEffect, useState } from 'react';
import { getPendantHistory } from '../utils';
import { import {
getRecastItemValue, getRecastItemValue,
RecastMetaProperty, RecastMetaProperty,
@@ -30,22 +29,22 @@ export const PendantHistoryPanel = ({
const [history, setHistory] = useState<RecastBlockValue[]>([]); const [history, setHistory] = useState<RecastBlockValue[]>([]);
const popoverHandlerRef = useRef<{ [key: string]: PopperHandler }>({}); const popoverHandlerRef = useRef<{ [key: string]: PopperHandler }>({});
const { getValueHistory } = getRecastItemValue(block);
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
const currentBlockValues = getRecastItemValue(block).getAllValue(); const currentBlockValues = getRecastItemValue(block).getAllValue();
const allProperties = getProperties(); const missValues = getProperties().filter(
const missProperties = allProperties.filter(
property => !currentBlockValues.find(v => v.id === property.id) property => !currentBlockValues.find(v => v.id === property.id)
); );
const pendantHistory = getPendantHistory({ const valueHistory = getValueHistory({
recastBlockId: recastBlock.id, recastBlockId: recastBlock.id,
}); });
const historyMap = missProperties.reduce<{ const historyMap = missValues.reduce<{
[key: RecastPropertyId]: string; [key: RecastPropertyId]: string[];
}>((history, property) => { }>((history, property) => {
if (pendantHistory[property.id]) { if (valueHistory[property.id]) {
history[property.id] = pendantHistory[property.id]; history[property.id] = valueHistory[property.id];
} }
return history; return history;
@@ -54,18 +53,30 @@ export const PendantHistoryPanel = ({
const blockHistory = ( const blockHistory = (
await Promise.all( await Promise.all(
Object.entries(historyMap).map( Object.entries(historyMap).map(
async ([propertyId, blockId]) => { async ([propertyId, blockIds]) => {
const latestValueBlock = ( const blocks = await groupBlock.children();
await groupBlock.children() const latestChangeBlock = blockIds
).find((block: AsyncBlock) => block.id === blockId); .reverse()
.reduce<AsyncBlock>((block, id) => {
if (!block) {
return blocks.find(
block => block.id === id
);
}
return block;
}, null);
return getRecastItemValue( if (latestChangeBlock) {
latestValueBlock return getRecastItemValue(
).getValue(propertyId as RecastPropertyId); latestChangeBlock
).getValue(propertyId as RecastPropertyId);
}
return null;
} }
) )
) )
).filter(v => v); ).filter(v => v);
setHistory(blockHistory); setHistory(blockHistory);
}; };

View File

@@ -4,7 +4,7 @@ import { ModifyPanelContentProps } from './types';
import { StyledDivider, StyledPopoverSubTitle } from '../StyledComponent'; import { StyledDivider, StyledPopoverSubTitle } from '../StyledComponent';
import { BasicSelect } from './Select'; import { BasicSelect } from './Select';
import { InformationProperty, InformationValue } from '../../recast-block'; import { InformationProperty, InformationValue } from '../../recast-block';
import { genInitialOptions, getPendantIconsConfigByName } from '../utils'; import { generateInitialOptions, getPendantIconsConfigByName } from '../utils';
export default (props: ModifyPanelContentProps) => { export default (props: ModifyPanelContentProps) => {
const { onPropertyChange, onValueChange, initialValue, property } = props; const { onPropertyChange, onValueChange, initialValue, property } = props;
@@ -38,7 +38,7 @@ export default (props: ModifyPanelContentProps) => {
}} }}
initialOptions={ initialOptions={
propProperty?.emailOptions || propProperty?.emailOptions ||
genInitialOptions( generateInitialOptions(
property?.type, property?.type,
getPendantIconsConfigByName('Email') getPendantIconsConfigByName('Email')
) )
@@ -66,7 +66,7 @@ export default (props: ModifyPanelContentProps) => {
}} }}
initialOptions={ initialOptions={
propProperty?.phoneOptions || propProperty?.phoneOptions ||
genInitialOptions( generateInitialOptions(
property?.type, property?.type,
getPendantIconsConfigByName('Phone') getPendantIconsConfigByName('Phone')
) )
@@ -94,7 +94,7 @@ export default (props: ModifyPanelContentProps) => {
}} }}
initialOptions={ initialOptions={
propProperty?.locationOptions || propProperty?.locationOptions ||
genInitialOptions( generateInitialOptions(
property?.type, property?.type,
getPendantIconsConfigByName('Location') getPendantIconsConfigByName('Location')
) )

View File

@@ -18,7 +18,9 @@ export default ({
user: { username, nickname, photo }, user: { username, nickname, photo },
} = useUserAndSpaces(); } = useUserAndSpaces();
const [selectedValue, setSelectedValue] = useState(initialValue?.value); const [selectedValue, setSelectedValue] = useState(
initialValue?.value || ''
);
const [focus, setFocus] = useState(false); const [focus, setFocus] = useState(false);
const theme = useTheme(); const theme = useTheme();
return ( return (

View File

@@ -21,7 +21,7 @@ import {
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import { HighLightIconInput } from './IconInput'; import { HighLightIconInput } from './IconInput';
import { PendantConfig, IconNames, OptionIdType, OptionType } from '../types'; import { PendantConfig, IconNames, OptionIdType, OptionType } from '../types';
import { genBasicOption } from '../utils'; import { generateBasicOption } from '../utils';
type OptionItemType = { type OptionItemType = {
option: OptionType; option: OptionType;
@@ -66,7 +66,7 @@ export const BasicSelect = ({
const [selectIds, setSelectIds] = useState<OptionIdType[]>(initialValue); const [selectIds, setSelectIds] = useState<OptionIdType[]>(initialValue);
const insertOption = (insertId: OptionIdType) => { const insertOption = (insertId: OptionIdType) => {
const newOption = genBasicOption({ const newOption = generateBasicOption({
index: options.length + 1, index: options.length + 1,
iconConfig, iconConfig,
}); });

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
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';
@@ -15,13 +14,13 @@ import {
StyledPopoverSubTitle, StyledPopoverSubTitle,
StyledPopoverWrapper, StyledPopoverWrapper,
} from '../StyledComponent'; } from '../StyledComponent';
import { genInitialOptions, getPendantConfigByType } from '../utils'; import {
generateRandomFieldName,
generateInitialOptions,
getPendantConfigByType,
} from '../utils';
import { useOnCreateSure } from './hooks'; import { useOnCreateSure } from './hooks';
const upperFirst = (str: string) => {
return `${str[0].toUpperCase()}${str.slice(1)}`;
};
export const CreatePendantPanel = ({ export const CreatePendantPanel = ({
block, block,
onSure, onSure,
@@ -35,7 +34,7 @@ export const CreatePendantPanel = ({
useEffect(() => { useEffect(() => {
selectedOption && selectedOption &&
setFieldName(upperFirst(`${selectedOption.type}#${nanoid(4)}`)); setFieldName(generateRandomFieldName(selectedOption.type));
}, [selectedOption]); }, [selectedOption]);
return ( return (
@@ -45,7 +44,7 @@ export const CreatePendantPanel = ({
<Select <Select
width={284} width={284}
placeholder="Search for a field type" placeholder="Search for a field type"
value={selectedOption} value={selectedOption ?? null}
onChange={(selectedValue: PendantOptions) => { onChange={(selectedValue: PendantOptions) => {
setSelectedOption(selectedValue); setSelectedOption(selectedValue);
}} }}
@@ -93,7 +92,7 @@ export const CreatePendantPanel = ({
<PendantModifyPanel <PendantModifyPanel
type={selectedOption.type} type={selectedOption.type}
// Select, MultiSelect, Status use this props as initial property // Select, MultiSelect, Status use this props as initial property
initialOptions={genInitialOptions( initialOptions={generateInitialOptions(
selectedOption.type, selectedOption.type,
getPendantConfigByType(selectedOption.type) getPendantConfigByType(selectedOption.type)
)} )}

View File

@@ -4,11 +4,11 @@ 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 {
getRecastItemValue,
type RecastBlockValue, type RecastBlockValue,
type RecastMetaProperty, type RecastMetaProperty,
} from '../../recast-block'; } from '../../recast-block';
import { getPendantConfigByType } from '../utils'; import { getPendantConfigByType } from '../utils';
import { usePendant } from '../use-pendant';
import { import {
StyledPopoverWrapper, StyledPopoverWrapper,
StyledOperationLabel, StyledOperationLabel,
@@ -42,7 +42,8 @@ export const UpdatePendantPanel = ({
}: Props) => { }: Props) => {
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 { removeValue } = getRecastItemValue(block);
const Icon = IconMap[iconConfig.iconName]; const Icon = IconMap[iconConfig.iconName];
const [fieldName, setFieldName] = useState(property.name); const [fieldName, setFieldName] = useState(property.name);
const onUpdateSure = useOnUpdateSure({ block, property }); const onUpdateSure = useOnUpdateSure({ block, property });
@@ -108,7 +109,7 @@ export const UpdatePendantPanel = ({
onDelete={ onDelete={
hasDelete hasDelete
? async () => { ? async () => {
await removePendant(property); await removeValue(property.id);
} }
: null : null
} }

View File

@@ -1,16 +1,23 @@
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { import {
genSelectOptionId, genSelectOptionId,
getRecastItemValue,
type InformationProperty, type InformationProperty,
type MultiSelectProperty, type MultiSelectProperty,
type RecastMetaProperty, type RecastMetaProperty,
type SelectOption, type SelectOption,
type SelectProperty, type SelectProperty,
useRecastBlock,
useRecastBlockMeta, useRecastBlockMeta,
useSelectProperty, useSelectProperty,
SelectValue,
MultiSelectValue,
StatusValue,
InformationValue,
TextValue,
DateValue,
} from '../../recast-block'; } from '../../recast-block';
import { type AsyncBlock } from '../../editor'; import { type AsyncBlock } from '../../editor';
import { usePendant } from '../use-pendant';
import { import {
type OptionType, type OptionType,
PendantTypes, PendantTypes,
@@ -41,8 +48,8 @@ const genOptionWithId = (options: OptionType[] = []) => {
export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => { export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
const { addProperty } = useRecastBlockMeta(); const { addProperty } = useRecastBlockMeta();
const { createSelect } = useSelectProperty(); const { createSelect } = useSelectProperty();
const { setPendant } = usePendant(block); const recastBlock = useRecastBlock();
const { setValue } = getRecastItemValue(block);
return async ({ return async ({
type, type,
fieldName, fieldName,
@@ -79,7 +86,14 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
tempSelectedId: newValue, tempSelectedId: newValue,
}); });
await setPendant(newProperty, selectedId); await setValue(
{
id: newProperty.id,
type: newProperty.type,
value: selectedId,
} as SelectValue | MultiSelectValue | StatusValue,
recastBlock.id
);
} else if (type === PendantTypes.Information) { } else if (type === PendantTypes.Information) {
const emailOptions = genOptionWithId(newPropertyItem.emailOptions); const emailOptions = genOptionWithId(newPropertyItem.emailOptions);
@@ -97,26 +111,33 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
locationOptions, locationOptions,
} as Omit<InformationProperty, 'id'>); } as Omit<InformationProperty, 'id'>);
await setPendant(newProperty, { await setValue(
email: getOfficialSelected({ {
isMulti: true, id: newProperty.id,
options: emailOptions, type: newProperty.type,
tempOptions: newPropertyItem.emailOptions, value: {
tempSelectedId: newValue.email, email: getOfficialSelected({
}), isMulti: true,
phone: getOfficialSelected({ options: emailOptions,
isMulti: true, tempOptions: newPropertyItem.emailOptions,
options: phoneOptions, tempSelectedId: newValue.email,
tempOptions: newPropertyItem.phoneOptions, }),
tempSelectedId: newValue.phone, phone: getOfficialSelected({
}), isMulti: true,
location: getOfficialSelected({ options: phoneOptions,
isMulti: true, tempOptions: newPropertyItem.phoneOptions,
options: locationOptions, tempSelectedId: newValue.phone,
tempOptions: newPropertyItem.locationOptions, }),
tempSelectedId: newValue.location, location: getOfficialSelected({
}), isMulti: true,
}); options: locationOptions,
tempOptions: newPropertyItem.locationOptions,
tempSelectedId: newValue.location,
}),
},
} as InformationValue,
recastBlock.id
);
} else { } else {
// TODO: Color and background should use pendant config, but ui is not design now // TODO: Color and background should use pendant config, but ui is not design now
const iconConfig = getPendantConfigByType(type); const iconConfig = getPendantConfigByType(type);
@@ -129,8 +150,14 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
color: iconConfig.color as CSSProperties['color'], color: iconConfig.color as CSSProperties['color'],
iconName: iconConfig.iconName, iconName: iconConfig.iconName,
}); });
await setValue(
await setPendant(newProperty, newValue); {
id: newProperty.id,
type: newProperty.type,
value: newValue,
} as TextValue | DateValue,
recastBlock.id
);
} }
}; };
}; };
@@ -144,8 +171,9 @@ export const useOnUpdateSure = ({
property: RecastMetaProperty; property: RecastMetaProperty;
}) => { }) => {
const { updateSelect } = useSelectProperty(); const { updateSelect } = useSelectProperty();
const { setPendant } = usePendant(block);
const { updateProperty } = useRecastBlockMeta(); const { updateProperty } = useRecastBlockMeta();
const { setValue } = getRecastItemValue(block);
const recastBlock = useRecastBlock();
return async ({ return async ({
type, type,
@@ -199,7 +227,14 @@ export const useOnUpdateSure = ({
tempSelectedId: newValue, tempSelectedId: newValue,
}); });
await setPendant(selectProperty, selectedId); await setValue(
{
id: selectProperty.id,
type: selectProperty.type,
value: selectedId,
} as SelectValue | MultiSelectValue | StatusValue,
recastBlock.id
);
} else if (type === PendantTypes.Information) { } else if (type === PendantTypes.Information) {
// const { emailOptions, phoneOptions, locationOptions } = // const { emailOptions, phoneOptions, locationOptions } =
// property as InformationProperty; // property as InformationProperty;
@@ -231,28 +266,42 @@ export const useOnUpdateSure = ({
locationOptions, locationOptions,
} as InformationProperty); } as InformationProperty);
await setPendant(newProperty, { await setValue(
email: getOfficialSelected({ {
isMulti: true, id: newProperty.id,
options: emailOptions as SelectOption[], type: newProperty.type,
tempOptions: newPropertyItem.emailOptions, value: {
tempSelectedId: newValue.email, email: getOfficialSelected({
}), isMulti: true,
phone: getOfficialSelected({ options: emailOptions as SelectOption[],
isMulti: true, tempOptions: newPropertyItem.emailOptions,
options: phoneOptions as SelectOption[], tempSelectedId: newValue.email,
tempOptions: newPropertyItem.phoneOptions, }),
tempSelectedId: newValue.phone, phone: getOfficialSelected({
}), isMulti: true,
location: getOfficialSelected({ options: phoneOptions as SelectOption[],
isMulti: true, tempOptions: newPropertyItem.phoneOptions,
options: locationOptions as SelectOption[], tempSelectedId: newValue.phone,
tempOptions: newPropertyItem.locationOptions, }),
tempSelectedId: newValue.location, location: getOfficialSelected({
}), isMulti: true,
}); options: locationOptions as SelectOption[],
tempOptions: newPropertyItem.locationOptions,
tempSelectedId: newValue.location,
}),
},
} as InformationValue,
recastBlock.id
);
} else { } else {
await setPendant(property, newValue); await setValue(
{
id: property.id,
type: property.type,
value: newValue,
} as TextValue | DateValue,
recastBlock.id
);
} }
if (fieldName !== property.name) { if (fieldName !== property.name) {

View File

@@ -1,5 +1,5 @@
import { import {
MuiZoom, MuiFade,
Popover, Popover,
PopperHandler, PopperHandler,
styled, styled,
@@ -100,16 +100,15 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
); );
})} })}
{hasAddBtn ? ( {hasAddBtn ? (
<MuiZoom in={showAddBtn}> <MuiFade in={showAddBtn}>
<div> <div>
<AddPendantPopover <AddPendantPopover
block={block} block={block}
iconStyle={{ marginTop: 4 }} iconStyle={{ marginTop: 4 }}
container={blockRenderContainerRef.current} container={blockRenderContainerRef.current}
trigger="click"
/> />
</div> </div>
</MuiZoom> </MuiFade>
) : null} ) : null}
</BlockPendantContainer> </BlockPendantContainer>
); );

View File

@@ -1,41 +0,0 @@
import { removePropertyValueRecord, setPendantHistory } from './utils';
import { AsyncBlock } from '../editor';
import {
getRecastItemValue,
RecastMetaProperty,
useRecastBlock,
} from '../recast-block';
export const usePendant = (block: AsyncBlock) => {
// const { getProperties, removeProperty } = useRecastBlockMeta();
const recastBlock = useRecastBlock();
const { getValue, setValue, removeValue } = getRecastItemValue(block);
// const { updateSelect } = useSelectProperty();
const setPendant = async (property: RecastMetaProperty, newValue: any) => {
const nv = {
id: property.id,
type: property.type,
value: newValue,
};
await setValue(nv);
setPendantHistory({
recastBlockId: recastBlock.id,
blockId: block.id,
propertyId: property.id,
});
};
const removePendant = async (property: RecastMetaProperty) => {
await removeValue(property.id);
removePropertyValueRecord({
recastBlockId: block.id,
propertyId: property.id,
});
};
return {
setPendant,
removePendant,
};
};

View File

@@ -1,84 +1,7 @@
import { import { PropertyType, SelectOption } from '../recast-block';
PropertyType, import { OptionIdType, OptionType, PendantConfig, PendantTypes } from './types';
RecastBlockValue,
RecastPropertyId,
SelectOption,
} from '../recast-block';
import { OptionIdType, OptionType } from './types';
import { pendantConfig } from './config'; import { pendantConfig } from './config';
import { PendantConfig, PendantTypes } from './types'; import { nanoid } from 'nanoid';
type Props = {
recastBlockId: string;
blockId: string;
propertyId: RecastPropertyId;
};
type StorageMap = {
[recastBlockId: string]: {
[propertyId: RecastPropertyId]: string;
};
};
const LOCAL_STORAGE_NAME = 'TEMPORARY_PENDANT_DATA';
const ensureLocalStorage = () => {
const data = localStorage.getItem(LOCAL_STORAGE_NAME);
if (!data) {
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify({}));
}
};
export const setPendantHistory = ({
recastBlockId,
blockId,
propertyId,
}: Props) => {
ensureLocalStorage();
const data: StorageMap = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_NAME) as string
);
if (!data[recastBlockId]) {
data[recastBlockId] = {};
}
const propertyValueRecord = data[recastBlockId];
propertyValueRecord[propertyId] = blockId;
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
};
export const getPendantHistory = ({
recastBlockId,
}: {
recastBlockId: string;
}) => {
ensureLocalStorage();
const data: StorageMap = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_NAME) as string
);
return data[recastBlockId] ?? {};
};
export const removePropertyValueRecord = ({
recastBlockId,
propertyId,
}: {
recastBlockId: string;
propertyId: RecastPropertyId;
}) => {
ensureLocalStorage();
const data: StorageMap = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_NAME) as string
);
if (!data[recastBlockId]) {
return;
}
delete data[recastBlockId][propertyId];
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
};
/** /**
* In select pendant panel, use mock options instead of use `createSelect` when add or delete option * In select pendant panel, use mock options instead of use `createSelect` when add or delete option
@@ -107,7 +30,7 @@ export const getOfficialSelected = ({
.map(id => { .map(id => {
return tempOptions.findIndex((o: OptionType) => o.id === id); return tempOptions.findIndex((o: OptionType) => o.id === id);
}) })
.filter(index => index != -1); .filter(index => index !== -1);
selectedId = selectedIndex.map((index: number) => { selectedId = selectedIndex.map((index: number) => {
return options[index].id; return options[index].id;
}); });
@@ -130,7 +53,7 @@ export const getPendantIconsConfigByName = (
return pendantConfig[pendantName]; return pendantConfig[pendantName];
}; };
export const genBasicOption = ({ export const generateBasicOption = ({
index, index,
iconConfig, iconConfig,
name = '', name = '',
@@ -159,22 +82,22 @@ export const genBasicOption = ({
/** /**
* Status Pendant is a Select Pendant built-in some options * Status Pendant is a Select Pendant built-in some options
* **/ * **/
export const genInitialOptions = ( export const generateInitialOptions = (
type: PendantTypes, type: PendantTypes,
iconConfig: PendantConfig iconConfig: PendantConfig
) => { ) => {
if (type === PendantTypes.Status) { if (type === PendantTypes.Status) {
return [ return [
genBasicOption({ index: 0, iconConfig, name: 'No Started' }), generateBasicOption({ index: 0, iconConfig, name: 'No Started' }),
genBasicOption({ generateBasicOption({
index: 1, index: 1,
iconConfig, iconConfig,
name: 'In Progress', name: 'In Progress',
}), }),
genBasicOption({ index: 2, iconConfig, name: 'Complete' }), generateBasicOption({ index: 2, iconConfig, name: 'Complete' }),
]; ];
} }
return [genBasicOption({ index: 0, iconConfig })]; return [generateBasicOption({ index: 0, iconConfig })];
}; };
export const checkPendantForm = ( export const checkPendantForm = (
@@ -222,3 +145,10 @@ export const checkPendantForm = (
return { passed: true, message: 'Check passed !' }; return { passed: true, message: 'Check passed !' };
}; };
const upperFirst = (str: string) => {
return `${str[0].toUpperCase()}${str.slice(1)}`;
};
export const generateRandomFieldName = (type: PendantTypes) =>
upperFirst(`${type}#${nanoid(4)}`);

View File

@@ -35,20 +35,6 @@ export class KeyboardManager {
} }
this.handler_map = {}; this.handler_map = {};
// WARNING: Remove the filter of hotkeys, the input event of input/select/textarea will be filtered out by default
// When there is a problem with the input of the text component, you need to pay attention to this
const old_filter = HotKeys.filter;
HotKeys.filter = event => {
let parent = (event.target as Element).parentElement;
while (parent) {
if (parent === editor.container) {
return old_filter(event);
}
parent = parent.parentElement;
}
return true;
};
HotKeys.setScope('editor'); HotKeys.setScope('editor');
// this.init_common_shortcut_cb(); // this.init_common_shortcut_cb();

View File

@@ -6,10 +6,15 @@ import {
PropertyType, PropertyType,
RecastBlockValue, RecastBlockValue,
RecastMetaProperty, RecastMetaProperty,
RecastPropertyId,
} from '../recast-block/types'; } from '../recast-block/types';
import type { DefaultGroup, KanbanGroup } from './types'; import type { DefaultGroup, KanbanGroup } from './types';
import { DEFAULT_GROUP_ID } from './types'; import { DEFAULT_GROUP_ID } from './types';
import {
generateInitialOptions,
generateRandomFieldName,
getPendantIconsConfigByName,
} from '../block-pendant/utils';
import { SelectOption } from '../recast-block';
/** /**
* - If the `groupBy` is `SelectProperty` or `MultiSelectProperty`, return `(Multi)SelectProperty.options`. * - If the `groupBy` is `SelectProperty` or `MultiSelectProperty`, return `(Multi)SelectProperty.options`.
@@ -23,6 +28,7 @@ export const getGroupOptions = async (
return []; return [];
} }
switch (groupBy.type) { switch (groupBy.type) {
case PropertyType.Status:
case PropertyType.Select: case PropertyType.Select:
case PropertyType.MultiSelect: { case PropertyType.MultiSelect: {
return groupBy.options.map(option => ({ return groupBy.options.map(option => ({
@@ -51,12 +57,15 @@ const isValueBelongOption = (
option: KanbanGroup option: KanbanGroup
) => { ) => {
switch (propertyValue.type) { switch (propertyValue.type) {
case PropertyType.Select: { case PropertyType.Select || PropertyType.Status: {
return propertyValue.value === option.id; return propertyValue.value === option.id;
} }
case PropertyType.MultiSelect: { case PropertyType.MultiSelect: {
return propertyValue.value.some(i => i === option.id); return propertyValue.value.some(i => i === option.id);
} }
// case PropertyType.Status: {
// return propertyValue.value === option.id;
// }
// case PropertyType.Text: { // case PropertyType.Text: {
// TOTODO:DO support this type // TOTODO:DO support this type
// } // }
@@ -96,40 +105,67 @@ export const calcCardGroup = (
/** /**
* Set group value for the card block * Set group value for the card block
*/ */
export const moveCardToGroup = async ( export const moveCardToGroup = async ({
groupById: RecastPropertyId, groupBy,
cardBlock: RecastItem, cardBlock,
group: KanbanGroup group,
) => { recastBlock,
}: {
groupBy: RecastMetaProperty;
cardBlock: RecastItem;
group: KanbanGroup;
recastBlock: RecastBlock;
}) => {
const { setValue, removeValue } = getRecastItemValue(cardBlock); const { setValue, removeValue } = getRecastItemValue(cardBlock);
let success = false; let success = false;
if (group.id === DEFAULT_GROUP_ID) { if (group.id === DEFAULT_GROUP_ID) {
success = await removeValue(groupById); success = await removeValue(groupBy.id);
return false; return false;
} }
switch (group.type) { switch (group.type) {
case PropertyType.Select: { case PropertyType.Select: {
success = await setValue({ success = await setValue(
id: groupById, {
type: group.type, id: groupBy.id,
value: group.id, type: group.type,
}); value: group.id,
},
recastBlock.id
);
break;
}
case PropertyType.Status: {
success = await setValue(
{
id: groupBy.id,
type: group.type,
value: group.id,
},
recastBlock.id
);
break; break;
} }
case PropertyType.MultiSelect: { case PropertyType.MultiSelect: {
success = await setValue({ success = await setValue(
id: groupById, {
type: group.type, id: groupBy.id,
value: [group.id], type: group.type,
}); value: [group.id],
},
recastBlock.id
);
break; break;
} }
case PropertyType.Text: { case PropertyType.Text: {
success = await setValue({ success = await setValue(
id: groupById, {
type: group.type, id: groupBy.id,
value: group.id, type: group.type,
}); value: group.id,
},
recastBlock.id
);
break; break;
} }
default: default:
@@ -194,14 +230,18 @@ export const genDefaultGroup = (groupBy: RecastMetaProperty): DefaultGroup => ({
items: [], items: [],
}); });
export const DEFAULT_GROUP_BY_PROPERTY = { export const generateDefaultGroupByProperty = (): {
name: 'Status', name: string;
options: [ options: Omit<SelectOption, 'id'>[];
{ name: 'No Started', color: '#E53535', background: '#FFCECE' }, type: PropertyType.Status;
{ name: 'In Progress', color: '#A77F1A', background: '#FFF5AB' }, } => ({
{ name: 'Complete', color: '#3C8867', background: '#C5FBE0' }, name: generateRandomFieldName(PropertyType.Status),
], type: PropertyType.Status,
}; options: generateInitialOptions(
PropertyType.Status,
getPendantIconsConfigByName(PropertyType.Status)
),
});
/** /**
* Unwrap blocks from the grid recursively. * Unwrap blocks from the grid recursively.

View File

@@ -7,6 +7,7 @@ export const useKanbanGroup = (groupBy: RecastMetaProperty) => {
const { updateSelect } = useSelectProperty(); const { updateSelect } = useSelectProperty();
switch (groupBy.type) { switch (groupBy.type) {
case PropertyType.Status:
case PropertyType.MultiSelect: case PropertyType.MultiSelect:
case PropertyType.Select: { case PropertyType.Select: {
const { const {

View File

@@ -18,8 +18,8 @@ import {
import { supportChildren } from '../utils'; import { supportChildren } from '../utils';
import { import {
calcCardGroup, calcCardGroup,
DEFAULT_GROUP_BY_PROPERTY,
genDefaultGroup, genDefaultGroup,
generateDefaultGroupByProperty,
getCardGroup, getCardGroup,
getGroupOptions, getGroupOptions,
moveCardToAfter, moveCardToAfter,
@@ -48,6 +48,7 @@ export const useRecastKanbanGroupBy = () => {
// Add other type groupBy support // Add other type groupBy support
const supportedGroupBy = getProperties().filter( const supportedGroupBy = getProperties().filter(
prop => prop =>
prop.type === PropertyType.Status ||
prop.type === PropertyType.Select || prop.type === PropertyType.Select ||
prop.type === PropertyType.MultiSelect prop.type === PropertyType.MultiSelect
); );
@@ -88,7 +89,8 @@ export const useRecastKanbanGroupBy = () => {
// TODO: support other property type // TODO: support other property type
if ( if (
groupByProperty.type !== PropertyType.Select && groupByProperty.type !== PropertyType.Select &&
groupByProperty.type !== PropertyType.MultiSelect groupByProperty.type !== PropertyType.MultiSelect &&
groupByProperty.type !== PropertyType.Status
) { ) {
console.warn('Not support groupBy type', groupByProperty); console.warn('Not support groupBy type', groupByProperty);
@@ -134,7 +136,7 @@ export const useInitKanbanEffect = ():
} }
// 3. no group by, no properties // 3. no group by, no properties
// create a new property and set it as group by // create a new property and set it as group by
const prop = await createSelect(DEFAULT_GROUP_BY_PROPERTY); const prop = await createSelect(generateDefaultGroupByProperty());
await setGroupBy(prop.id); await setGroupBy(prop.id);
}; };
@@ -197,7 +199,12 @@ export const useRecastKanban = () => {
beforeBlock: string | null, beforeBlock: string | null,
afterBlock: string | null afterBlock: string | null
) => { ) => {
await moveCardToGroup(groupBy.id, child, kanbanMap[id]); await moveCardToGroup({
groupBy,
cardBlock: child,
group: kanbanMap[id],
recastBlock,
});
if (beforeBlock) { if (beforeBlock) {
const block = await editor.getBlockById( const block = await editor.getBlockById(
beforeBlock beforeBlock
@@ -286,7 +293,12 @@ export const useKanban = () => {
); );
if (isChangedGroup) { if (isChangedGroup) {
// 1.2 Move to the target group // 1.2 Move to the target group
await moveCardToGroup(groupBy.id, targetCard, targetGroup); await moveCardToGroup({
groupBy,
cardBlock: targetCard,
group: targetGroup,
recastBlock,
});
} }
// 2. Reorder the card // 2. Reorder the card
@@ -324,7 +336,12 @@ export const useKanban = () => {
} }
recastBlock.append(newBlock); recastBlock.append(newBlock);
const newCard = newBlock as unknown as RecastItem; const newCard = newBlock as unknown as RecastItem;
await moveCardToGroup(groupBy.id, newCard, group); await moveCardToGroup({
groupBy,
cardBlock: newCard,
group,
recastBlock,
});
}, },
[editor, groupBy.id, recastBlock] [editor, groupBy.id, recastBlock]
); );

View File

@@ -46,7 +46,10 @@ export type DefaultGroup = KanbanGroupBase & {
type SelectGroup = KanbanGroupBase & type SelectGroup = KanbanGroupBase &
SelectOption & { SelectOption & {
type: PropertyType.Select | PropertyType.MultiSelect; type:
| PropertyType.Select
| PropertyType.MultiSelect
| PropertyType.Status;
}; };
type TextGroup = KanbanGroupBase & { type TextGroup = KanbanGroupBase & {

View File

@@ -0,0 +1,84 @@
import { RecastPropertyId } from './types';
// TODO: The logic for keeping history should be supported by the network layer
type Props = {
recastBlockId: string;
blockId: string;
propertyId: RecastPropertyId;
};
type HistoryStorageMap = {
[recastBlockId: string]: {
[propertyId: RecastPropertyId]: string[];
};
};
const LOCAL_STORAGE_NAME = 'TEMPORARY_HISTORY_DATA';
const ensureLocalStorage = () => {
const data = localStorage.getItem(LOCAL_STORAGE_NAME);
if (!data) {
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify({}));
}
};
const ensureHistoryAtom = (
data: HistoryStorageMap,
recastBlockId: string,
propertyId: RecastPropertyId
): HistoryStorageMap => {
if (!data[recastBlockId]) {
data[recastBlockId] = {};
}
if (!data[recastBlockId][propertyId]) {
data[recastBlockId][propertyId] = [];
}
return data;
};
export const setHistory = ({ recastBlockId, blockId, propertyId }: Props) => {
ensureLocalStorage();
const data: HistoryStorageMap = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_NAME) as string
);
ensureHistoryAtom(data, recastBlockId, propertyId);
const propertyHistory = data[recastBlockId][propertyId];
if (propertyHistory.includes(blockId)) {
const idIndex = propertyHistory.findIndex(id => id === blockId);
propertyHistory.splice(idIndex, 1);
}
propertyHistory.push(blockId);
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
};
export const getHistory = ({ recastBlockId }: { recastBlockId: string }) => {
ensureLocalStorage();
const data: HistoryStorageMap = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_NAME) as string
);
return data[recastBlockId] ?? {};
};
export const removeHistory = ({
recastBlockId,
blockId,
propertyId,
}: Props) => {
ensureLocalStorage();
const data: HistoryStorageMap = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_NAME) as string
);
ensureHistoryAtom(data, recastBlockId, propertyId);
const propertyHistory = data[recastBlockId][propertyId];
if (propertyHistory.includes(blockId)) {
const idIndex = propertyHistory.findIndex(id => id === blockId);
propertyHistory.splice(idIndex, 1);
}
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
};

View File

@@ -15,6 +15,7 @@ import {
SelectProperty, SelectProperty,
TABLE_VALUES_KEY, TABLE_VALUES_KEY,
} from './types'; } from './types';
import { getHistory, removeHistory, setHistory } from './history';
/** /**
* Generate a unique id for a property * Generate a unique id for a property
@@ -240,7 +241,13 @@ export const getRecastItemValue = (block: RecastItem | AsyncBlock) => {
return props[id]; return props[id];
}; };
const setValue = (newValue: RecastBlockValue) => { const setValue = (newValue: RecastBlockValue, recastBlockId: string) => {
setHistory({
recastBlockId: recastBlockId,
blockId: block.id,
propertyId: newValue.id,
});
return recastItem.setProperty(TABLE_VALUES_KEY, { return recastItem.setProperty(TABLE_VALUES_KEY, {
...props, ...props,
[newValue.id]: newValue, [newValue.id]: newValue,
@@ -249,22 +256,30 @@ export const getRecastItemValue = (block: RecastItem | AsyncBlock) => {
const removeValue = (propertyId: RecastPropertyId) => { const removeValue = (propertyId: RecastPropertyId) => {
const { [propertyId]: omitted, ...restProps } = props; const { [propertyId]: omitted, ...restProps } = props;
removeHistory({
recastBlockId: block.id,
propertyId: propertyId,
blockId: block.id,
});
return recastItem.setProperty(TABLE_VALUES_KEY, restProps); return recastItem.setProperty(TABLE_VALUES_KEY, restProps);
}; };
return { getAllValue, getValue, setValue, removeValue };
const getValueHistory = getHistory;
return { getAllValue, getValue, setValue, removeValue, getValueHistory };
}; };
const isSelectLikeProperty = ( const isSelectLikeProperty = (
metaProperty?: RecastMetaProperty metaProperty?: RecastMetaProperty
): metaProperty is SelectProperty | MultiSelectProperty => { ): metaProperty is SelectProperty | MultiSelectProperty | StatusProperty => {
if ( return (
!metaProperty || metaProperty &&
(metaProperty.type !== PropertyType.Select && (metaProperty.type === PropertyType.Status ||
metaProperty.type !== PropertyType.MultiSelect) metaProperty.type === PropertyType.Select ||
) { metaProperty.type === PropertyType.MultiSelect)
return false; );
}
return true;
}; };
/** /**
@@ -312,7 +327,7 @@ export const useSelectProperty = () => {
}; };
const updateSelect = ( const updateSelect = (
selectProperty: SelectProperty | MultiSelectProperty selectProperty: StatusProperty | SelectProperty | MultiSelectProperty
) => { ) => {
// if (typeof selectProperty === 'string') { // if (typeof selectProperty === 'string') {
// const maybeSelectProperty = getProperty(selectProperty); // const maybeSelectProperty = getProperty(selectProperty);

View File

@@ -51,6 +51,7 @@ import {
tooltipClasses, tooltipClasses,
Typography, Typography,
Zoom, Zoom,
Fade,
} from '@mui/material'; } from '@mui/material';
export { alpha } from '@mui/system'; export { alpha } from '@mui/system';
@@ -233,6 +234,11 @@ export const MuiInput = Input;
*/ */
export const MuiZoom = Zoom; export const MuiZoom = Zoom;
/**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/
export const MuiFade = Fade;
/** /**
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic. * @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
*/ */