fix interfere in Menu.tsx

This commit is contained in:
DiamondThree
2022-08-03 15:30:02 +08:00
80 changed files with 3509 additions and 1137 deletions
@@ -3,8 +3,12 @@ import { services } from '@toeverything/datasource/db-service';
import type { ReturnEditorBlock } from '@toeverything/datasource/db-service';
import type { TDShape } from '@toeverything/components/board-types';
import { Editor } from '@toeverything/components/board-shapes';
import { usePageClientWidth } from '@toeverything/datasource/state';
export const useShapes = (workspace: string, rootBlockId: string) => {
const { pageClientWidth } = usePageClientWidth();
// page padding left and right total 300px
const editorShapeInitSize = pageClientWidth - 300;
const [blocks, setBlocks] = useState<ReturnEditorBlock[]>();
useEffect(() => {
services.api.editorBlock
@@ -58,8 +62,9 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
acc[block.id] = { ...shapeProps, id: block.id };
} else {
acc[block.id] = Editor.getShape({
point: [groupCount * 740, 200],
point: [groupCount * editorShapeInitSize + 200, 200],
id: block.id,
size: [editorShapeInitSize, 200],
...shapeProps,
affineId: shapeProps.affineId ?? block.id,
workspace: block.workspace,
@@ -64,18 +64,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
};
Component = TDShapeUtil.Component<T, E, TDMeta>(
(
{
shape,
meta: {
app: { useStore },
},
events,
isEditing,
onShapeChange,
},
ref
) => {
({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => {
const containerRef = useRef<HTMLDivElement>();
const {
workspace,
@@ -83,7 +72,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
size: [width, height],
} = shape;
const state = useStore();
const state = app.useStore();
const { currentPageId } = state.appState;
const { editingId } = state.document.pageStates[currentPageId];
const { shapes } = state.document.pages[currentPageId];
@@ -134,11 +123,19 @@ export class EditorUtil extends TDShapeUtil<T, E> {
[isEditing]
);
const activateIfEditing = useCallback(() => {
if (editingText && editingId !== shape.id) {
app.setEditingText(shape.id);
}
}, [app, shape.id, editingText, editingId]);
return (
<HTMLContainer ref={ref} {...events}>
<Container
ref={containerRef}
onPointerDown={stopPropagation}
onMouseEnter={activateIfEditing}
onDragEnter={activateIfEditing}
>
<MemoAffineEditor
workspace={workspace}
@@ -967,6 +967,29 @@ export class TldrawApp extends StateManager<TDSnapshot> {
);
};
/**
* used for EditorUtil only
* @param id
* @returns
*/
setEditingText = (id: string) => {
if (this.readOnly) return;
this.patchState(
{
document: {
pageStates: {
[this.currentPageId]: {
selectedIds: [id],
editingId: id,
},
},
},
},
`set_editing_id`
);
};
/**
* Set or clear the hovered id
* @param id [string]
+1
View File
@@ -149,6 +149,7 @@ export interface TDMeta {
isDarkMode: boolean;
app: {
useStore: () => TDSnapshot;
setEditingText: (id: string) => void;
};
}
@@ -26,7 +26,6 @@ export const PendantHistoryPanel = ({
const { getProperties } = useRecastBlockMeta();
const { getProperty } = useRecastBlockMeta();
const { getAllValue } = getRecastItemValue(block);
const recastBlock = useRecastBlock();
const [history, setHistory] = useState<RecastBlockValue[]>([]);
@@ -34,7 +33,7 @@ export const PendantHistoryPanel = ({
useEffect(() => {
const init = async () => {
const currentBlockValues = getAllValue();
const currentBlockValues = getRecastItemValue(block).getAllValue();
const allProperties = getProperties();
const missProperties = allProperties.filter(
property => !currentBlockValues.find(v => v.id === property.id)
@@ -52,24 +51,26 @@ export const PendantHistoryPanel = ({
return history;
}, {});
const blockHistory = await Promise.all(
Object.entries(historyMap).map(
async ([propertyId, blockId]) => {
const latestValueBlock = (
await groupBlock.children()
).find((block: AsyncBlock) => block.id === blockId);
const blockHistory = (
await Promise.all(
Object.entries(historyMap).map(
async ([propertyId, blockId]) => {
const latestValueBlock = (
await groupBlock.children()
).find((block: AsyncBlock) => block.id === blockId);
return getRecastItemValue(latestValueBlock).getValue(
propertyId as RecastPropertyId
);
}
return getRecastItemValue(
latestValueBlock
).getValue(propertyId as RecastPropertyId);
}
)
)
);
).filter(v => v);
setHistory(blockHistory);
};
init();
}, [getAllValue, getProperties, groupBlock, recastBlock]);
}, [block, getProperties, groupBlock, recastBlock]);
return (
<StyledPendantHistoryPanel>
@@ -1,11 +1,11 @@
import React, { CSSProperties, useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { nanoid } from 'nanoid';
import { Input, Option, Select, Tooltip } from '@toeverything/components/ui';
import { HelpCenterIcon } from '@toeverything/components/icons';
import { AsyncBlock } from '../../editor';
import { IconMap, pendantOptions } from '../config';
import { OptionType, PendantOptions, PendantTypes } from '../types';
import { PendantOptions } from '../types';
import { PendantModifyPanel } from '../pendant-modify-panel';
import {
StyledDivider,
@@ -15,23 +15,13 @@ import {
StyledPopoverSubTitle,
StyledPopoverWrapper,
} from '../StyledComponent';
import {
genSelectOptionId,
InformationProperty,
useRecastBlock,
useRecastBlockMeta,
useSelectProperty,
} from '../../recast-block';
import {
genInitialOptions,
getOfficialSelected,
getPendantConfigByType,
} from '../utils';
import { usePendant } from '../use-pendant';
import { genInitialOptions, getPendantConfigByType } from '../utils';
import { useOnCreateSure } from './hooks';
const upperFirst = (str: string) => {
return `${str[0].toUpperCase()}${str.slice(1)}`;
};
export const CreatePendantPanel = ({
block,
onSure,
@@ -41,9 +31,7 @@ export const CreatePendantPanel = ({
}) => {
const [selectedOption, setSelectedOption] = useState<PendantOptions>();
const [fieldName, setFieldName] = useState<string>('');
const { addProperty, removeProperty } = useRecastBlockMeta();
const { createSelect } = useSelectProperty();
const { setPendant } = usePendant(block);
const onCreateSure = useOnCreateSure({ block });
useEffect(() => {
selectedOption &&
@@ -110,91 +98,13 @@ export const CreatePendantPanel = ({
getPendantConfigByType(selectedOption.type)
)}
iconConfig={getPendantConfigByType(selectedOption.type)}
// isStatusSelect={selectedOption.name === 'Status'}
onSure={async (type, newPropertyItem, newValue) => {
if (!fieldName) {
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);
}
await onCreateSure({
type,
newPropertyItem,
newValue,
fieldName,
});
onSure?.();
}}
/>
@@ -203,10 +113,3 @@ export const CreatePendantPanel = ({
</StyledPopoverWrapper>
);
};
const genOptionWithId = (options: OptionType[] = []) => {
return options.map((option: OptionType) => ({
...option,
id: genSelectOptionId(),
}));
};
@@ -1,27 +1,16 @@
import { useState } from 'react';
import { Input, Tooltip } from '@toeverything/components/ui';
import { HelpCenterIcon } from '@toeverything/components/icons';
import { PendantModifyPanel } from '../pendant-modify-panel';
import type { AsyncBlock } from '../../editor';
import {
genSelectOptionId,
InformationProperty,
type MultiSelectProperty,
type RecastBlockValue,
type RecastMetaProperty,
type SelectOption,
type SelectProperty,
useRecastBlockMeta,
useSelectProperty,
} from '../../recast-block';
import { OptionType, PendantTypes, TempInformationType } from '../types';
import {
getOfficialSelected,
getPendantConfigByType,
// getPendantIconsConfigByNameOrType,
} from '../utils';
import { getPendantConfigByType } from '../utils';
import { usePendant } from '../use-pendant';
import {
StyledPopoverWrapper,
StyledOperationTitle,
StyledOperationLabel,
StyledInputEndAdornment,
StyledDivider,
@@ -29,10 +18,8 @@ import {
StyledPopoverSubTitle,
} from '../StyledComponent';
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 = {
value: RecastBlockValue;
@@ -53,13 +40,12 @@ export const UpdatePendantPanel = ({
onCancel,
titleEditable = false,
}: Props) => {
const { updateSelect } = useSelectProperty();
const { setPendant, removePendant } = usePendant(block);
const pendantOption = pendantOptions.find(v => v.type === property.type);
const iconConfig = getPendantConfigByType(property.type);
const { removePendant } = usePendant(block);
const Icon = IconMap[iconConfig.iconName];
const { updateProperty } = useRecastBlockMeta();
const [fieldTitle, setFieldTitle] = useState(property.name);
const [fieldName, setFieldName] = useState(property.name);
const onUpdateSure = useOnUpdateSure({ block, property });
return (
<StyledPopoverWrapper>
@@ -77,10 +63,10 @@ export const UpdatePendantPanel = ({
<StyledOperationLabel>Field Title</StyledOperationLabel>
{titleEditable ? (
<Input
value={fieldTitle}
value={fieldName}
placeholder="Input your field name here"
onChange={e => {
setFieldTitle(e.target.value);
setFieldName(e.target.value);
}}
endAdornment={
<Tooltip content="Help info here">
@@ -111,114 +97,12 @@ export const UpdatePendantPanel = ({
property={property}
type={property.type}
onSure={async (type, newPropertyItem, newValue) => {
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 (fieldTitle !== property.name) {
await updateProperty({
...property,
name: fieldTitle,
});
}
await onUpdateSure({
type,
newPropertyItem,
newValue,
fieldName,
});
onSure?.();
}}
onDelete={
@@ -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,
});
}
};
};
@@ -1,4 +1,5 @@
import {
PropertyType,
RecastBlockValue,
RecastPropertyId,
SelectOption,
@@ -175,3 +176,49 @@ export const genInitialOptions = (
}
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 !' };
};
@@ -297,10 +297,10 @@ export class ScrollManager {
}
public lock() {
this._scrollController.lockScroll();
this._scrollController?.lockScroll();
}
public unLock() {
this._scrollController.unLockScroll();
this._scrollController?.unLockScroll();
}
}
@@ -23,6 +23,7 @@ import type { DragDropManager } from './drag-drop';
import { MouseManager } from './mouse';
import { Observable } from 'rxjs';
import { Point } from '@toeverything/utils';
import { ScrollManager } from './scroll';
// import { BrowserClipboard } from './clipboard/browser-clipboard';
@@ -63,6 +64,7 @@ export interface VirgoSelection {
// Editor's external API
export interface Virgo {
selectionManager: SelectionManager;
scrollManager: ScrollManager;
createBlock: (
type: keyof BlockFlavors,
parentId?: string
@@ -40,26 +40,30 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
bottom: 0,
});
const [search_text, set_search_text] = useState<string>('');
const [search_blocks, set_search_blocks] = useState<QueryResult>([]);
const [searchText, setSearchText] = useState<string>('');
const [searchBlocks, setSearchBlocks] = useState<QueryResult>([]);
const commandMenuContentRef = useRef();
// TODO: Two-way link to be developed
// useEffect(() => {
// QueryBlocks(editor, search_text, result => set_search_blocks(result));
// }, [editor, search_text]);
// QueryBlocks(editor, searchText, result => set_searchBlocks(result));
// }, [editor, searchText]);
const hideMenu = () => {
setShow(false);
editor.scrollManager.unLock();
};
const [types, categories] = useMemo(() => {
const types: Array<BlockFlavorKeys | string> = [];
const categories: Array<CommandMenuCategories> = [];
if (search_blocks.length) {
Object.values(search_blocks).forEach(({ id }) => types.push(id));
if (searchBlocks.length) {
Object.values(searchBlocks).forEach(({ id }) => types.push(id));
categories.push(CommandMenuCategories.pages);
}
Object.entries(menuItemsMap).forEach(([category, itemInfoList]) => {
itemInfoList.forEach(info => {
if (
!search_text ||
info.text.toLowerCase().includes(search_text.toLowerCase())
!searchText ||
info.text.toLowerCase().includes(searchText.toLowerCase())
) {
types.push(info.type);
}
@@ -73,9 +77,9 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
});
});
return [types, categories];
}, [search_blocks, search_text]);
}, [searchBlocks, searchText]);
const check_if_show_command_menu = useCallback(
const checkIfShowCommandMenu = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
const { type, anchorNode } = editor.selection.currentSelectInfo;
if (event.key === '/' && type === 'Range') {
@@ -101,8 +105,9 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
);
}
});
set_search_text('');
setSearchText('');
setShow(true);
editor.scrollManager.lock();
const rect =
editor.selection.currentSelectInfo?.browserSelection
?.getRangeAt(0)
@@ -137,12 +142,12 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
[editor, blockId]
);
const handle_click_others = useCallback(
const handleClickOthers = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (show) {
const { anchorNode } = editor.selection.currentSelectInfo;
if (anchorNode.id !== blockId) {
setShow(false);
hideMenu();
return;
}
setTimeout(() => {
@@ -150,12 +155,12 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
editor.blockHelper.getSearchSlashText(blockId);
// check if has search text
if (searchText && searchText.startsWith('/')) {
set_search_text(searchText.slice(1));
setSearchText(searchText.slice(1));
} else {
setShow(false);
hideMenu();
}
if (searchText.length > 6 && !types.length) {
setShow(false);
hideMenu();
}
});
}
@@ -163,18 +168,18 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
[editor, show, blockId, types]
);
const handle_keyup = useCallback(
const handleKeyup = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
check_if_show_command_menu(event);
handle_click_others(event);
checkIfShowCommandMenu(event);
handleClickOthers(event);
},
[check_if_show_command_menu, handle_click_others]
[checkIfShowCommandMenu, handleClickOthers]
);
const handle_key_down = useCallback(
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Escape') {
setShow(false);
hideMenu();
}
},
[]
@@ -183,23 +188,23 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
useEffect(() => {
const sub = hooks
.get(HookType.ON_ROOT_NODE_KEYUP)
.subscribe(handle_keyup);
.subscribe(handleKeyup);
sub.add(
hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handle_key_down)
.subscribe(handleKeyDown)
);
return () => {
sub.unsubscribe();
};
}, [handle_keyup, handle_key_down, hooks]);
}, [handleKeyup, handleKeyDown, hooks]);
const handle_click_away = () => {
setShow(false);
const handleClickAway = () => {
hideMenu();
};
const handle_selected = async (type: BlockFlavorKeys | string) => {
const handleSelected = async (type: BlockFlavorKeys | string) => {
const text = await editor.commands.textCommands.getBlockText(blockId);
editor.blockHelper.removeSearchSlash(blockId, true);
if (type.startsWith('Virgo')) {
@@ -224,21 +229,21 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
block.firstCreateFlag = true;
}
}
setShow(false);
hideMenu();
};
const handle_close = () => {
const handleClose = () => {
editor.blockHelper.removeSearchSlash(blockId);
};
return (
<div
style={{ zIndex: 1 }}
onKeyUpCapture={handle_keyup}
onKeyUpCapture={handleKeyup}
ref={commandMenuContentRef}
>
{show ? (
<MuiClickAwayListener onClickAway={handle_click_away}>
<MuiClickAwayListener onClickAway={handleClickAway}>
<div>
<CommandMenuContainer
editor={editor}
@@ -249,9 +254,9 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
}}
isShow={show}
blockId={blockId}
onSelected={handle_selected}
onclose={handle_close}
searchBlocks={search_blocks}
onSelected={handleSelected}
onclose={handleClose}
searchBlocks={searchBlocks}
types={types}
categories={categories}
/>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M11.04 12.96V18h1.92v-5.04H18v-1.92h-5.04V6h-1.92v5.04H6v1.92h5.04Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 185 B

@@ -0,0 +1,21 @@
import { FC } from 'react';
// eslint-disable-next-line no-restricted-imports
import { SvgIcon } from '@mui/material';
// eslint-disable-next-line no-restricted-imports
import type { SvgIconProps } from '@mui/material';
export interface AddIconProps extends Omit<SvgIconProps, 'color'> {
color?: string
}
export const AddIcon: FC<AddIconProps> = ({ color, style, ...props}) => {
const propsStyles = {"color": color};
const customStyles = {};
const styles = {...propsStyles, ...customStyles, ...style}
return (
<SvgIcon style={styles} {...props}>
<path fillRule="evenodd" d="M11.04 12.96V18h1.92v-5.04H18v-1.92h-5.04V6h-1.92v5.04H6v1.92h5.04Z" clipRule="evenodd" />
</SvgIcon>
)
};
@@ -1,4 +1,4 @@
export const timestamp = 1659000896562;
export const timestamp = 1659423582387;
export * from './image/image';
export * from './format-clear/format-clear';
export * from './backward-undo/backward-undo';
@@ -125,4 +125,8 @@ export * from './eraser/eraser';
export * from './group-by/group-by';
export * from './layout/layout';
export * from './lock/lock';
export * from './unlock/unlock';
export * from './unlock/unlock';
export * from './more-tags-an-subblocks/more-tags-an-subblocks';
export * from './page-in-page-tree/page-in-page-tree';
export * from './pin/pin';
export * from './add/add';
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M2.2 3v13A3.8 3.8 0 0 0 6 19.8h10.316A2.801 2.801 0 0 0 21.8 19a2.8 2.8 0 0 0-5.484-.8H6A2.2 2.2 0 0 1 3.8 16v-6h12.584a2.801 2.801 0 1 0-.12-1.6H3.8V3H2.2ZM19 7.8a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4ZM17.8 19a1.2 1.2 0 1 1 2.4 0 1.2 1.2 0 0 1-2.4 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 371 B

@@ -0,0 +1,21 @@
import { FC } from 'react';
// eslint-disable-next-line no-restricted-imports
import { SvgIcon } from '@mui/material';
// eslint-disable-next-line no-restricted-imports
import type { SvgIconProps } from '@mui/material';
export interface MoreTagsAnSubblocksIconProps extends Omit<SvgIconProps, 'color'> {
color?: string
}
export const MoreTagsAnSubblocksIcon: FC<MoreTagsAnSubblocksIconProps> = ({ color, style, ...props}) => {
const propsStyles = {"color": color};
const customStyles = {};
const styles = {...propsStyles, ...customStyles, ...style}
return (
<SvgIcon style={styles} {...props}>
<path fillRule="evenodd" d="M2.2 3v13A3.8 3.8 0 0 0 6 19.8h10.316A2.801 2.801 0 0 0 21.8 19a2.8 2.8 0 0 0-5.484-.8H6A2.2 2.2 0 0 1 3.8 16v-6h12.584a2.801 2.801 0 1 0-.12-1.6H3.8V3H2.2ZM19 7.8a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4ZM17.8 19a1.2 1.2 0 1 1 2.4 0 1.2 1.2 0 0 1-2.4 0Z" clipRule="evenodd" />
</SvgIcon>
)
};
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.5"/></svg>

After

Width:  |  Height:  |  Size: 99 B

@@ -0,0 +1,21 @@
import { FC } from 'react';
// eslint-disable-next-line no-restricted-imports
import { SvgIcon } from '@mui/material';
// eslint-disable-next-line no-restricted-imports
import type { SvgIconProps } from '@mui/material';
export interface PageInPageTreeIconProps extends Omit<SvgIconProps, 'color'> {
color?: string
}
export const PageInPageTreeIcon: FC<PageInPageTreeIconProps> = ({ color, style, ...props}) => {
const propsStyles = {"color": color};
const customStyles = {};
const styles = {...propsStyles, ...customStyles, ...style}
return (
<SvgIcon style={styles} {...props}>
<circle cx={12} cy={12} r={2.5} />
</SvgIcon>
)
};
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g clip-path="url(#a)"><path fill-rule="evenodd" d="M20.403 7.84a2 2 0 0 1 0 2.828l-2.12 2.12-1.414-1.414-2.596 2.595a6.003 6.003 0 0 1-1.172 6.83l-4.243-4.243-3.834 3.834a1 1 0 1 1-1.414-1.414l3.834-3.834L3.2 10.9a6.002 6.002 0 0 1 6.83-1.173l2.595-2.596-1.414-1.414 2.12-2.12a2 2 0 0 1 2.829 0l4.242 4.242Z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path d="M0 0H24V24H0z" transform="matrix(-1 0 0 1 24 0)"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 501 B

@@ -0,0 +1,21 @@
import { FC } from 'react';
// eslint-disable-next-line no-restricted-imports
import { SvgIcon } from '@mui/material';
// eslint-disable-next-line no-restricted-imports
import type { SvgIconProps } from '@mui/material';
export interface PinIconProps extends Omit<SvgIconProps, 'color'> {
color?: string
}
export const PinIcon: FC<PinIconProps> = ({ color, style, ...props}) => {
const propsStyles = {"color": color};
const customStyles = {};
const styles = {...propsStyles, ...customStyles, ...style}
return (
<SvgIcon style={styles} {...props}>
<g clipPath="url(#a)"><path fillRule="evenodd" d="M20.403 7.84a2 2 0 0 1 0 2.828l-2.12 2.12-1.414-1.414-2.596 2.595a6.003 6.003 0 0 1-1.172 6.83l-4.243-4.243-3.834 3.834a1 1 0 1 1-1.414-1.414l3.834-3.834L3.2 10.9a6.002 6.002 0 0 1 6.83-1.173l2.595-2.596-1.414-1.414 2.12-2.12a2 2 0 0 1 2.829 0l4.242 4.242Z" clipRule="evenodd" /></g><defs><clipPath id="a"><path d="M0 0H24V24H0z" transform="matrix(-1 0 0 1 24 0)" /></clipPath></defs>
</SvgIcon>
)
};
@@ -3,13 +3,14 @@ import {
LogoIcon,
SideBarViewIcon,
SearchIcon,
SideBarViewCloseIcon,
} from '@toeverything/components/icons';
import { useShowSettingsSidebar } from '@toeverything/datasource/state';
import { CurrentPageTitle } from './Title';
import { EditorBoardSwitcher } from './EditorBoardSwitcher';
export const LayoutHeader = () => {
const { toggleSettingsSidebar: toggleInfoSidebar } =
const { toggleSettingsSidebar: toggleInfoSidebar, showSettingsSidebar } =
useShowSettingsSidebar();
return (
@@ -31,7 +32,11 @@ export const LayoutHeader = () => {
</div>
<IconButton onClick={toggleInfoSidebar} size="large">
<SideBarViewIcon />
{showSettingsSidebar ? (
<SideBarViewIcon />
) : (
<SideBarViewCloseIcon />
)}
</IconButton>
</StyledHelper>
</FlexContainer>
@@ -50,31 +50,35 @@ export const Activities = () => {
const [recentPages, setRecentPages] = useState([]);
const userId = user?.id;
const fetchRecentPages = useCallback(async () => {
if (!userId || !currentSpaceId) {
return;
}
const recent_pages = await services.api.userConfig.getRecentPages(
currentSpaceId,
userId
);
setRecentPages(recent_pages);
}, [userId, currentSpaceId]);
/* temporarily remove:show recently viewed documents */
// const fetchRecentPages = useCallback(async () => {
// if (!userId || !currentSpaceId) {
// return;
// }
// const recent_pages = await services.api.userConfig.getRecentPages(
// currentSpaceId,
// userId
// );
// setRecentPages(recent_pages);
// }, [userId, currentSpaceId]);
useEffect(() => {
(async () => {
await fetchRecentPages();
})();
}, [fetchRecentPages]);
// useEffect(() => {
// (async () => {
// await fetchRecentPages();
// })();
// }, [fetchRecentPages]);
/* show recently edit documents */
const getRecentEditPages = async (state, block) => {
console.log(state, await block.children());
};
useEffect(() => {
let unobserve: () => void;
const observe = async () => {
unobserve = await services.api.userConfig.observe(
{ workspace: currentSpaceId },
() => {
fetchRecentPages();
}
getRecentEditPages
);
};
observe();
@@ -82,7 +86,7 @@ export const Activities = () => {
return () => {
unobserve?.();
};
}, [currentSpaceId, fetchRecentPages]);
}, [currentSpaceId, getRecentEditPages]);
return (
<StyledWrapper>
@@ -95,30 +95,37 @@ export function DndTree(props: DndTreeProps) {
>
{/* <button onClick={() => handleAdd()}> add top node</button> */}
{flattenedItems.map(
({ id, title, children, collapsed, depth }) => (
<DndTreeItem
key={id}
id={id}
// value={id}
value={title}
collapsed={Boolean(collapsed && children.length)}
depth={
id === activeId && projected
? projected.depth
: depth
}
indentationWidth={indentationWidth}
indicator={showDragIndicator}
onCollapse={
collapsible && children.length
? () => handleCollapse(id)
: undefined
}
onRemove={
removable ? () => handleRemove(id) : undefined
}
/>
)
({ id, title, children, collapsed, depth }) => {
return (
<DndTreeItem
key={id}
id={id}
// value={id}
value={title}
collapsed={Boolean(
collapsed && children.length
)}
depth={
id === activeId && projected
? projected.depth
: depth
}
indentationWidth={indentationWidth}
indicator={showDragIndicator}
childCount={children.length}
onCollapse={
collapsible && children.length
? () => handleCollapse(id)
: undefined
}
onRemove={
removable
? () => handleRemove(id)
: undefined
}
/>
);
}
)}
<DragOverlay
dropAnimation={dropAnimation}
@@ -4,6 +4,9 @@ import {
Cascader,
CascaderItemProps,
MuiDivider as Divider,
MuiClickAwayListener as ClickAwayListener,
IconButton,
styled,
} from '@toeverything/components/ui';
import React from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
@@ -11,6 +14,8 @@ import { copyToClipboard } from '@toeverything/utils';
import { services, TemplateFactory } from '@toeverything/datasource/db-service';
import { NewFromTemplatePortal } from './NewFromTemplatePortal';
import { useFlag } from '@toeverything/datasource/feature-flags';
import { MoreIcon, AddIcon } from '@toeverything/components/icons';
const MESSAGES = {
COPY_LINK_SUCCESS: 'Copyed link to clipboard',
};
@@ -19,6 +24,12 @@ interface ActionsProps {
pageId: string;
onRemove: () => void;
}
const StyledAction = styled('div')({
display: 'flex',
alignItems: 'center',
});
function DndTreeItemMoreActions(props: ActionsProps) {
const [alert_open, set_alert_open] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState<HTMLDivElement | null>(null);
@@ -233,29 +244,42 @@ function DndTreeItemMoreActions(props: ActionsProps) {
];
return (
<>
<span
className={styles['TreeItemMoreActions']}
onClick={handleClick}
>
···
</span>
<Cascader
items={menuList}
anchorEl={anchorEl}
placement="right-start"
open={open}
onClose={handleClose}
></Cascader>
<Snackbar
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
open={alert_open}
message={MESSAGES.COPY_LINK_SUCCESS}
key={'bottomcenter'}
autoHideDuration={2000}
onClose={handle_alert_close}
/>
</>
<ClickAwayListener onClickAway={() => handleClose()}>
<div>
<div className={styles['TreeItemMoreActions']}>
<StyledAction>
<IconButton
size="small"
onClick={handle_new_child_page}
>
<AddIcon />
</IconButton>
<IconButton
size="small"
hoverColor="#E0E6EB"
onClick={handleClick}
>
<MoreIcon />
</IconButton>
</StyledAction>
</div>
<Cascader
items={menuList}
anchorEl={anchorEl}
placement="right-start"
open={open}
onClose={handleClose}
></Cascader>
<Snackbar
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
open={alert_open}
message={MESSAGES.COPY_LINK_SUCCESS}
key={'bottomcenter'}
autoHideDuration={2000}
onClose={handle_alert_close}
/>
</div>
</ClickAwayListener>
);
}
@@ -65,7 +65,7 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
const navigate = useNavigate();
const BooleanPageTreeItemMoreActions = useFlag(
'BooleanPageTreeItemMoreActions',
false
true
);
return (
<li
@@ -94,7 +94,12 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
title={value}
>
<Action onClick={onCollapse}>
{collapsed ? <ArrowRightIcon /> : <ArrowDropDownIcon />}
{childCount !== 0 &&
(collapsed ? (
<ArrowRightIcon />
) : (
<ArrowDropDownIcon />
))}
</Action>
{/*<Action>*/}
{/* <DocumentIcon />*/}
@@ -83,7 +83,7 @@
justify-content: space-around;
background-color: #fff;
color: #4c6275;
padding: 0 0.5rem;
padding-right: 0.5rem;
overflow: hidden;
.TreeItemMoreActions {
+38 -36
View File
@@ -36,6 +36,7 @@ interface IconButtonProps {
style?: CSSProperties;
className?: string;
size?: SizeType;
hoverColor?: string;
}
export const IconButton: FC<PropsWithChildren<IconButtonProps>> = ({
@@ -57,47 +58,48 @@ export const IconButton: FC<PropsWithChildren<IconButtonProps>> = ({
);
};
const Container = styled('button')<{ size?: SizeType }>(
({ theme, size = SIZE_MIDDLE }) => {
const { iconSize, areaSize } = SIZE_CONFIG[size];
const Container = styled('button')<{
size?: SizeType;
hoverColor?: string;
}>(({ theme, size = SIZE_MIDDLE, hoverColor }) => {
const { iconSize, areaSize } = SIZE_CONFIG[size];
return {
return {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: areaSize,
height: areaSize,
backgroundColor: 'transparent',
color: theme.affine.palette.icons,
padding: theme.affine.spacing.iconPadding,
borderRadius: '3px',
'& svg': {
width: iconSize,
height: iconSize,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: areaSize,
height: areaSize,
backgroundColor: theme.affine.palette.white,
color: theme.affine.palette.icons,
padding: theme.affine.spacing.iconPadding,
borderRadius: '5px',
},
'& svg': {
width: iconSize,
height: iconSize,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
'&:hover': {
backgroundColor: hoverColor || theme.affine.palette.hover,
},
'&:hover': {
backgroundColor: theme.affine.palette.hover,
},
[`&${buttonStatus.hover}`]: {
backgroundColor: theme.affine.palette.hover,
},
[`&${buttonStatus.hover}`]: {
backgroundColor: theme.affine.palette.hover,
},
'&:focus': {
color: theme.affine.palette.primary,
},
[`&.${buttonStatus.focus}`]: {
color: theme.affine.palette.primary,
},
'&:focus': {
color: theme.affine.palette.primary,
},
[`&.${buttonStatus.focus}`]: {
color: theme.affine.palette.primary,
},
[`&${buttonStatus.disabled}`]: {
cursor: 'not-allowed',
},
};
}
);
[`&${buttonStatus.disabled}`]: {
cursor: 'not-allowed',
},
};
});