From e250f780a47d6a46d2e0d7ef0e99493f96267b28 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Wed, 28 Feb 2024 14:26:08 +0800 Subject: [PATCH] feat: support to show condition in filter chip label (#4751) * feat: support to show condition in filter chip label * fix: improve the performance of inline formula with selection --- .../components/database/Database.hooks.ts | 7 +++ .../components/database/Database.tsx | 1 + .../database_settings/DatabaseCollection.tsx | 8 ++- .../database/components/filter/Filter.tsx | 57 +++++++++++++++---- .../database/components/filter/Filters.tsx | 4 +- .../filter/date_filter/DateFilter.tsx | 16 ++---- .../filter/date_filter/DateFilterValue.tsx | 52 +++++++++++++++++ .../number_filter/NumberFilterValue.tsx | 39 +++++++++++++ .../select_filter/SelectFilterValue.tsx | 38 +++++++++++++ .../filter/text_filter/TextFilterValue.tsx | 34 +++++++++++ .../database/components/property/Property.tsx | 5 +- .../components/database/database.scss | 6 ++ .../components/editor/command/formula.ts | 11 +++- .../components/editor/command/index.ts | 18 ++++++ .../components/editor/command/mark.ts | 50 ++++++++-------- .../blocks/bulleted_list/BulletedListIcon.tsx | 2 +- .../blocks/numbered_list/NumberListIcon.tsx | 2 +- .../inline_formula/InlineFormula.tsx | 2 +- .../SelectionToolbar.hooks.ts | 16 +++++- .../selection_toolbar/actions/bold/Bold.tsx | 1 + .../actions/formula/Formula.tsx | 14 ++++- .../editor/provider/utils/action.ts | 26 ++++++++- .../editor/provider/utils/convert.ts | 10 ++-- .../components/editor/stores/inline_node.ts | 8 ++- frontend/resources/translations/en.json | 10 +++- 25 files changed, 366 insertions(+), 71 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilterValue.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/number_filter/NumberFilterValue.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/database.scss diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts index ef478b62c4..e323b7d1a8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts @@ -106,6 +106,13 @@ export const useFiltersCount = () => { ); }; +export function useStaticTypeOption(fieldId: string) { + const context = useContext(DatabaseContext); + const typeOptions = context.typeOptions; + + return typeOptions[fieldId] as T; +} + export function useTypeOption(fieldId: string) { const context = useContext(DatabaseContext); const typeOptions = useSnapshot(context.typeOptions); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx index 72c995a8e8..97927fd9f4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx @@ -15,6 +15,7 @@ import ExpandRecordModal from '$app/components/database/components/edit_record/E import { subscribeNotifications } from '$app/application/notification'; import { Page } from '$app_reducers/pages/slice'; import { getPage } from '$app/application/folder/page.service'; +import './database.scss'; interface Props { selectedViewId?: string; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx index b3c4a2c054..0fe9fb6d5b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx @@ -8,9 +8,11 @@ interface Props { export const DatabaseCollection = ({ open }: Props) => { return ( -
- - +
+
+ + +
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx index 8bd5f1688f..fdd7bccb5b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx @@ -1,20 +1,20 @@ import React, { FC, useMemo, useState } from 'react'; import { - Filter as FilterType, - Field as FieldData, - UndeterminedFilter, - TextFilterData, - SelectFilterData, - NumberFilterData, CheckboxFilterData, ChecklistFilterData, DateFilterData, + Field as FieldData, + Filter as FilterType, + NumberFilterData, + SelectFilterData, + TextFilterData, + UndeterminedFilter, } from '$app/application/database'; import { Chip, Popover } from '@mui/material'; import { Property } from '$app/components/database/components/property'; import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg'; import TextFilter from './text_filter/TextFilter'; -import { FieldType } from '@/services/backend'; +import { CheckboxFilterConditionPB, ChecklistFilterConditionPB, FieldType } from '@/services/backend'; import FilterActions from '$app/components/database/components/filter/FilterActions'; import { updateFilter } from '$app/application/database/filter/filter_service'; import { useViewId } from '$app/hooks'; @@ -22,6 +22,11 @@ import SelectFilter from './select_filter/SelectFilter'; import DateFilter from '$app/components/database/components/filter/date_filter/DateFilter'; import FilterConditionSelect from '$app/components/database/components/filter/FilterConditionSelect'; +import TextFilterValue from '$app/components/database/components/filter/text_filter/TextFilterValue'; +import SelectFilterValue from '$app/components/database/components/filter/select_filter/SelectFilterValue'; +import NumberFilterValue from '$app/components/database/components/filter/number_filter/NumberFilterValue'; +import { useTranslation } from 'react-i18next'; +import DateFilterValue from '$app/components/database/components/filter/date_filter/DateFilterValue'; interface Props { filter: FilterType; @@ -57,6 +62,7 @@ const getFilterComponent = (field: FieldData) => { function Filter({ filter, field }: Props) { const viewId = useViewId(); + const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const handleClick = (e: React.MouseEvent) => { @@ -70,7 +76,10 @@ function Filter({ filter, field }: Props) { const onDataChange = async (data: UndeterminedFilter['data']) => { const newFilter = { ...filter, - data, + data: { + ...(filter.data || {}), + ...data, + }, } as UndeterminedFilter; try { @@ -105,14 +114,42 @@ function Filter({ filter, field }: Props) { } }, [field, filter]); + const conditionValue = useMemo(() => { + switch (field.type) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Number: + return ; + case FieldType.Checkbox: + return (filter.data as CheckboxFilterData).condition === CheckboxFilterConditionPB.IsChecked + ? t('grid.checkboxFilter.isChecked') + : t('grid.checkboxFilter.isUnchecked'); + case FieldType.Checklist: + return (filter.data as ChecklistFilterData).condition === ChecklistFilterConditionPB.IsComplete + ? t('grid.checklistFilter.isComplete') + : t('grid.checklistFilter.isIncomplted'); + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return ; + default: + return ''; + } + }, [field.id, field.type, filter.data, t]); + return ( <> - +
+ + {conditionValue}
} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx index ce4874b814..860ce9f69f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx @@ -29,9 +29,9 @@ function Filters() { }; return ( -
+
{options.map(({ filter, field }) => (field ? : null))} - { - const now = Date.now() / 1000; - if (isRange) { - return filter.data.start ? filter.data.start : now; + return filter.data.start; } - return filter.data.timestamp ? filter.data.timestamp : now; + return filter.data.timestamp; }, [filter.data.start, filter.data.timestamp, isRange]); const endTimestamp = useMemo(() => { - const now = Date.now() / 1000; - if (isRange) { - return filter.data.end ? filter.data.end : now; + return filter.data.end; } - return now; + return; }, [filter.data.end, isRange]); const timeFormat = useMemo(() => { @@ -64,7 +60,7 @@ function DateFilter({ filter, field, onChange }: Props) { onChange({ condition, timestamp: date, - start: date, + start: endDate ? date : undefined, end: endDate, }); }} @@ -81,7 +77,7 @@ function DateFilter({ filter, field, onChange }: Props) { onChange({ condition, timestamp: date, - start: date, + start: endDate ? date : undefined, end: endDate, }); }} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilterValue.tsx new file mode 100644 index 0000000000..dd75d25852 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilterValue.tsx @@ -0,0 +1,52 @@ +import React, { useMemo } from 'react'; +import { DateFilterData } from '$app/application/database'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import { DateFilterConditionPB } from '@/services/backend'; + +function DateFilterValue({ data }: { data: DateFilterData }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!data.timestamp) return ''; + + let startStr = ''; + let endStr = ''; + + if (data.start) { + const end = data.end ?? data.start; + const moreThanOneYear = dayjs.unix(end).diff(dayjs.unix(data.start), 'year') > 1; + const format = moreThanOneYear ? 'MMM D, YYYY' : 'MMM D'; + + startStr = dayjs.unix(data.start).format(format); + endStr = dayjs.unix(end).format(format); + } + + const timestamp = dayjs.unix(data.timestamp).format('MMM D'); + + switch (data.condition) { + case DateFilterConditionPB.DateIs: + return `: ${timestamp}`; + case DateFilterConditionPB.DateBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.before')} ${timestamp}`; + case DateFilterConditionPB.DateAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.after')} ${timestamp}`; + case DateFilterConditionPB.DateOnOrBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrBefore')} ${timestamp}`; + case DateFilterConditionPB.DateOnOrAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrAfter')} ${timestamp}`; + case DateFilterConditionPB.DateWithIn: + return `: ${startStr} - ${endStr}`; + case DateFilterConditionPB.DateIsEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isEmpty')}`; + case DateFilterConditionPB.DateIsNotEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [data, t]); + + return <>{value}; +} + +export default DateFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/number_filter/NumberFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/number_filter/NumberFilterValue.tsx new file mode 100644 index 0000000000..658ef13d69 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/number_filter/NumberFilterValue.tsx @@ -0,0 +1,39 @@ +import React, { useMemo } from 'react'; +import { NumberFilterData } from '$app/application/database'; +import { NumberFilterConditionPB } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterValue({ data }: { data: NumberFilterData }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!data.content) { + return ''; + } + + const content = parseInt(data.content); + + switch (data.condition) { + case NumberFilterConditionPB.Equal: + return `= ${content}`; + case NumberFilterConditionPB.NotEqual: + return `!= ${content}`; + case NumberFilterConditionPB.GreaterThan: + return `> ${content}`; + case NumberFilterConditionPB.GreaterThanOrEqualTo: + return `>= ${content}`; + case NumberFilterConditionPB.LessThan: + return `< ${content}`; + case NumberFilterConditionPB.LessThanOrEqualTo: + return `<= ${content}`; + case NumberFilterConditionPB.NumberIsEmpty: + return t('grid.textFilter.isEmpty'); + case NumberFilterConditionPB.NumberIsNotEmpty: + return t('grid.textFilter.isNotEmpty'); + } + }, [data.condition, data.content, t]); + + return <>{value}; +} + +export default NumberFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx new file mode 100644 index 0000000000..4b6d29d79d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx @@ -0,0 +1,38 @@ +import React, { useMemo } from 'react'; +import { SelectFilterData, SelectTypeOption } from '$app/application/database'; +import { useStaticTypeOption } from '$app/components/database'; +import { useTranslation } from 'react-i18next'; +import { SelectOptionConditionPB } from '@/services/backend'; + +function SelectFilterValue({ data, fieldId }: { data: SelectFilterData; fieldId: string }) { + const typeOption = useStaticTypeOption(fieldId); + const { t } = useTranslation(); + const value = useMemo(() => { + if (!data.optionIds?.length) return ''; + + const options = data.optionIds + .map((optionId) => { + const option = typeOption?.options?.find((option) => option.id === optionId); + + return option?.name; + }) + .join(', '); + + switch (data.condition) { + case SelectOptionConditionPB.OptionIs: + return `: ${options}`; + case SelectOptionConditionPB.OptionIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`; + case SelectOptionConditionPB.OptionIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case SelectOptionConditionPB.OptionIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [data.condition, data.optionIds, t, typeOption?.options]); + + return <>{value}; +} + +export default SelectFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx new file mode 100644 index 0000000000..9377050bf1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { TextFilterData } from '$app/application/database'; +import { TextFilterConditionPB } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; + +function TextFilterValue({ data }: { data: TextFilterData }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!data.content) return ''; + switch (data.condition) { + case TextFilterConditionPB.Contains: + case TextFilterConditionPB.Is: + return `: ${data.content}`; + case TextFilterConditionPB.DoesNotContain: + case TextFilterConditionPB.IsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${data.content}`; + case TextFilterConditionPB.StartsWith: + return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${data.content}`; + case TextFilterConditionPB.EndsWith: + return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${data.content}`; + case TextFilterConditionPB.TextIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case TextFilterConditionPB.TextIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [t, data]); + + return <>{value}; +} + +export default TextFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx index b2cd83a9a1..67f358a5a4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx @@ -10,6 +10,7 @@ export interface FieldProps { menuOpened?: boolean; onOpenMenu?: (id: string) => void; onCloseMenu?: (id: string) => void; + className?: string; } const initialAnchorOrigin: PopoverOrigin = { @@ -22,7 +23,7 @@ const initialTransformOrigin: PopoverOrigin = { horizontal: 'left', }; -export const Property: FC = ({ field, onCloseMenu, menuOpened }) => { +export const Property: FC = ({ field, onCloseMenu, className, menuOpened }) => { const ref = useRef(null); const [anchorPosition, setAnchorPosition] = useState< | { @@ -63,7 +64,7 @@ export const Property: FC = ({ field, onCloseMenu, menuOpened }) => return ( <> -
+
{field.name}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/database.scss b/frontend/appflowy_tauri/src/appflowy_app/components/database/database.scss new file mode 100644 index 0000000000..358868b46f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/database.scss @@ -0,0 +1,6 @@ +.database-collection { + ::-webkit-scrollbar { + width: 0px; + height: 0px; + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts index f12d21d76e..d5a0e0c976 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts @@ -1,7 +1,6 @@ import { ReactEditor } from 'slate-react'; -import { Editor, Element as SlateElement, NodeEntry, Range, Transforms } from 'slate'; +import { Editor, Element, Element as SlateElement, NodeEntry, Range, Transforms } from 'slate'; import { EditorInlineNodeType, FormulaNode } from '$app/application/document/document.types'; -import { isMarkActive } from '$app/components/editor/command/mark'; export function insertFormula(editor: ReactEditor, formula?: string) { if (editor.selection) { @@ -80,5 +79,11 @@ export function unwrapFormula(editor: ReactEditor) { } export function isFormulaActive(editor: ReactEditor) { - return isMarkActive(editor, EditorInlineNodeType.Formula); + const [match] = editor.nodes({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorInlineNodeType.Formula; + }, + }); + + return Boolean(match); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts index 14fcc8b63f..40b4a6c458 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts @@ -229,6 +229,16 @@ export const CustomEditor = { return !!match; }, + formulaActiveNode(editor: ReactEditor) { + const [match] = editor.nodes({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorInlineNodeType.Formula; + }, + }); + + return match ? (match as NodeEntry) : undefined; + }, + isMentionActive(editor: ReactEditor) { const [match] = editor.nodes({ match: (n) => { @@ -519,6 +529,14 @@ export const CustomEditor = { return editor.isEmpty(textNode); }, + includeInlineBlocks: (editor: ReactEditor) => { + const [match] = Editor.nodes(editor, { + match: (n) => Element.isElement(n) && editor.isInline(n), + }); + + return Boolean(match); + }, + getNodeTextContent(node: Node): string { if (Element.isElement(node) && node.type === EditorInlineNodeType.Formula) { return (node as FormulaNode).data || ''; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts index 42130e24c5..45f3362f53 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts @@ -1,5 +1,5 @@ import { ReactEditor } from 'slate-react'; -import { Editor, Text, Range } from 'slate'; +import { Editor, Text, Range, Element } from 'slate'; import { EditorInlineNodeType, EditorMarkFormat } from '$app/application/document/document.types'; export function toggleMark( @@ -33,18 +33,13 @@ export function isMarkActive(editor: ReactEditor, format: EditorMarkFormat | Edi const isExpanded = Range.isExpanded(selection); if (isExpanded) { - const matches = Array.from(getSelectionNodeEntry(editor) || []); - - return matches.every((match) => { - const [node] = match; + const texts = getSelectionTexts(editor); + return texts.every((node) => { const { text, ...attributes } = node; - if (!text) { - return true; - } - - return !!(attributes as Record)[format]; + if (!text) return true; + return Boolean((attributes as Record)[format]); }); } @@ -53,10 +48,12 @@ export function isMarkActive(editor: ReactEditor, format: EditorMarkFormat | Edi return marks ? !!marks[format] : false; } -function getSelectionNodeEntry(editor: ReactEditor) { +function getSelectionTexts(editor: ReactEditor) { const selection = editor.selection; - if (!selection) return null; + if (!selection) return []; + + const texts: Text[] = []; const isExpanded = Range.isExpanded(selection); @@ -73,16 +70,25 @@ function getSelectionNodeEntry(editor: ReactEditor) { } } - return Editor.nodes(editor, { - match: Text.isText, - at: { - anchor, - focus, - }, + Array.from( + Editor.nodes(editor, { + at: { + anchor, + focus, + }, + }) + ).forEach((match) => { + const node = match[0] as Element; + + if (Text.isText(node)) { + texts.push(node); + } else if (Editor.isInline(editor, node)) { + texts.push(...(node.children as Text[])); + } }); } - return null; + return texts; } /** @@ -97,13 +103,11 @@ export function getAllMarks(editor: ReactEditor) { const isExpanded = Range.isExpanded(selection); if (isExpanded) { - const matches = Array.from(getSelectionNodeEntry(editor) || []); + const texts = getSelectionTexts(editor); const marks: Record = {}; - matches.forEach((match) => { - const [node] = match; - + texts.forEach((node) => { Object.entries(node).forEach(([key, value]) => { if (key !== 'text') { marks[key] = value; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx index c94353d45c..0e712b3269 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx @@ -8,7 +8,7 @@ function BulletedListIcon({ block: _, className }: { block: BulletedListNode; cl e.preventDefault(); }} contentEditable={false} - className={`${className} bulleted-icon flex w-[23px] justify-center pr-1 font-medium`} + className={`${className} bulleted-icon flex min-w-[23px] justify-center pr-1 font-medium`} /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx index 9891365b36..c0ee4f3ead 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx @@ -36,7 +36,7 @@ function NumberListIcon({ block, className }: { block: NumberedListNode; classNa }} contentEditable={false} data-number={index} - className={`${className} numbered-icon flex w-[23px] justify-center pr-1 font-medium`} + className={`${className} numbered-icon flex w-[23px] min-w-[23px] justify-center pr-1 font-medium`} /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx index bbbd485fab..245592cbea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx @@ -16,7 +16,7 @@ export const InlineFormula = memo( const { popoverOpen = false, setRange, openPopover, closePopover } = useEditorInlineBlockState('formula'); const anchor = useRef(null); const selected = useSelected(); - const open = popoverOpen && selected; + const open = Boolean(popoverOpen && selected); const handleClick = useCallback( (e: MouseEvent) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts index 648ca84079..29ce475e45 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts @@ -3,7 +3,7 @@ import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } f import { getSelectionPosition } from '$app/components/editor/components/tools/selection_toolbar/utils'; import debounce from 'lodash-es/debounce'; import { CustomEditor } from '$app/components/editor/command'; -import { BaseRange, Editor, Range as SlateRange } from 'slate'; +import { BaseRange, Range as SlateRange } from 'slate'; import { useDecorateDispatch } from '$app/components/editor/stores/decorate'; const DELAY = 300; @@ -118,9 +118,21 @@ export function useSelectionToolbar(ref: MutableRefObject const { selection } = editor; - if (!isFocusedEditor || !selection || SlateRange.isCollapsed(selection) || Editor.string(editor, selection) === '') { + const close = () => { debounceRecalculatePosition.cancel(); closeToolbar(); + }; + + if (!isFocusedEditor || !selection || SlateRange.isCollapsed(selection)) { + close(); + return; + } + + // There has a bug which the text of selection is empty when the selection include inline blocks + const isEmptyText = !CustomEditor.includeInlineBlocks(editor) && editor.string(selection) === ''; + + if (isEmptyText) { + close(); return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/Bold.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/Bold.tsx index fe2b690068..509ca4b379 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/Bold.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/Bold.tsx @@ -11,6 +11,7 @@ export function Bold() { const { t } = useTranslation(); const editor = useSlateStatic(); const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Bold); + const modifier = useMemo(() => getHotKey(EditorMarkFormat.Bold).modifier, []); const onClick = useCallback(() => { CustomEditor.toggleMark(editor, { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx index 5aa190d1a7..38f18a1d30 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx @@ -11,20 +11,28 @@ export function Formula() { const editor = useSlateStatic(); const isActivatedMention = CustomEditor.isMentionActive(editor); + const formulaMatch = CustomEditor.formulaActiveNode(editor); const isActivated = !isActivatedMention && CustomEditor.isFormulaActive(editor); const { setRange, openPopover } = useEditorInlineBlockState('formula'); const onClick = useCallback(() => { - const selection = editor.selection; + let selection = editor.selection; if (!selection) return; - CustomEditor.toggleFormula(editor); + if (formulaMatch) { + selection = editor.range(formulaMatch[1]); + editor.select(selection); + } else { + CustomEditor.toggleFormula(editor); + } requestAnimationFrame(() => { + if (!selection) return; + setRange(selection); openPopover(); }); - }, [editor, setRange, openPopover]); + }, [editor, formulaMatch, setRange, openPopover]); return ( ; const actions = []; + if ([EditorInlineNodeType.Formula, EditorInlineNodeType.Mention].includes(yXmlText.getAttribute('type'))) { + const parentYXmlText = yXmlText.parent as Y.XmlText; + const parentDelta = parentYXmlText.toDelta() as YDelta; + const index = parentDelta.findIndex((op) => op.insert === yXmlText); + const ops = YDelta2Delta(parentDelta); + + const retainIndex = ops.reduce((acc, op, currentIndex) => { + if (currentIndex < index) { + return acc + (op.insert as string).length ?? 0; + } + + return acc; + }, 0); + + const newDelta = [ + { + retain: retainIndex, + }, + ...delta, + ]; + + actions.push(...generateApplyTextActions(parentYXmlText, newDelta)); + } + if (yXmlText.getAttribute('type') === 'text') { actions.push(...textOps2BlockActions(rootId, yXmlText, delta)); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts index d63e09c796..42d0f372c7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts @@ -11,7 +11,7 @@ export function transformToInlineElement(op: Op): Element | null { const attributes = op.attributes; if (!attributes) return null; - const formula = attributes.formula as string; + const { formula, mention, ...attrs } = attributes; if (formula) { return { @@ -20,23 +20,23 @@ export function transformToInlineElement(op: Op): Element | null { children: [ { text: op.insert as string, + ...attrs, }, ], }; } - const matchMention = attributes.mention as Mention; - - if (matchMention) { + if (mention) { return { type: EditorInlineNodeType.Mention, children: [ { text: op.insert as string, + ...attrs, }, ], data: { - ...matchMention, + ...(mention as Mention), }, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/inline_node.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/inline_node.ts index a6d536522e..6607a546d8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/inline_node.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/inline_node.ts @@ -1,5 +1,5 @@ import { createContext, useCallback, useContext, useMemo } from 'react'; -import { BaseRange } from 'slate'; +import { BaseRange, Path } from 'slate'; import { proxy, useSnapshot } from 'valtio'; export interface EditorInlineBlockState { @@ -43,8 +43,10 @@ export function useEditorInlineBlockState(key: 'formula') { }, [context, key]); const setRange = useCallback( - (range: BaseRange) => { - context[key].range = range; + (at: BaseRange | Path) => { + const range = Path.isPath(at) ? { anchor: at, focus: at } : at; + + context[key].range = range as BaseRange; }, [context, key] ); diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f5258622be..97357e8319 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -550,7 +550,15 @@ "onOrAfter": "Is on or after", "between": "Is between", "empty": "Is empty", - "notEmpty": "Is not empty" + "notEmpty": "Is not empty", + "choicechipPrefix": { + "before": "Before", + "after": "After", + "onOrBefore": "On or before", + "onOrAfter": "On or after", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty" + } }, "numberFilter": { "equal": "Equals",