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',
},
};
});
@@ -0,0 +1,237 @@
{
"type": "group",
"properties": {},
"blocks": [
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "As a collaborative real-time editor, AFFiNE aims to resolve problems in three situations:"
}
]
}
},
"blocks": []
},
{
"type": "bullet",
"properties": {
"text": {
"value": [
{
"text": "Multi-master replication: the synchronization of data between equipment and applications;"
}
]
}
},
"blocks": []
},
{
"type": "bullet",
"properties": {
"text": {
"value": [
{
"text": "Eventual consistency: the consistence of data regardless of network latency and outage;"
}
]
}
},
"blocks": []
},
{
"type": "bullet",
"properties": {
"text": {
"value": [
{
"text": "Conflict resolution: the resolution of conflict between simultaneous edits."
}
]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "To achieve these aims, a proper collaborative algorithm should be used. There are hundreds of collaborative algorithms being invented over the past three decades, but they usually fall into two categories: either operational transformation (OT) or conflict-free replicated data type (CRDT). We think CRDT is a better choice."
}
]
}
},
"blocks": []
},
{
"type": "heading1",
"properties": {
"text": {
"value": [{ "bold": true, "text": "What does CRDT do" }]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "CRDT is capable of discovering and resolving conflicts while ensuring the effective distribution and merge of date. It may sound like magic, but think about historical study, it is very possible to discover the truth from scattered evidence."
}
]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "For CRDT, every piece of data is like a \"historic fragment\". We keep collecting the fragments from other clients and then restore the truth by excluding repeated data and correcting false information."
}
]
}
},
"blocks": []
},
{
"type": "heading1",
"properties": {
"text": {
"value": [{ "bold": true, "text": "Why CRDT is better" }]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "In contrast to OT, CRDT possesses three big advantages:"
}
]
}
},
"blocks": []
},
{
"type": "heading2",
"properties": {
"text": {
"value": [{ "bold": true, "text": "Flexibility" }]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "CRDT supports more data types. For example, Yjs supports Array, Map, and Treelike, and therefore applies to more business scenarios."
}
]
}
},
"blocks": []
},
{
"type": "heading2",
"properties": {
"text": {
"value": [{ "bold": true, "text": "Performance" }]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "CRDT tolerates higher latency and can wait longer for solving conflicts, whereas the calculation of OT in the same condition may become too overwhelming for a server to sustain."
}
]
}
},
"blocks": []
},
{
"type": "heading2",
"properties": {
"text": {
"value": [{ "bold": true, "text": "Extensibility" }]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "Because CRDT supports more data types and editor elements, it is more extensible."
}
]
}
},
"blocks": []
},
{
"type": "heading1",
"properties": {
"text": { "value": [{ "text": "Conclusion" }] }
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice."
}
]
}
},
"blocks": []
},
{
"type": "text",
"properties": { "text": { "value": [{ "text": "" }] } },
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{ "text": "" },
{
"type": "link",
"url": "https://blog.affine.pro/",
"id": "link.stubssslo0rq",
"children": [{ "text": "← View all posts" }]
},
{ "text": "" }
]
}
},
"blocks": []
}
]
}
@@ -0,0 +1,15 @@
{
"type": "group",
"properties": {},
"blocks": [
{
"type": "text",
"properties": {
"text": {
"value": [{ "text": "" }]
}
},
"blocks": []
}
]
}
@@ -0,0 +1,159 @@
{
"type": "group",
"properties": {},
"blocks": [
{
"type": "heading2",
"properties": {
"text": {
"value": [
{
"bold": true,
"text": "Performance"
}
]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "CRDT tolerates higher latency and can wait longer for solving conflicts, whereas the calculation of OT in the same condition may become too overwhelming for a server to sustain."
}
]
}
},
"blocks": []
},
{
"type": "heading2",
"properties": {
"text": {
"value": [
{
"bold": true,
"text": "Extensibility"
}
]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "Because CRDT supports more data types and editor elements, it is more extensible."
}
]
}
},
"blocks": []
},
{
"type": "heading1",
"properties": {
"text": {
"value": [
{
"text": "Conclusion"
}
]
}
},
"blocks": []
},
{
"type": "grid",
"properties": {},
"blocks": [
{
"type": "gridItem",
"properties": {
"gridItemWidth": "50%"
},
"blocks": [
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice."
}
]
}
},
"blocks": []
}
]
},
{
"type": "gridItem",
"properties": {
"gridItemWidth": "50%"
},
"blocks": [
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": "Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice."
}
]
}
},
"blocks": []
}
]
}
]
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": ""
}
]
}
},
"blocks": []
},
{
"type": "text",
"properties": {
"text": {
"value": [
{
"text": ""
},
{
"type": "link",
"url": "https://blog.affine.pro/",
"id": "link.stubssslo0rq",
"children": [
{
"text": "← View all posts"
}
]
},
{
"text": ""
}
]
}
}
}
]
}
@@ -1,592 +0,0 @@
//@ts-nocheck
import { GroupTemplate } from './types';
type GroupTemplateMap = Record<GroupTemplateKeys, GroupTemplate>;
const groupTemplateMap: GroupTemplateMap = {
empty: {
type: 'group',
properties: {},
blocks: [
{
type: 'text',
properties: {
text: {
value: [{ text: '' }],
},
},
blocks: [],
},
],
},
todolist: {
type: 'group',
properties: {},
blocks: [
{
type: 'heading1',
properties: {
text: {
value: [{ text: '🎓Graduating from the project' }],
},
},
blocks: [],
},
{
type: 'todo',
properties: {
text: {
value: [
{
text: 'Congratulations! Now you can start create your own projects!',
},
],
},
},
blocks: [],
},
{
type: 'todo',
properties: {
text: {
value: [
{
text: 'To start using workspaces and folders hit the ',
},
{
bold: true,
text: 'button at the top left of the screen',
},
],
},
},
blocks: [],
},
{
type: 'todo',
properties: {
text: {
value: [
{
text: '',
},
{
text: 'At any time if you feel lost do visit our 📚Blog: ',
},
{
type: 'link',
url: 'https://blog.affine.pro/',
id: 'link.qx4yhw81or54',
children: [
{
text: 'https://blog.affine.pro',
},
],
},
{
text: '',
},
],
},
collapsed: {
value: false,
},
},
blocks: [],
},
{
type: 'todo',
properties: {
text: {
value: [
{
text: '',
},
{
text: 'If you have any suggestions drop a post in our Reddit Channel',
},
{
type: 'link',
url: 'https://www.reddit.com/r/Affine/',
id: 'link.zeafc4ogfvrb',
children: [
{
text: 'https://www.reddit.com/r/Affine/',
},
],
},
{
text: ' ',
},
],
},
},
blocks: [],
},
{
type: 'heading2',
properties: {
text: {
value: [
{
text: '🎉 The Essentials. Check things off after you tried them!',
},
],
},
},
blocks: [],
},
{
type: 'todo',
properties: {
text: {
value: [
{ text: ' ✅ ' },
{ bold: true, text: 'Check' },
{
text: ' the text box here to complete the task!',
},
],
},
},
blocks: [],
},
{
type: 'todo',
properties: {
text: {
value: [
{ text: '' },
{ text: '' },
{ text: ' 👋 ' },
{ bold: true, text: 'Drag' },
{
text: ' the ⠟ button left of the checkbox to reorder tasks',
},
{ text: '' },
{ text: '' },
],
},
},
blocks: [],
},
{
type: 'todo',
properties: {
text: {
value: [
{ text: '' },
{ text: '' },
{ text: ' ➡️ ' },
{ bold: true, text: 'Fold' },
{ text: ' and ' },
{ bold: true, text: 'Unfold' },
{
text: ' a task to simplify your list using the arrow on the right ⤵️',
},
],
},
numberType: 'type1',
collapsed: { value: false },
},
},
],
},
blog: {
type: 'group',
properties: {},
blocks: [
{
type: 'text',
properties: {
text: {
value: [
{
text: 'As a collaborative real-time editor, AFFiNE aims to resolve problems in three situations:',
},
],
},
},
blocks: [],
},
{
type: 'bullet',
properties: {
text: {
value: [
{
text: 'Multi-master replication: the synchronization of data between equipment and applications;',
},
],
},
},
blocks: [],
},
{
type: 'bullet',
properties: {
text: {
value: [
{
text: 'Eventual consistency: the consistence of data regardless of network latency and outage;',
},
],
},
},
blocks: [],
},
{
type: 'bullet',
properties: {
text: {
value: [
{
text: 'Conflict resolution: the resolution of conflict between simultaneous edits.',
},
],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'To achieve these aims, a proper collaborative algorithm should be used. There are hundreds of collaborative algorithms being invented over the past three decades, but they usually fall into two categories: either operational transformation (OT) or conflict-free replicated data type (CRDT). We think CRDT is a better choice.',
},
],
},
},
blocks: [],
},
{
type: 'heading1',
properties: {
text: {
value: [{ bold: true, text: 'What does CRDT do' }],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'CRDT is capable of discovering and resolving conflicts while ensuring the effective distribution and merge of date. It may sound like magic, but think about historical study, it is very possible to discover the truth from scattered evidence.',
},
],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'For CRDT, every piece of data is like a "historic fragment". We keep collecting the fragments from other clients and then restore the truth by excluding repeated data and correcting false information.',
},
],
},
},
blocks: [],
},
{
type: 'heading1',
properties: {
text: {
value: [{ bold: true, text: 'Why CRDT is better' }],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'In contrast to OT, CRDT possesses three big advantages:',
},
],
},
},
blocks: [],
},
{
type: 'heading2',
properties: {
text: {
value: [{ bold: true, text: 'Flexibility' }],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'CRDT supports more data types. For example, Yjs supports Array, Map, and Treelike, and therefore applies to more business scenarios.',
},
],
},
},
blocks: [],
},
{
type: 'heading2',
properties: {
text: {
value: [{ bold: true, text: 'Performance' }],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'CRDT tolerates higher latency and can wait longer for solving conflicts, whereas the calculation of OT in the same condition may become too overwhelming for a server to sustain.',
},
],
},
},
blocks: [],
},
{
type: 'heading2',
properties: {
text: {
value: [{ bold: true, text: 'Extensibility' }],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'Because CRDT supports more data types and editor elements, it is more extensible.',
},
],
},
},
blocks: [],
},
{
type: 'heading1',
properties: {
text: { value: [{ text: 'Conclusion' }] },
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice.',
},
],
},
},
blocks: [],
},
{
type: 'text',
properties: { text: { value: [{ text: '' }] } },
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{ text: '' },
{
type: 'link',
url: 'https://blog.affine.pro/',
id: 'link.stubssslo0rq',
children: [{ text: '← View all posts' }],
},
{ text: '' },
],
},
},
blocks: [],
},
],
},
grid: {
type: 'group',
properties: {},
blocks: [
{
type: 'heading2',
properties: {
text: {
value: [
{
bold: true,
text: 'Performance',
},
],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'CRDT tolerates higher latency and can wait longer for solving conflicts, whereas the calculation of OT in the same condition may become too overwhelming for a server to sustain.',
},
],
},
},
blocks: [],
},
{
type: 'heading2',
properties: {
text: {
value: [
{
bold: true,
text: 'Extensibility',
},
],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: 'Because CRDT supports more data types and editor elements, it is more extensible.',
},
],
},
},
blocks: [],
},
{
type: 'heading1',
properties: {
text: {
value: [
{
text: 'Conclusion',
},
],
},
},
blocks: [],
},
{
type: 'grid',
properties: {},
blocks: [
{
type: 'gridItem',
properties: {
gridItemWidth: '50%',
},
blocks: [
{
type: 'text',
properties: {
text: {
value: [
{
text: 'Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice.',
},
],
},
},
blocks: [],
},
],
},
{
type: 'gridItem',
properties: {
gridItemWidth: '50%',
},
blocks: [
{
type: 'text',
properties: {
text: {
value: [
{
text: 'Collaborative algorithm is still a foreign concept for many developers. There are some introductions to it, but as to how it shall be used, there is still lack of clear explanation. I hope this article helps. If you also work for a startup company and want some suggestions, CRDT, especially Yjs, should be a better choice.',
},
],
},
},
blocks: [],
},
],
},
],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: '',
},
],
},
},
blocks: [],
},
{
type: 'text',
properties: {
text: {
value: [
{
text: '',
},
{
type: 'link',
url: 'https://blog.affine.pro/',
id: 'link.stubssslo0rq',
children: [
{
text: '← View all posts',
},
],
},
{
text: '',
},
],
},
},
},
],
},
};
export type GroupTemplateKeys = 'todolist' | 'blog' | 'empty' | 'grid';
export { groupTemplateMap };
@@ -1,6 +1,19 @@
import { groupTemplateMap } from './group-templates';
import { Template, TemplateMeta } from './types';
const defaultTemplateList: Array<TemplateMeta> = [
import { Template, TemplateMeta, GroupTemplate } from './types';
import blogTemplate from './blog.json';
import emptyTemplate from './empty.json';
import gridTemplate from './grid.json';
import todoTemplate from './todo.json';
export type GroupTemplateKeys = 'todolist' | 'blog' | 'empty' | 'grid';
type GroupTemplateMap = Record<GroupTemplateKeys, GroupTemplate>;
const groupTemplateMap = {
empty: emptyTemplate,
todolist: todoTemplate,
blog: blogTemplate,
grid: gridTemplate,
} as GroupTemplateMap;
const defaultTemplateList = [
{
name: 'New From Quick Start',
groupKeys: ['todolist'],
@@ -9,12 +22,13 @@ const defaultTemplateList: Array<TemplateMeta> = [
{ name: 'New From Blog', groupKeys: ['blog'] },
{ name: ' New Todolist', groupKeys: ['todolist'] },
{ name: ' New Empty Page', groupKeys: ['empty'] },
];
] as const;
const TemplateFactory = {
defaultTemplateList: defaultTemplateList,
generatePageTemplateByGroupKeys(props: TemplateMeta): Template {
const newTitle = props.name || 'Get Started with AFFiNE';
const keys = props.groupKeys || [];
const keys: GroupTemplateKeys[] = props.groupKeys || [];
const blankPage: Template = {
type: 'page',
properties: {
@@ -0,0 +1,173 @@
{
"type": "group",
"properties": {},
"blocks": [
{
"type": "heading1",
"properties": {
"text": {
"value": [{ "text": "🎓Graduating from the project" }]
}
},
"blocks": []
},
{
"type": "todo",
"properties": {
"text": {
"value": [
{
"text": "Congratulations! Now you can start create your own projects!"
}
]
}
},
"blocks": []
},
{
"type": "todo",
"properties": {
"text": {
"value": [
{
"text": "To start using workspaces and folders hit the "
},
{
"bold": true,
"text": "button at the top left of the screen"
}
]
}
},
"blocks": []
},
{
"type": "todo",
"properties": {
"text": {
"value": [
{
"text": ""
},
{
"text": "At any time if you feel lost do visit our 📚Blog: "
},
{
"type": "link",
"url": "https://blog.affine.pro/",
"id": "link.qx4yhw81or54",
"children": [
{
"text": "https://blog.affine.pro"
}
]
},
{
"text": ""
}
]
},
"collapsed": {
"value": false
}
},
"blocks": []
},
{
"type": "todo",
"properties": {
"text": {
"value": [
{
"text": ""
},
{
"text": "If you have any suggestions drop a post in our Reddit Channel"
},
{
"type": "link",
"url": "https://www.reddit.com/r/Affine/",
"id": "link.zeafc4ogfvrb",
"children": [
{
"text": "https://www.reddit.com/r/Affine/"
}
]
},
{
"text": " "
}
]
}
},
"blocks": []
},
{
"type": "heading2",
"properties": {
"text": {
"value": [
{
"text": "🎉 The Essentials. Check things off after you tried them!"
}
]
}
},
"blocks": []
},
{
"type": "todo",
"properties": {
"text": {
"value": [
{ "text": " ✅ " },
{ "bold": true, "text": "Check" },
{
"text": " the text box here to complete the task!"
}
]
}
},
"blocks": []
},
{
"type": "todo",
"properties": {
"text": {
"value": [
{ "text": "" },
{ "text": "" },
{ "text": " 👋 " },
{ "bold": true, "text": "Drag" },
{
"text": " the ⠟ button left of the checkbox to reorder tasks"
},
{ "text": "" },
{ "text": "" }
]
}
},
"blocks": []
},
{
"type": "todo",
"properties": {
"text": {
"value": [
{ "text": "" },
{ "text": "" },
{ "text": " ➡️ " },
{ "bold": true, "text": "Fold" },
{ "text": " and " },
{ "bold": true, "text": "Unfold" },
{
"text": " a task to simplify your list using the arrow on the right ⤵️"
}
]
},
"numberType": "type1",
"collapsed": { "value": false }
}
}
]
}
@@ -1,5 +1,5 @@
import { DefaultColumnsValue, BlockFlavorKeys } from './../index';
import { groupTemplateMap, GroupTemplateKeys } from './group-templates';
import { BlockFlavorKeys, DefaultColumnsValue } from './../index';
import { GroupTemplateKeys } from './template-factory';
// interface Block {
// type: BlockFlavorKeys;
@@ -112,11 +112,15 @@ export class UserConfig extends ServiceBaseClass {
async getWorkspaceName(workspace: string): Promise<string> {
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
const workspaceName =
workspace_db_block.getDecoration<string>(WORKSPACE_CONFIG) ||
workspace_db_block.id;
workspace_db_block.getDecoration<string>(WORKSPACE_CONFIG) || '';
return workspaceName;
}
async getWorkspaceId(workspace: string): Promise<string> {
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
return workspace_db_block.id;
}
async setWorkspaceName(workspace: string, workspaceName: string) {
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
workspace_db_block.setDecoration(WORKSPACE_CONFIG, workspaceName);
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "jwst"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+2
View File
@@ -0,0 +1,2 @@
// Open Source version coming soon
// the rust version of jwt
+10
View File
@@ -26,3 +26,13 @@ export const useCurrentEditors = () => {
setCurrentEditors,
};
};
// when first time transfer doc to board, need init the editor shape size to page size.
const _pageClientWidth = atom<number>(1020);
export const usePageClientWidth = () => {
const [pageClientWidth, setPageClientWidth] = useAtom(_pageClientWidth);
return {
pageClientWidth,
setPageClientWidth,
};
};