mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 18:46:19 +08:00
fix interfere in Menu.tsx
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -149,6 +149,7 @@ export interface TDMeta {
|
||||
isDarkMode: boolean;
|
||||
app: {
|
||||
useStore: () => TDSnapshot;
|
||||
setEditingText: (id: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+16
-15
@@ -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>
|
||||
|
||||
+12
-109
@@ -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(),
|
||||
}));
|
||||
};
|
||||
|
||||
+15
-131
@@ -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 |
+21
@@ -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 />*/}
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@
|
||||
justify-content: space-around;
|
||||
background-color: #fff;
|
||||
color: #4c6275;
|
||||
padding: 0 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
overflow: hidden;
|
||||
|
||||
.TreeItemMoreActions {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
@@ -0,0 +1,2 @@
|
||||
// Open Source version coming soon
|
||||
// the rust version of jwt
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user