mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support updating the view name and icon through document (#3099)
* feat: support updating the view name and icon through document * fix: store expand pages * fix: refactor text link * fix: update cargo.toml * fix: update test * fix: update event map * fix: move deal with icon codes to a single file * fix: delete useless code from flutter * fix: document banner * fix: build error * fix: update rust library --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
parent
70914e6228
commit
16a01e11ed
@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
@ -115,18 +114,6 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
|
||||
// Set the icon and cover of the view
|
||||
ViewBackendService.updateView(
|
||||
viewId: viewId,
|
||||
iconURL: iconURL,
|
||||
coverURL: coverURL,
|
||||
).then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +123,7 @@ class RowBannerEvent with _$RowBannerEvent {
|
||||
const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
|
||||
_DidReceiveRowMeta;
|
||||
const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
|
||||
_DidReceiveFieldUdate;
|
||||
_DidReceiveFieldUpdate;
|
||||
const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
|
||||
const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ extension ViewExtension on ViewPB {
|
||||
return widget;
|
||||
}
|
||||
|
||||
Widget icon() {
|
||||
Widget defaultIcon() {
|
||||
final iconName = switch (layout) {
|
||||
ViewLayoutPB.Board => 'editor/board',
|
||||
ViewLayoutPB.Calendar => 'editor/calendar',
|
||||
|
@ -138,8 +138,6 @@ class ViewBackendService {
|
||||
static Future<Either<ViewPB, FlowyError>> updateView({
|
||||
required String viewId,
|
||||
String? name,
|
||||
String? iconURL,
|
||||
String? coverURL,
|
||||
bool? isFavorite,
|
||||
}) {
|
||||
final payload = UpdateViewPayloadPB.create()..viewId = viewId;
|
||||
@ -148,14 +146,6 @@ class ViewBackendService {
|
||||
payload.name = name;
|
||||
}
|
||||
|
||||
if (iconURL != null) {
|
||||
payload.iconUrl = iconURL;
|
||||
}
|
||||
|
||||
if (coverURL != null) {
|
||||
payload.coverUrl = coverURL;
|
||||
}
|
||||
|
||||
if (isFavorite != null) {
|
||||
payload.isFavorite = isFavorite;
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
// icon
|
||||
SizedBox.square(
|
||||
dimension: 16,
|
||||
child: widget.view.icon(),
|
||||
child: widget.view.defaultIcon(),
|
||||
),
|
||||
const HSpace(5),
|
||||
// title
|
||||
|
20
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
20
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -105,7 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
[[package]]
|
||||
name = "appflowy-integrate"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -1021,7 +1021,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -1039,7 +1039,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-client-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab-sync",
|
||||
@ -1057,7 +1057,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1084,7 +1084,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1096,7 +1096,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -1115,7 +1115,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -1135,7 +1135,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-persistence"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
@ -1155,7 +1155,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1185,7 +1185,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-sync"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab",
|
||||
|
@ -34,20 +34,20 @@ default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[patch.crates-io]
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
|
||||
#collab = { path = "../../AppFlowy-Collab/collab" }
|
||||
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
||||
#collab-document = { path = "../../AppFlowy-Collab/collab-document" }
|
||||
#collab-database = { path = "../../AppFlowy-Collab/collab-database" }
|
||||
#appflowy-integrate = { path = "../../AppFlowy-Collab/appflowy-integrate" }
|
||||
#collab-plugins = { path = "../../AppFlowy-Collab/collab-plugins" }
|
||||
#collab = { path = "../../../../AppFlowy-Collab/collab" }
|
||||
#collab-folder = { path = "../../../../AppFlowy-Collab/collab-folder" }
|
||||
#collab-document = { path = "../../../../AppFlowy-Collab/collab-document" }
|
||||
#collab-database = { path = "../../../../AppFlowy-Collab/collab-database" }
|
||||
#appflowy-integrate = { path = "../../../../AppFlowy-Collab/appflowy-integrate" }
|
||||
#collab-plugins = { path = "../../../../AppFlowy-Collab/collab-plugins" }
|
||||
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { UserSettingController } from '$app/stores/effects/user/user_setting_controller';
|
||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||
import { Theme as ThemeType, Theme, ThemeMode } from '$app/interfaces';
|
||||
import { Theme as ThemeType, ThemeMode } from '$app/interfaces';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { getDesignTokens } from '$app/utils/mui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -18,28 +18,18 @@ export function useUserSetting() {
|
||||
return controller;
|
||||
}, [currentUser?.id]);
|
||||
|
||||
const loadUserSetting = useCallback(async () => {
|
||||
if (!userSettingController) return;
|
||||
const settings = await userSettingController.getAppearanceSetting();
|
||||
|
||||
if (!settings) return;
|
||||
dispatch(currentUserActions.setUserSetting(settings));
|
||||
await i18n.changeLanguage(settings.language);
|
||||
}, [dispatch, i18n, userSettingController]);
|
||||
|
||||
useEffect(() => {
|
||||
userSettingController?.getAppearanceSetting().then((res) => {
|
||||
if (!res) return;
|
||||
const locale = res.locale;
|
||||
let language = 'en';
|
||||
|
||||
if (locale.language_code && locale.country_code) {
|
||||
language = `${locale.language_code}-${locale.country_code}`;
|
||||
} else if (locale.language_code) {
|
||||
language = locale.language_code;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
currentUserActions.setUserSetting({
|
||||
themeMode: res.theme_mode,
|
||||
theme: res.theme as Theme,
|
||||
language: language,
|
||||
})
|
||||
);
|
||||
i18n.changeLanguage(language);
|
||||
});
|
||||
}, [i18n, dispatch, userSettingController]);
|
||||
void loadUserSetting();
|
||||
}, [loadUserSetting]);
|
||||
|
||||
const { themeMode = ThemeMode.Light, theme: themeType = ThemeType.Default } = useAppSelector((state) => {
|
||||
return state.currentUser.userSetting || {};
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import emojiData, { EmojiMartData } from '@emoji-mart/data';
|
||||
import { init, FrequentlyUsed, getEmojiDataFromNative, Store } from 'emoji-mart';
|
||||
|
||||
import { PopoverProps } from '@mui/material/Popover';
|
||||
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { chunkArray } from '$app/utils/tool';
|
||||
|
||||
export const EMOJI_SIZE = 32;
|
||||
|
||||
export const PER_ROW_EMOJI_COUNT = 13;
|
||||
|
||||
export const MAX_FREQUENTLY_ROW_COUNT = 2;
|
||||
|
||||
export interface EmojiCategory {
|
||||
id: string;
|
||||
emojis: Emoji[];
|
||||
@ -15,48 +23,87 @@ interface Emoji {
|
||||
name: string;
|
||||
native: string;
|
||||
}
|
||||
export function useLoadEmojiData({ skin }: { skin: number }) {
|
||||
|
||||
export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: string) => void }) {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [emojiCategories, setEmojiCategories] = useState<EmojiCategory[]>([]);
|
||||
const [skin, setSkin] = useState<number>(() => {
|
||||
return Number(Store.get('skin')) || 0;
|
||||
});
|
||||
|
||||
const onSkinChange = useCallback((val: number) => {
|
||||
setSkin(val);
|
||||
Store.set('skin', String(val));
|
||||
}, []);
|
||||
|
||||
const loadEmojiData = useCallback(
|
||||
async (searchVal?: string) => {
|
||||
const { emojis, categories } = emojiData as EmojiMartData;
|
||||
|
||||
const filteredCategories = categories
|
||||
.map((category) => {
|
||||
const { id, emojis: categoryEmojis } = category;
|
||||
|
||||
return {
|
||||
id,
|
||||
emojis: categoryEmojis
|
||||
.filter((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
|
||||
if (!searchVal) return true;
|
||||
return filterSearchValue(emoji, searchVal);
|
||||
})
|
||||
.map((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
const { name, skins } = emoji;
|
||||
|
||||
return {
|
||||
id: emojiId,
|
||||
name,
|
||||
native: skins[skin] ? skins[skin].native : skins[0].native,
|
||||
};
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter((category) => category.emojis.length > 0);
|
||||
|
||||
setEmojiCategories(filteredCategories);
|
||||
},
|
||||
[skin]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { emojis, categories } = emojiData as EmojiMartData;
|
||||
void (async () => {
|
||||
await init({ data: emojiData, maxFrequentRows: MAX_FREQUENTLY_ROW_COUNT, perLine: PER_ROW_EMOJI_COUNT });
|
||||
await loadEmojiData();
|
||||
})();
|
||||
}, [loadEmojiData]);
|
||||
|
||||
const emojiCategories = categories
|
||||
.map((category) => {
|
||||
const { id, emojis: categoryEmojis } = category;
|
||||
useEffect(() => {
|
||||
void loadEmojiData(searchValue);
|
||||
}, [loadEmojiData, searchValue]);
|
||||
|
||||
return {
|
||||
id,
|
||||
emojis: categoryEmojis
|
||||
.filter((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
const onSelect = useCallback(
|
||||
async (native: string) => {
|
||||
onEmojiSelect(native);
|
||||
if (!native) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searchValue) return true;
|
||||
return filterSearchValue(emoji, searchValue);
|
||||
})
|
||||
.map((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
const { id, name, skins } = emoji;
|
||||
const data = await getEmojiDataFromNative(native);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
native: skins[skin] ? skins[skin].native : skins[0].native,
|
||||
};
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter((category) => category.emojis.length > 0);
|
||||
|
||||
setEmojiCategories(emojiCategories);
|
||||
}, [skin, searchValue]);
|
||||
FrequentlyUsed.add(data);
|
||||
},
|
||||
[onEmojiSelect]
|
||||
);
|
||||
|
||||
return {
|
||||
emojiCategories,
|
||||
skin,
|
||||
setSearchValue,
|
||||
searchValue,
|
||||
onSelect,
|
||||
onSkinChange,
|
||||
skin,
|
||||
};
|
||||
}
|
||||
|
||||
@ -124,9 +171,8 @@ export function useVirtualizedCategories({ count }: { count: number }) {
|
||||
count,
|
||||
getScrollElement: () => ref.current,
|
||||
estimateSize: () => {
|
||||
return 60;
|
||||
return EMOJI_SIZE;
|
||||
},
|
||||
overscan: 3,
|
||||
});
|
||||
|
||||
return { virtualize, ref };
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EMOJI_SIZE,
|
||||
EmojiCategory,
|
||||
getRowsWithCategories,
|
||||
PER_ROW_EMOJI_COUNT,
|
||||
useVirtualizedCategories,
|
||||
} from '$app/components/_shared/EmojiPicker/EmojiPicker.hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -16,7 +18,7 @@ function EmojiPickerCategories({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const rows = useMemo(() => {
|
||||
return getRowsWithCategories(emojiCategories, 13);
|
||||
return getRowsWithCategories(emojiCategories, PER_ROW_EMOJI_COUNT);
|
||||
}, [emojiCategories]);
|
||||
|
||||
const { ref, virtualize } = useVirtualizedCategories({
|
||||
@ -27,6 +29,7 @@ function EmojiPickerCategories({
|
||||
const getCategoryName = useCallback(
|
||||
(id: string) => {
|
||||
const i18nName: Record<string, string> = {
|
||||
frequent: t('emoji.categories.frequentlyUsed'),
|
||||
people: t('emoji.categories.people'),
|
||||
nature: t('emoji.categories.nature'),
|
||||
foods: t('emoji.categories.food'),
|
||||
@ -43,7 +46,12 @@ function EmojiPickerCategories({
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={'mt-2 w-[416px] flex-1 items-center justify-center overflow-y-auto overflow-x-hidden'}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`mt-2 w-[${
|
||||
EMOJI_SIZE * PER_ROW_EMOJI_COUNT
|
||||
}px] flex-1 items-center justify-center overflow-y-auto overflow-x-hidden`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: virtualize.getTotalSize(),
|
||||
@ -72,7 +80,14 @@ function EmojiPickerCategories({
|
||||
<div className={'flex'}>
|
||||
{item.emojis?.map((emoji) => {
|
||||
return (
|
||||
<div key={emoji.id} className={'flex h-[32px] w-[32px] items-center justify-center'}>
|
||||
<div
|
||||
key={emoji.id}
|
||||
style={{
|
||||
width: EMOJI_SIZE,
|
||||
height: EMOJI_SIZE,
|
||||
}}
|
||||
className={`flex items-center justify-center`}
|
||||
>
|
||||
<IconButton
|
||||
size={'small'}
|
||||
onClick={() => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, IconButton } from '@mui/material';
|
||||
import { DeleteOutlineRounded, SearchOutlined } from '@mui/icons-material';
|
||||
import { Circle, DeleteOutlineRounded, SearchOutlined } from '@mui/icons-material';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { randomEmoji } from '$app/utils/document/emoji';
|
||||
@ -12,26 +12,26 @@ import { useTranslation } from 'react-i18next';
|
||||
const skinTones = [
|
||||
{
|
||||
value: 0,
|
||||
label: '✋',
|
||||
color: '#ffc93a',
|
||||
},
|
||||
{
|
||||
label: '✋🏻',
|
||||
color: '#ffdab7',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '✋🏼',
|
||||
color: '#e7b98f',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '✋🏽',
|
||||
color: '#c88c61',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '✋🏾',
|
||||
color: '#a46134',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: '✋🏿',
|
||||
color: '#5d4437',
|
||||
value: 5,
|
||||
},
|
||||
];
|
||||
@ -78,7 +78,11 @@ function EmojiPickerHeader({ onEmojiSelect, onSkinSelect, searchValue, onSearchC
|
||||
<Tooltip title={t('emoji.selectSkinTone')}>
|
||||
<div className={'random-emoji-btn mr-2 rounded border border-line-divider'}>
|
||||
<IconButton size={'small'} className={'h-[25px] w-[25px]'} onClick={onOpen}>
|
||||
{skinTones[skin].label}
|
||||
<Circle
|
||||
style={{
|
||||
fill: skinTones[skin].color,
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -100,7 +104,7 @@ function EmojiPickerHeader({ onEmojiSelect, onSkinSelect, searchValue, onSearchC
|
||||
<div className={'mx-0.5'} key={skinTone.value}>
|
||||
<IconButton
|
||||
style={{
|
||||
backgroundColor: skinTone.value === skin ? 'var(--fill-list-hover)' : 'transparent',
|
||||
backgroundColor: skinTone.value === skin ? 'var(--fill-list-hover)' : undefined,
|
||||
}}
|
||||
size={'small'}
|
||||
onClick={() => {
|
||||
@ -108,7 +112,11 @@ function EmojiPickerHeader({ onEmojiSelect, onSkinSelect, searchValue, onSearchC
|
||||
popoverProps.onClose?.();
|
||||
}}
|
||||
>
|
||||
{skinTone.label}
|
||||
<Circle
|
||||
style={{
|
||||
fill: skinTone.color,
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,33 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useLoadEmojiData } from '$app/components/_shared/EmojiPicker/EmojiPicker.hooks';
|
||||
|
||||
import EmojiPickerHeader from '$app/components/_shared/EmojiPicker/EmojiPickerHeader';
|
||||
import EmojiPickerCategories from '$app/components/_shared/EmojiPicker/EmojiPickerCategories';
|
||||
import { useLoadEmojiData } from './EmojiPicker.hooks';
|
||||
import EmojiPickerHeader from './EmojiPickerHeader';
|
||||
import EmojiPickerCategories from './EmojiPickerCategories';
|
||||
|
||||
interface Props {
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
}
|
||||
|
||||
function EmojiPickerComponent({ onEmojiSelect }: Props) {
|
||||
const [skin, setSkin] = useState(0);
|
||||
|
||||
const { emojiCategories, setSearchValue, searchValue } = useLoadEmojiData({
|
||||
skin,
|
||||
});
|
||||
function EmojiPicker(props: Props) {
|
||||
const { skin, onSkinChange, emojiCategories, setSearchValue, searchValue, onSelect } = useLoadEmojiData(props);
|
||||
|
||||
return (
|
||||
<div className={'emoji-picker flex h-[360px] max-h-[70vh] flex-col p-4 pt-2'}>
|
||||
<EmojiPickerHeader
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
onEmojiSelect={onSelect}
|
||||
skin={skin}
|
||||
onSkinSelect={setSkin}
|
||||
onSkinSelect={onSkinChange}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
/>
|
||||
<EmojiPickerCategories onEmojiSelect={onEmojiSelect} emojiCategories={emojiCategories} />
|
||||
<EmojiPickerCategories onEmojiSelect={onSelect} emojiCategories={emojiCategories} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmojiPickerComponent;
|
||||
export default EmojiPicker;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { t } from 'i18next';
|
||||
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
|
||||
import { Button } from '../../_shared/Button';
|
||||
|
||||
import { useLogin } from '../Login/Login.hooks';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
export const GetStarted = () => {
|
||||
const { onAutoSignInClick } = useLogin();
|
||||
@ -20,8 +21,8 @@ export const GetStarted = () => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id='Get-Started' className='flex w-full max-w-[340px] flex-col gap-6 ' aria-label='Get-Started'>
|
||||
<Button size={'primary'} onClick={() => onAutoSignInClick()}>
|
||||
<div id='Get-Started' className='flex w-full max-w-[340px] flex-col ' aria-label='Get-Started'>
|
||||
<Button size={'large'} variant={'contained'} onClick={() => onAutoSignInClick()}>
|
||||
{t('signUp.getStartedText')}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -10,7 +10,7 @@ import { get } from '$app/utils/tool';
|
||||
|
||||
const headingBlockTopOffset: Record<number, string> = {
|
||||
1: '0.4rem',
|
||||
2: '0.2rem',
|
||||
2: '0.35rem',
|
||||
3: '0.15rem',
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,72 @@
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { updatePageIcon } from '$app_reducers/pages/async_actions';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ViewIconTypePB } from '@/services/backend';
|
||||
import { CoverType } from '$app/interfaces/document';
|
||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
export const heightCls = {
|
||||
cover: 'h-[220px]',
|
||||
icon: 'h-[80px]',
|
||||
coverAndIcon: 'h-[250px]',
|
||||
none: 'h-0',
|
||||
};
|
||||
|
||||
export function useDocumentBanner(id: string) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { docId, controller } = useSubscribeDocument();
|
||||
const icon = useAppSelector((state) => state.pages.pageMap[docId]?.icon);
|
||||
const { node } = useSubscribeNode(id);
|
||||
const { cover, coverType } = node.data;
|
||||
|
||||
const onUpdateIcon = useCallback(
|
||||
(icon: string) => {
|
||||
dispatch(
|
||||
updatePageIcon({
|
||||
id: docId,
|
||||
icon: icon
|
||||
? {
|
||||
ty: ViewIconTypePB.Emoji,
|
||||
value: icon,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, docId]
|
||||
);
|
||||
|
||||
const onUpdateCover = useCallback(
|
||||
(coverType: CoverType | null, cover: string | null) => {
|
||||
dispatch(
|
||||
updateNodeDataThunk({
|
||||
id,
|
||||
data: {
|
||||
coverType: coverType || '',
|
||||
cover: cover || '',
|
||||
},
|
||||
controller,
|
||||
})
|
||||
);
|
||||
},
|
||||
[controller, dispatch, id]
|
||||
);
|
||||
|
||||
const className = useMemo(() => {
|
||||
if (cover && icon) return heightCls.coverAndIcon;
|
||||
if (cover) return heightCls.cover;
|
||||
if (icon) return heightCls.icon;
|
||||
return heightCls.none;
|
||||
}, [cover, icon]);
|
||||
|
||||
return {
|
||||
onUpdateCover,
|
||||
onUpdateIcon,
|
||||
className,
|
||||
icon,
|
||||
cover,
|
||||
coverType,
|
||||
node,
|
||||
};
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import EmojiPicker from '$app/components/_shared/EmojiPicker';
|
||||
import { PageIcon } from '$app_reducers/pages/slice';
|
||||
|
||||
function DocumentIcon({
|
||||
icon,
|
||||
className,
|
||||
onUpdateIcon,
|
||||
}: {
|
||||
icon?: string;
|
||||
icon?: PageIcon;
|
||||
className?: string;
|
||||
onUpdateIcon: (icon: string) => void;
|
||||
}) {
|
||||
@ -41,13 +42,15 @@ function DocumentIcon({
|
||||
<>
|
||||
<div className={`absolute bottom-0 left-0 pt-[20px] ${className}`}>
|
||||
<div onClick={onOpen} className={'h-full w-full cursor-pointer rounded text-6xl hover:text-7xl'}>
|
||||
{icon}
|
||||
{icon.value}
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorReference='anchorPosition'
|
||||
anchorPosition={anchorPosition}
|
||||
disableAutoFocus
|
||||
disableRestoreFocus
|
||||
onClose={() => setAnchorPosition(undefined)}
|
||||
>
|
||||
<EmojiPicker onEmojiSelect={onEmojiSelect} />
|
@ -2,18 +2,23 @@ import React, { useCallback } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EmojiEmotionsOutlined, ImageOutlined } from '@mui/icons-material';
|
||||
import { BlockType, NestedBlock } from '$app/interfaces/document';
|
||||
import { randomColor } from '$app/components/document/DocumentTitle/cover/config';
|
||||
import { BlockType, CoverType, NestedBlock } from '$app/interfaces/document';
|
||||
import { randomColor } from '$app/components/document/DocumentBanner/cover/config';
|
||||
import { randomEmoji } from '$app/utils/document/emoji';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
interface Props {
|
||||
node: NestedBlock<BlockType.PageBlock>;
|
||||
onUpdateCover: (coverType: 'image' | 'color', cover: string) => void;
|
||||
onUpdateCover: (coverType: CoverType, cover: string) => void;
|
||||
onUpdateIcon: (icon: string) => void;
|
||||
}
|
||||
function TitleButtonGroup({ onUpdateIcon, onUpdateCover, node }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const showAddIcon = !node.data.icon;
|
||||
const { docId } = useSubscribeDocument();
|
||||
const icon = useAppSelector((state) => state.pages.pageMap[docId]?.icon);
|
||||
|
||||
const showAddIcon = !icon;
|
||||
const showAddCover = !node.data.cover;
|
||||
|
||||
const onAddIcon = useCallback(() => {
|
||||
@ -25,7 +30,7 @@ function TitleButtonGroup({ onUpdateIcon, onUpdateCover, node }: Props) {
|
||||
const onAddCover = useCallback(() => {
|
||||
const color = randomColor();
|
||||
|
||||
onUpdateCover('color', color);
|
||||
onUpdateCover(CoverType.Color, color);
|
||||
}, [onUpdateCover]);
|
||||
|
||||
return (
|
@ -1,9 +1,8 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { DeleteOutlineRounded } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ButtonGroup } from '@mui/material';
|
||||
import Button from '@mui/material/Button';
|
||||
import ChangeCoverPopover from '$app/components/document/DocumentTitle/cover/ChangeCoverPopover';
|
||||
import ChangeCoverPopover from '$app/components/document/DocumentBanner/cover/ChangeCoverPopover';
|
||||
import { CoverType } from '$app/interfaces/document';
|
||||
|
||||
function ChangeCoverButton({
|
||||
visible,
|
||||
@ -13,8 +12,8 @@ function ChangeCoverButton({
|
||||
}: {
|
||||
visible: boolean;
|
||||
cover: string;
|
||||
coverType: 'image' | 'color';
|
||||
onUpdateCover: (coverType: 'image' | 'color' | '', cover: string) => void;
|
||||
coverType: CoverType;
|
||||
onUpdateCover: (coverType: CoverType | null, cover: string | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [anchorPosition, setAnchorPosition] = useState<undefined | { top: number; left: number }>(undefined);
|
||||
@ -32,7 +31,7 @@ function ChangeCoverButton({
|
||||
}, []);
|
||||
|
||||
const onDeleteCover = useCallback(() => {
|
||||
onUpdateCover('', '');
|
||||
onUpdateCover(null, null);
|
||||
}, [onUpdateCover]);
|
||||
|
||||
return (
|
@ -1,30 +1,30 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Popover, { PopoverActions } from '@mui/material/Popover';
|
||||
import ChangeColors from '$app/components/document/DocumentTitle/cover/ChangeColors';
|
||||
import ChangeImages from '$app/components/document/DocumentTitle/cover/ChangeImages';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import React, { useRef } from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import ChangeColors from '$app/components/document/DocumentBanner/cover/ChangeColors';
|
||||
import ChangeImages from '$app/components/document/DocumentBanner/cover/ChangeImages';
|
||||
import { CoverType } from '$app/interfaces/document';
|
||||
|
||||
function ChangeCoverPopover({
|
||||
open,
|
||||
anchorPosition,
|
||||
onClose,
|
||||
coverType,
|
||||
cover,
|
||||
onUpdateCover,
|
||||
}: {
|
||||
open: boolean;
|
||||
anchorPosition?: { top: number; left: number };
|
||||
onClose: () => void;
|
||||
coverType: 'image' | 'color';
|
||||
coverType: CoverType;
|
||||
cover: string;
|
||||
onUpdateCover: (coverType: 'image' | 'color', cover: string) => void;
|
||||
onUpdateCover: (coverType: CoverType, cover: string) => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
disableAutoFocus
|
||||
disableRestoreFocus
|
||||
anchorReference={'anchorPosition'}
|
||||
anchorPosition={anchorPosition}
|
||||
onClose={onClose}
|
||||
@ -50,11 +50,11 @@ function ChangeCoverPopover({
|
||||
>
|
||||
<ChangeColors
|
||||
onChange={(color) => {
|
||||
onUpdateCover('color', color);
|
||||
onUpdateCover(CoverType.Color, color);
|
||||
}}
|
||||
cover={cover}
|
||||
/>
|
||||
<ChangeImages cover={cover} onChange={(url) => onUpdateCover('image', url)} />
|
||||
<ChangeImages cover={cover} onChange={(url) => onUpdateCover(CoverType.Image, url)} />
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import GalleryList from '$app/components/document/DocumentTitle/cover/GalleryList';
|
||||
import GalleryList from '$app/components/document/DocumentBanner/cover/GalleryList';
|
||||
import Button from '@mui/material/Button';
|
||||
import { readCoverImageUrls, readImage, writeCoverImageUrls } from '$app/utils/document/image';
|
||||
import { Log } from '$app/utils/log';
|
||||
import { Image } from '$app/components/document/DocumentTitle/cover/GalleryItem';
|
||||
import { Image } from '$app/components/document/DocumentBanner/cover/GalleryItem';
|
||||
|
||||
function ChangeImages({ cover, onChange }: { onChange: (url: string) => void; cover: string }) {
|
||||
const { t } = useTranslation();
|
@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import ChangeCoverButton from '$app/components/document/DocumentTitle/cover/ChangeCoverButton';
|
||||
import ChangeCoverButton from '$app/components/document/DocumentBanner/cover/ChangeCoverButton';
|
||||
import { readImage } from '$app/utils/document/image';
|
||||
import { CoverType } from '$app/interfaces/document';
|
||||
|
||||
function DocumentCover({
|
||||
cover,
|
||||
@ -9,9 +10,9 @@ function DocumentCover({
|
||||
onUpdateCover,
|
||||
}: {
|
||||
cover?: string;
|
||||
coverType?: 'image' | 'color';
|
||||
coverType?: CoverType;
|
||||
className?: string;
|
||||
onUpdateCover: (coverType: 'image' | 'color' | '', cover: string) => void;
|
||||
onUpdateCover: (coverType: CoverType | null, cover: string | null) => void;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [leftOffset, setLeftOffset] = useState(0);
|
||||
@ -55,7 +56,7 @@ function DocumentCover({
|
||||
}, [handleWidthChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (coverType === 'image' && cover) {
|
||||
if (coverType === CoverType.Image && cover) {
|
||||
void (async () => {
|
||||
const src = await readImage(cover);
|
||||
|
||||
@ -75,8 +76,11 @@ function DocumentCover({
|
||||
}}
|
||||
className={`absolute top-0 w-full overflow-hidden ${className}`}
|
||||
>
|
||||
{coverType === 'image' && <img src={coverSrc} className={'h-full w-full object-cover'} />}
|
||||
{coverType === 'color' && <div className={'h-full w-full'} style={{ backgroundColor: cover }} />}
|
||||
{coverType === CoverType.Image ? (
|
||||
<img src={coverSrc} className={'h-full w-full object-cover'} />
|
||||
) : (
|
||||
<div className={'h-full w-full'} style={{ backgroundColor: cover }} />
|
||||
)}
|
||||
<ChangeCoverButton onUpdateCover={onUpdateCover} visible={hover} cover={cover} coverType={coverType} />
|
||||
</div>
|
||||
);
|
@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ImageEdit from '$app/components/document/_shared/UploadImage/ImageEdit';
|
||||
import GalleryItem, { Image } from '$app/components/document/DocumentTitle/cover/GalleryItem';
|
||||
import GalleryItem, { Image } from '$app/components/document/DocumentBanner/cover/GalleryItem';
|
||||
|
||||
interface Props {
|
||||
onSelected: (image: Image) => void;
|
@ -0,0 +1,30 @@
|
||||
import { heightCls, useDocumentBanner } from './DocumentBanner.hooks';
|
||||
import TitleButtonGroup from './TitleButtonGroup';
|
||||
import DocumentCover from './cover/DocumentCover';
|
||||
import DocumentIcon from './DocumentIcon';
|
||||
|
||||
function DocumentBanner({ id, hover }: { id: string; hover: boolean }) {
|
||||
const { onUpdateCover, node, onUpdateIcon, icon, cover, className, coverType } = useDocumentBanner(id);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: icon || cover ? 'block' : 'none',
|
||||
}}
|
||||
className={`relative ${className}`}
|
||||
>
|
||||
<DocumentCover onUpdateCover={onUpdateCover} className={heightCls.cover} cover={cover} coverType={coverType} />
|
||||
<DocumentIcon onUpdateIcon={onUpdateIcon} className={heightCls.icon} icon={icon} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
opacity: hover ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<TitleButtonGroup node={node} onUpdateCover={onUpdateCover} onUpdateIcon={onUpdateIcon} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocumentBanner;
|
@ -1,47 +1,28 @@
|
||||
import { useSubscribeNode } from '../_shared/SubscribeNode.hooks';
|
||||
import { useCallback } from 'react';
|
||||
import { updateNodeDataThunk } from '$app_reducers/document/async-actions';
|
||||
import { useEffect } from 'react';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { documentActions } from '$app_reducers/document/slice';
|
||||
|
||||
export function useDocumentTitle(id: string) {
|
||||
const { node } = useSubscribeNode(id);
|
||||
const { controller } = useSubscribeDocument();
|
||||
const dispatch = useAppDispatch();
|
||||
const onUpdateIcon = useCallback(
|
||||
(icon: string) => {
|
||||
dispatch(
|
||||
updateNodeDataThunk({
|
||||
id,
|
||||
data: {
|
||||
icon,
|
||||
},
|
||||
controller,
|
||||
})
|
||||
);
|
||||
},
|
||||
[controller, dispatch, id]
|
||||
);
|
||||
const { docId } = useSubscribeDocument();
|
||||
const page = useAppSelector((state) => state.pages.pageMap[docId]);
|
||||
|
||||
const onUpdateCover = useCallback(
|
||||
(coverType: 'image' | 'color' | '', cover: string) => {
|
||||
useEffect(() => {
|
||||
if (page) {
|
||||
dispatch(
|
||||
updateNodeDataThunk({
|
||||
id,
|
||||
data: {
|
||||
cover,
|
||||
coverType,
|
||||
},
|
||||
controller,
|
||||
documentActions.updateRootNodeDelta({
|
||||
docId,
|
||||
delta: [{ insert: page.name }],
|
||||
rootId: id,
|
||||
})
|
||||
);
|
||||
},
|
||||
[controller, dispatch, id]
|
||||
);
|
||||
}
|
||||
}, [dispatch, docId, id, page]);
|
||||
|
||||
return {
|
||||
node,
|
||||
onUpdateCover,
|
||||
onUpdateIcon,
|
||||
};
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { BlockType, NestedBlock } from '$app/interfaces/document';
|
||||
import DocumentCover from '$app/components/document/DocumentTitle/cover/DocumentCover';
|
||||
import DocumentIcon from '$app/components/document/DocumentTitle/DocumentIcon';
|
||||
|
||||
const heightCls = {
|
||||
cover: 'h-[220px]',
|
||||
icon: 'h-[80px]',
|
||||
coverAndIcon: 'h-[250px]',
|
||||
none: 'h-0',
|
||||
};
|
||||
|
||||
function DocumentTopPanel({
|
||||
node,
|
||||
onUpdateCover,
|
||||
onUpdateIcon,
|
||||
}: {
|
||||
node: NestedBlock<BlockType.PageBlock>;
|
||||
onUpdateCover: (coverType: 'image' | 'color' | '', cover: string) => void;
|
||||
onUpdateIcon: (icon: string) => void;
|
||||
}) {
|
||||
const { cover, coverType, icon } = node.data;
|
||||
|
||||
const className = useMemo(() => {
|
||||
if (cover && icon) return heightCls.coverAndIcon;
|
||||
if (cover) return heightCls.cover;
|
||||
if (icon) return heightCls.icon;
|
||||
return heightCls.none;
|
||||
}, [cover, icon]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: icon || cover ? 'block' : 'none',
|
||||
}}
|
||||
className={`relative ${className}`}
|
||||
>
|
||||
<DocumentCover onUpdateCover={onUpdateCover} className={heightCls.cover} cover={cover} coverType={coverType} />
|
||||
<DocumentIcon onUpdateIcon={onUpdateIcon} className={heightCls.icon} icon={icon} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocumentTopPanel;
|
@ -2,11 +2,10 @@ import React, { useState } from 'react';
|
||||
import { useDocumentTitle } from './DocumentTitle.hooks';
|
||||
import TextBlock from '../TextBlock';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TitleButtonGroup from './TitleButtonGroup';
|
||||
import DocumentTopPanel from './DocumentTopPanel';
|
||||
import DocumentBanner from '$app/components/document/DocumentBanner';
|
||||
|
||||
export default function DocumentTitle({ id }: { id: string }) {
|
||||
const { node, onUpdateCover, onUpdateIcon } = useDocumentTitle(id);
|
||||
const { node } = useDocumentTitle(id);
|
||||
const { t } = useTranslation();
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
@ -14,14 +13,7 @@ export default function DocumentTitle({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
|
||||
<DocumentTopPanel onUpdateCover={onUpdateCover} onUpdateIcon={onUpdateIcon} node={node} />
|
||||
<div
|
||||
style={{
|
||||
opacity: hover ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<TitleButtonGroup node={node} onUpdateCover={onUpdateCover} onUpdateIcon={onUpdateIcon} />
|
||||
</div>
|
||||
<DocumentBanner id={node.id} hover={hover} />
|
||||
<div data-block-id={node.id} className='doc-title relative text-4xl font-bold'>
|
||||
<TextBlock placeholder={t('document.title.placeholder')} childIds={[]} node={node} />
|
||||
</div>
|
||||
|
@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import BlockSideToolbar from '../BlockSideToolbar';
|
||||
import BlockSelection from '../BlockSelection';
|
||||
import TextActionMenu from '$app/components/document/TextActionMenu';
|
||||
import BlockSlash from '$app/components/document/BlockSlash';
|
||||
import { useCopy } from '$app/components/document/_shared/CopyPasteHooks/useCopy';
|
||||
import { usePaste } from '$app/components/document/_shared/CopyPasteHooks/usePaste';
|
||||
import LinkEditPopover from '$app/components/document/_shared/TextLink/LinkEditPopover';
|
||||
import { useUndoRedo } from '$app/components/document/_shared/UndoHooks/useUndoRedo';
|
||||
import TemporaryPopover from '$app/components/document/_shared/TemporaryInput/TemporaryPopover';
|
||||
|
||||
@ -18,7 +16,6 @@ export default function Overlay({ container }: { container: HTMLDivElement }) {
|
||||
<TextActionMenu container={container} />
|
||||
<BlockSelection container={container} />
|
||||
<BlockSlash container={container} />
|
||||
<LinkEditPopover />
|
||||
<TemporaryPopover />
|
||||
</>
|
||||
);
|
||||
|
@ -10,8 +10,8 @@ export default function QuoteBlock({
|
||||
childIds?: string[];
|
||||
}) {
|
||||
return (
|
||||
<div className={'py-[2px]'}>
|
||||
<div className={'border-l-4 border-solid border-fill-default px-3 '}>
|
||||
<div className={'py-[2px] pl-0.5'}>
|
||||
<div className={'border-l-4 border-solid border-fill-default pl-3'}>
|
||||
<TextBlock node={node} />
|
||||
<NodeChildren childIds={childIds} />
|
||||
</div>
|
||||
|
@ -4,7 +4,6 @@ import ToolbarTooltip from '$app/components/document/_shared/ToolbarTooltip';
|
||||
import { getFormatActiveThunk, toggleFormatThunk } from '$app_reducers/document/async-actions/format';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
import { newLinkThunk } from '$app_reducers/document/async-actions/link';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { RANGE_NAME } from '$app/constants/document/name';
|
||||
import { createTemporary } from '$app_reducers/document/async-actions/temporary';
|
||||
@ -57,14 +56,6 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
||||
[controller, dispatch, isActive]
|
||||
);
|
||||
|
||||
const addLink = useCallback(() => {
|
||||
dispatch(
|
||||
newLinkThunk({
|
||||
docId,
|
||||
})
|
||||
);
|
||||
}, [dispatch, docId]);
|
||||
|
||||
const addTemporaryInput = useCallback(
|
||||
(type: TemporaryType) => {
|
||||
dispatch(createTemporary({ type, docId }));
|
||||
@ -103,12 +94,12 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
||||
case TextAction.Code:
|
||||
return toggleFormat(format);
|
||||
case TextAction.Link:
|
||||
return addLink();
|
||||
return addTemporaryInput(TemporaryType.Link);
|
||||
case TextAction.Equation:
|
||||
return addTemporaryInput(TemporaryType.Equation);
|
||||
}
|
||||
},
|
||||
[addLink, addTemporaryInput, toggleFormat]
|
||||
[addTemporaryInput, toggleFormat]
|
||||
);
|
||||
|
||||
const formatIcon = useMemo(() => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { useVirtualizedList } from './VirtualizedList.hooks';
|
||||
import DocumentTitle from '../DocumentTitle';
|
||||
import Overlay from '../Overlay';
|
||||
@ -15,9 +14,8 @@ export default function VirtualizedList({
|
||||
node: Node;
|
||||
renderNode: (nodeId: string) => JSX.Element;
|
||||
}) {
|
||||
const { virtualize, parentRef } = useVirtualizedList(childIds.length);
|
||||
const { virtualize, parentRef } = useVirtualizedList(childIds.length + 1);
|
||||
const virtualItems = virtualize.getVirtualItems();
|
||||
|
||||
const { docId } = useSubscribeDocument();
|
||||
|
||||
return (
|
||||
@ -46,12 +44,14 @@ export default function VirtualizedList({
|
||||
}}
|
||||
>
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const id = childIds[virtualRow.index];
|
||||
const isDocumentTitle = virtualRow.index === 0;
|
||||
const id = isDocumentTitle ? node.id : childIds[virtualRow.index - 1];
|
||||
|
||||
return (
|
||||
<div className={'pt-[0.5px]'} key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
|
||||
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
||||
{renderNode(id)}
|
||||
<div className={isDocumentTitle ? '' : 'pt-[0.5px]'} key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
|
||||
{
|
||||
isDocumentTitle ? <DocumentTitle id={node.id} /> : renderNode(id)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -22,7 +22,6 @@ function InlineContainer({
|
||||
}: {
|
||||
getSelection: (node: Element) => RangeStaticNoId | null;
|
||||
children: React.ReactNode;
|
||||
formula: string;
|
||||
selectedText: string;
|
||||
isLast: boolean;
|
||||
isFirst: boolean;
|
||||
@ -52,7 +51,7 @@ function InlineContainer({
|
||||
selection,
|
||||
selectedText,
|
||||
type: temporaryType,
|
||||
data: temporaryData as { latex: string },
|
||||
data: temporaryData
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -0,0 +1,67 @@
|
||||
import React, { useCallback, useContext, useRef } from 'react';
|
||||
import { RangeStaticNoId, TemporaryType } from '$app/interfaces/document';
|
||||
import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { createTemporary } from '$app_reducers/document/async-actions/temporary';
|
||||
|
||||
function LinkInline({
|
||||
children,
|
||||
getSelection,
|
||||
selectedText,
|
||||
temporaryType,
|
||||
data,
|
||||
}: {
|
||||
getSelection: (node: Element) => RangeStaticNoId | null;
|
||||
children: React.ReactNode;
|
||||
selectedText: string;
|
||||
temporaryType: TemporaryType;
|
||||
data: {
|
||||
href?: string;
|
||||
};
|
||||
}) {
|
||||
const id = useContext(NodeIdContext);
|
||||
const { docId } = useSubscribeDocument();
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!ref.current) return;
|
||||
const selection = getSelection(ref.current);
|
||||
|
||||
if (!selection) return;
|
||||
const rect = ref.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(
|
||||
createTemporary({
|
||||
docId,
|
||||
state: {
|
||||
id,
|
||||
selection,
|
||||
selectedText,
|
||||
type: temporaryType,
|
||||
data: {
|
||||
href: data.href,
|
||||
text: selectedText,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[data, dispatch, docId, getSelection, id, selectedText, temporaryType]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span onClick={onClick} ref={ref} className='cursor-pointer text-text-link-default'>
|
||||
<span className={' border-b-[1px] border-b-text-link-default'}>{children}</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkInline;
|
@ -1,12 +1,11 @@
|
||||
import { ReactEditor, RenderLeafProps } from 'slate-react';
|
||||
import { BaseText } from 'slate';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import TextLink from '../TextLink';
|
||||
import { converToIndexLength } from '$app/utils/document/slate_editor';
|
||||
import LinkHighLight from '$app/components/document/_shared/TextLink/LinkHighLight';
|
||||
import TemporaryInput from '$app/components/document/_shared/TemporaryInput';
|
||||
import InlineContainer from '$app/components/document/_shared/InlineBlock/InlineContainer';
|
||||
import { TemporaryType } from '$app/interfaces/document';
|
||||
import LinkInline from '$app/components/document/_shared/InlineBlock/LinkInline';
|
||||
|
||||
interface Attributes {
|
||||
bold?: boolean;
|
||||
@ -17,8 +16,6 @@ interface Attributes {
|
||||
selection_high_lighted?: boolean;
|
||||
href?: string;
|
||||
prism_token?: string;
|
||||
link_selection_lighted?: boolean;
|
||||
link_placeholder?: string;
|
||||
temporary?: boolean;
|
||||
formula?: string;
|
||||
font_color?: string;
|
||||
@ -69,9 +66,16 @@ const TextLeaf = (props: TextLeafProps) => {
|
||||
|
||||
if (leaf.href) {
|
||||
newChildren = (
|
||||
<TextLink getSelection={getSelection} title={leaf.text} href={leaf.href}>
|
||||
<LinkInline
|
||||
temporaryType={TemporaryType.Link}
|
||||
getSelection={getSelection}
|
||||
selectedText={leaf.text}
|
||||
data={{
|
||||
href: leaf.href,
|
||||
}}
|
||||
>
|
||||
{newChildren}
|
||||
</TextLink>
|
||||
</LinkInline>
|
||||
);
|
||||
}
|
||||
|
||||
@ -85,7 +89,6 @@ const TextLeaf = (props: TextLeafProps) => {
|
||||
isLast={isLast}
|
||||
isFirst={text === parent.children[0]}
|
||||
getSelection={getSelection}
|
||||
formula={leaf.formula}
|
||||
data={data}
|
||||
temporaryType={temporaryType}
|
||||
selectedText={leaf.text}
|
||||
@ -100,21 +103,12 @@ const TextLeaf = (props: TextLeafProps) => {
|
||||
leaf.prism_token && leaf.prism_token,
|
||||
leaf.strikethrough && 'line-through',
|
||||
leaf.selection_high_lighted && 'bg-content-blue-100',
|
||||
leaf.link_selection_lighted && 'text-text-link-selector bg-content-blue-100',
|
||||
leaf.code && !leaf.temporary && 'inline-code',
|
||||
leaf.bold && 'font-bold',
|
||||
leaf.italic && 'italic',
|
||||
leaf.underline && 'underline',
|
||||
].filter(Boolean);
|
||||
|
||||
if (leaf.link_placeholder && leaf.text) {
|
||||
newChildren = (
|
||||
<LinkHighLight leaf={leaf} title={leaf.link_placeholder}>
|
||||
{newChildren}
|
||||
</LinkHighLight>
|
||||
);
|
||||
}
|
||||
|
||||
if (leaf.temporary) {
|
||||
newChildren = (
|
||||
<TemporaryInput getSelection={getSelection} leaf={leaf}>
|
||||
|
@ -25,7 +25,6 @@ export function useEditor({
|
||||
decorateSelection,
|
||||
onKeyDown,
|
||||
isCodeBlock,
|
||||
linkDecorateSelection,
|
||||
temporarySelection,
|
||||
}: EditorProps) {
|
||||
const { editor } = useSlateYjs({ delta });
|
||||
@ -97,10 +96,6 @@ export function useEditor({
|
||||
getDecorateRange(path, decorateSelection, {
|
||||
selection_high_lighted: true,
|
||||
}),
|
||||
getDecorateRange(path, linkDecorateSelection?.selection, {
|
||||
link_selection_lighted: true,
|
||||
link_placeholder: linkDecorateSelection?.placeholder,
|
||||
}),
|
||||
getDecorateRange(path, temporarySelection, {
|
||||
temporary: true,
|
||||
}),
|
||||
@ -108,7 +103,7 @@ export function useEditor({
|
||||
|
||||
return ranges;
|
||||
},
|
||||
[temporarySelection, decorateSelection, linkDecorateSelection, getDecorateRange]
|
||||
[temporarySelection, decorateSelection, getDecorateRange]
|
||||
);
|
||||
|
||||
const onKeyDownRewrite = useCallback(
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { TEXT_LINK_NAME } from '$app/constants/document/name';
|
||||
|
||||
export function useSubscribeLinkPopover() {
|
||||
const { docId } = useSubscribeDocument();
|
||||
|
||||
const linkPopover = useAppSelector((state) => {
|
||||
return state[TEXT_LINK_NAME][docId];
|
||||
});
|
||||
|
||||
return linkPopover;
|
||||
}
|
@ -18,19 +18,8 @@ export function useSubscribeDecorate(id: string) {
|
||||
return temporary.selection;
|
||||
});
|
||||
|
||||
const linkDecorateSelection = useAppSelector((state) => {
|
||||
const linkPopoverState = state[TEXT_LINK_NAME][docId];
|
||||
|
||||
if (!linkPopoverState?.open || linkPopoverState?.id !== id) return;
|
||||
return {
|
||||
selection: linkPopoverState.selection,
|
||||
placeholder: linkPopoverState.title,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
decorateSelection,
|
||||
linkDecorateSelection,
|
||||
temporarySelection,
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,125 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { LinkOff, OpenInNew } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import CopyIcon from '@mui/icons-material/CopyAll';
|
||||
import { copyText } from '$app/utils/document/copy_paste';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
|
||||
function LinkEditContent({
|
||||
value,
|
||||
onChange,
|
||||
onConfirm,
|
||||
}: {
|
||||
value: {
|
||||
href?: string;
|
||||
text?: string;
|
||||
};
|
||||
onChange: (val: { href: string; text: string }) => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
const valueRef = useRef<{
|
||||
href?: string;
|
||||
text?: string;
|
||||
}>(value);
|
||||
const { t } = useTranslation();
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onConfirm();
|
||||
}
|
||||
},
|
||||
[onConfirm]
|
||||
);
|
||||
|
||||
const operations = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <OpenInNew />,
|
||||
tooltip: t('document.inlineLink.openInNewTab'),
|
||||
onClick: () => {
|
||||
void open(valueRef.current.href || '');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <CopyIcon />,
|
||||
tooltip: t('document.inlineLink.copyLink'),
|
||||
onClick: () => {
|
||||
void copyText(valueRef.current.href || '');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <LinkOff />,
|
||||
tooltip: t('document.inlineLink.removeLink'),
|
||||
onClick: () => {
|
||||
onChange({
|
||||
href: '',
|
||||
text: valueRef.current.text || '',
|
||||
});
|
||||
onConfirm();
|
||||
},
|
||||
},
|
||||
],
|
||||
[onChange, t, onConfirm]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex w-[420px] flex-col items-end p-4'}>
|
||||
<div className={'flex w-full items-center justify-end'}>
|
||||
{operations.map((operation, index) => (
|
||||
<Tooltip placement={'top'} key={index} title={operation.tooltip}>
|
||||
<div className={'ml-2 cursor-pointer rounded border border-line-divider'}>
|
||||
<IconButton onClick={operation.onClick}>{operation.icon}</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<div className={'flex h-[150px] w-full flex-col justify-between'}>
|
||||
<TextField
|
||||
autoFocus
|
||||
placeholder={t('document.inlineLink.url.placeholder')}
|
||||
label={t('document.inlineLink.url.label')}
|
||||
onKeyDown={onKeyDown}
|
||||
variant='standard'
|
||||
value={value.href}
|
||||
onChange={(e) => {
|
||||
const newVal = e.target.value;
|
||||
|
||||
if (newVal === value.href) return;
|
||||
onChange({
|
||||
text: value.text || '',
|
||||
href: newVal,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
placeholder={t('document.inlineLink.title.placeholder')}
|
||||
label={t('document.inlineLink.title.label')}
|
||||
onKeyDown={onKeyDown}
|
||||
variant='standard'
|
||||
value={value.text}
|
||||
onChange={(e) => {
|
||||
const newVal = e.target.value;
|
||||
|
||||
if (newVal === value.text) return;
|
||||
onChange({
|
||||
text: newVal,
|
||||
href: value.href || '',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className={'flex w-full items-center justify-end'}>
|
||||
<Button onClick={onConfirm} color='primary'>
|
||||
{t('button.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkEditContent;
|
@ -1,4 +1,3 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Functions } from '@mui/icons-material';
|
||||
import KatexMath from '$app/components/document/_shared/KatexMath';
|
||||
|
||||
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { AddLinkOutlined } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function TemporaryLink({ href = '', text = '' }: { href?: string; text?: string }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className={'bg-content-blue-100'} contentEditable={false}>
|
||||
{text ? (
|
||||
<span className={'text-text-link-default underline'}>{text}</span>
|
||||
) : (
|
||||
<span className={'text-text-caption'}>
|
||||
<AddLinkOutlined /> {t('document.inlineLink.title.label')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default TemporaryLink;
|
@ -8,6 +8,7 @@ import { formatTemporary } from '$app_reducers/document/async-actions/temporary'
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useSubscribeTemporary } from '$app/components/document/_shared/SubscribeTemporary.hooks';
|
||||
import LinkEditContent from '$app/components/document/_shared/TemporaryInput/LinkEditContent';
|
||||
|
||||
const AFTER_RENDER_DELAY = 100;
|
||||
|
||||
@ -97,7 +98,7 @@ function TemporaryPopover() {
|
||||
case TemporaryType.Equation:
|
||||
return (
|
||||
<EquationEditContent
|
||||
value={data.latex}
|
||||
value={data.latex || ''}
|
||||
onChange={(latex: string) =>
|
||||
onChangeData({
|
||||
latex,
|
||||
@ -106,6 +107,17 @@ function TemporaryPopover() {
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
);
|
||||
case TemporaryType.Link:
|
||||
return (
|
||||
<LinkEditContent
|
||||
value={{
|
||||
href: data.href || '',
|
||||
text: data.text || '',
|
||||
}}
|
||||
onChange={(val: { href: string; text: string }) => onChangeData(val)}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [onChangeData, onConfirm, temporaryState]);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { RangeStaticNoId, TemporaryType } from '$app/interfaces/document';
|
||||
import TemporaryEquation from '$app/components/document/_shared/TemporaryInput/TemporaryEquation';
|
||||
import { useSubscribeTemporary } from '$app/components/document/_shared/SubscribeTemporary.hooks';
|
||||
@ -6,6 +6,7 @@ import { PopoverPosition } from '@mui/material';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { temporaryActions } from '$app_reducers/document/temporary_slice';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import TemporaryLink from '$app/components/document/_shared/TemporaryInput/TemporaryLink';
|
||||
|
||||
function TemporaryInput({
|
||||
leaf,
|
||||
@ -21,7 +22,9 @@ function TemporaryInput({
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const { docId } = useSubscribeDocument();
|
||||
const match = useMemo(() => {
|
||||
const [match, setMatch] = useState(false);
|
||||
|
||||
const getMatch = useCallback(() => {
|
||||
if (!ref.current) return false;
|
||||
if (!leaf.text) return false;
|
||||
if (!temporaryState) return false;
|
||||
@ -29,6 +32,7 @@ function TemporaryInput({
|
||||
const selection = getSelection(ref.current);
|
||||
|
||||
if (!selection) return false;
|
||||
|
||||
return leaf.text === selectedText || selection.index <= temporaryState.selection.index;
|
||||
}, [leaf.text, temporaryState, getSelection]);
|
||||
|
||||
@ -38,7 +42,9 @@ function TemporaryInput({
|
||||
|
||||
switch (type) {
|
||||
case TemporaryType.Equation:
|
||||
return <TemporaryEquation latex={data.latex} />;
|
||||
return <TemporaryEquation latex={data.latex || ''} />;
|
||||
case TemporaryType.Link:
|
||||
return <TemporaryLink {...data} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -69,6 +75,11 @@ function TemporaryInput({
|
||||
});
|
||||
}, [dispatch, docId, id, match, setAnchorPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
const match = getMatch();
|
||||
setMatch(match);
|
||||
}, [getMatch]);
|
||||
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{match ? renderPlaceholder() : null}
|
||||
|
@ -1,39 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
||||
function EditLink({
|
||||
autoFocus,
|
||||
text,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
autoFocus?: boolean;
|
||||
text: string;
|
||||
value: string;
|
||||
onChange?: (newValue: string) => void;
|
||||
}) {
|
||||
const [val, setVal] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(val);
|
||||
}, [val, onChange]);
|
||||
|
||||
return (
|
||||
<div className={'mb-2 w-[100%] text-sm'}>
|
||||
<TextField
|
||||
className={'w-[100%]'}
|
||||
label={text}
|
||||
autoFocus={autoFocus}
|
||||
variant='standard'
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
|
||||
setVal(newValue);
|
||||
}}
|
||||
value={val}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditLink;
|
@ -1,96 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import BlockPortal from '$app/components/document/BlockPortal';
|
||||
import { getNode } from '$app/utils/document/node';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import CopyIcon from '@mui/icons-material/CopyAll';
|
||||
import { copyText } from '$app/utils/document/copy_paste';
|
||||
import { useMessage } from '$app/components/document/_shared/Message';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const iconSize = {
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
};
|
||||
|
||||
function EditLinkToolbar({
|
||||
blockId,
|
||||
linkElement,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
href,
|
||||
editing,
|
||||
onEdit,
|
||||
}: {
|
||||
blockId: string;
|
||||
linkElement: HTMLAnchorElement;
|
||||
href: string;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
editing: boolean;
|
||||
onEdit: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { show, contentHolder } = useMessage();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const toolbarDom = ref.current;
|
||||
|
||||
if (!toolbarDom) return;
|
||||
|
||||
const linkRect = linkElement.getBoundingClientRect();
|
||||
const node = getNode(blockId);
|
||||
|
||||
if (!node) return;
|
||||
const nodeRect = node.getBoundingClientRect();
|
||||
const top = linkRect.top - nodeRect.top + linkRect.height + 4;
|
||||
const left = linkRect.left - nodeRect.left;
|
||||
|
||||
toolbarDom.style.top = `${top}px`;
|
||||
toolbarDom.style.left = `${left}px`;
|
||||
toolbarDom.style.opacity = '1';
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<BlockPortal blockId={blockId}>
|
||||
<div
|
||||
ref={ref}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
style={{
|
||||
opacity: 0,
|
||||
}}
|
||||
className='absolute z-10 inline-flex h-[32px] min-w-[200px] max-w-[400px] items-stretch overflow-hidden rounded-[8px] bg-bg-body leading-tight text-text-title shadow-md transition-opacity duration-100'
|
||||
>
|
||||
<div className={'flex w-[100%] items-center justify-between px-2 text-[75%]'}>
|
||||
<div className={'mr-2'}>
|
||||
<LanguageIcon sx={iconSize} />
|
||||
</div>
|
||||
<div className={'mr-2 flex-1 overflow-hidden text-ellipsis whitespace-nowrap'}>{href}</div>
|
||||
<div
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyText(href);
|
||||
show({ message: t('message.copy.success'), duration: 6000 });
|
||||
} catch {
|
||||
show({ message: t('message.copy.fail'), duration: 6000 });
|
||||
}
|
||||
}}
|
||||
className={'mr-2 cursor-pointer'}
|
||||
>
|
||||
<CopyIcon sx={iconSize} />
|
||||
</div>
|
||||
<div onClick={onEdit} className={'cursor-pointer'}>
|
||||
{t('button.edit')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlockPortal>
|
||||
)}
|
||||
{contentHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditLinkToolbar;
|
@ -1,135 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { DeleteOutline, Done } from '@mui/icons-material';
|
||||
import EditLink from '$app/components/document/_shared/TextLink/EditLink';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { linkPopoverActions, rangeActions } from '$app_reducers/document/slice';
|
||||
import { formatLinkThunk } from '$app_reducers/document/async-actions/link';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
import { useSubscribeLinkPopover } from '$app/components/document/_shared/SubscribeLinkPopover.hooks';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function LinkEditPopover() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { docId, controller } = useSubscribeDocument();
|
||||
const { t } = useTranslation();
|
||||
const popoverState = useSubscribeLinkPopover();
|
||||
const { anchorPosition, id, selection, title = '', href = '', open = false } = popoverState;
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(linkPopoverActions.closeLinkPopover(docId));
|
||||
}, [dispatch, docId]);
|
||||
|
||||
const onExited = useCallback(() => {
|
||||
if (!id || !selection) return;
|
||||
const newSelection = {
|
||||
index: selection.index,
|
||||
length: title.length,
|
||||
};
|
||||
|
||||
dispatch(
|
||||
rangeActions.setRange({
|
||||
docId,
|
||||
id,
|
||||
rangeStatic: newSelection,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
rangeActions.setCaret({
|
||||
docId,
|
||||
caret: {
|
||||
id,
|
||||
...newSelection,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [docId, id, selection, title, dispatch]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(newVal: { href?: string; title: string }) => {
|
||||
if (!id) return;
|
||||
if (newVal.title === title && newVal.href === href) return;
|
||||
|
||||
dispatch(
|
||||
linkPopoverActions.updateLinkPopover({
|
||||
docId,
|
||||
linkState: {
|
||||
id,
|
||||
href: newVal.href,
|
||||
title: newVal.title,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[docId, dispatch, href, id, title]
|
||||
);
|
||||
|
||||
const onDone = useCallback(async () => {
|
||||
if (!controller) return;
|
||||
await dispatch(
|
||||
formatLinkThunk({
|
||||
controller,
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
}, [controller, dispatch, onClose]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
open={open}
|
||||
disableAutoFocus={true}
|
||||
anchorReference='anchorPosition'
|
||||
anchorPosition={anchorPosition}
|
||||
TransitionProps={{
|
||||
onExited,
|
||||
}}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col p-3'>
|
||||
<EditLink
|
||||
text={t('document.inlineLink.url.label')}
|
||||
value={href}
|
||||
onChange={(link) => {
|
||||
onChange({
|
||||
href: link,
|
||||
title,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<EditLink
|
||||
text={t('document.inlineLink.title.label')}
|
||||
value={title}
|
||||
onChange={(text) =>
|
||||
onChange({
|
||||
href,
|
||||
title: text,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className={'flex items-center justify-end'}>
|
||||
<Button onClick={onDone}>
|
||||
<Done />
|
||||
{t('button.done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkEditPopover;
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { isOverlappingPrefix } from '$app/utils/document/temporary';
|
||||
|
||||
function LinkHighLight({ children, leaf, title }: { leaf: { text: string }; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
{leaf.text === title || isOverlappingPrefix(leaf.text, title) ? (
|
||||
<span contentEditable={false}>{title}</span>
|
||||
) : null}
|
||||
|
||||
<span className={'absolute opacity-0'}>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkHighLight;
|
@ -1,27 +0,0 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { debounce } from '$app/utils/tool';
|
||||
|
||||
export function useTextLink(id: string) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const ref = useRef<HTMLAnchorElement | null>(null);
|
||||
|
||||
const show = useMemo(() => debounce(() => setEditing(true), 500), []);
|
||||
const hide = useMemo(() => debounce(() => setEditing(false), 500), []);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
hide.cancel();
|
||||
show();
|
||||
}, [hide, show]);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
show.cancel();
|
||||
hide();
|
||||
}, [hide, show]);
|
||||
|
||||
return {
|
||||
editing,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
ref,
|
||||
};
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||
import { useTextLink } from '$app/components/document/_shared/TextLink/TextLink.hooks';
|
||||
import EditLinkToolbar from '$app/components/document/_shared/TextLink/EditLinkToolbar';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { linkPopoverActions } from '$app_reducers/document/slice';
|
||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||
|
||||
function TextLink({
|
||||
getSelection,
|
||||
title,
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
getSelection: (node: Element) => {
|
||||
index: number;
|
||||
length: number;
|
||||
} | null;
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
title: string;
|
||||
}) {
|
||||
const blockId = useContext(NodeIdContext);
|
||||
const { editing, ref, onMouseEnter, onMouseLeave } = useTextLink(blockId);
|
||||
const dispatch = useAppDispatch();
|
||||
const { docId } = useSubscribeDocument();
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
if (!ref.current) return;
|
||||
const selection = getSelection(ref.current);
|
||||
|
||||
if (!selection) return;
|
||||
const rect = ref.current?.getBoundingClientRect();
|
||||
|
||||
if (!rect) return;
|
||||
dispatch(
|
||||
linkPopoverActions.setLinkPopover({
|
||||
docId,
|
||||
linkState: {
|
||||
anchorPosition: {
|
||||
top: rect.top + rect.height,
|
||||
left: rect.left + rect.width / 2,
|
||||
},
|
||||
id: blockId,
|
||||
selection,
|
||||
title,
|
||||
href,
|
||||
open: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [blockId, dispatch, docId, getSelection, href, ref, title]);
|
||||
|
||||
if (!blockId) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
ref={ref}
|
||||
href={href}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='cursor-pointer text-text-link-default'
|
||||
>
|
||||
<span className={' border-b-[1px] border-b-text-link-default '}>{children}</span>
|
||||
</a>
|
||||
{ref.current && (
|
||||
<EditLinkToolbar
|
||||
editing={editing}
|
||||
href={href}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
linkElement={ref.current}
|
||||
blockId={blockId}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextLink;
|
@ -1,18 +1,17 @@
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useAppSelector } from "$app/stores/store";
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { PageController } from '$app/stores/effects/workspace/page/page_controller';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { Page, pagesActions } from '$app_reducers/pages/slice';
|
||||
import { Log } from '$app/utils/log';
|
||||
import { Page } from '$app_reducers/pages/slice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PageController } from "$app/stores/effects/workspace/page/page_controller";
|
||||
|
||||
export function useLoadExpandedPages() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]);
|
||||
const currentPageId = params.id;
|
||||
const pageMap = useAppSelector((state) => state.pages.pageMap);
|
||||
const [pagePath, setPagePath] = useState<
|
||||
(
|
||||
| Page
|
||||
@ -22,37 +21,39 @@ export function useLoadExpandedPages() {
|
||||
)[]
|
||||
>([]);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (id: string) => {
|
||||
if (!id) return;
|
||||
const controller = new PageController(id);
|
||||
const loadPagePath = useCallback(
|
||||
async (pageId: string) => {
|
||||
let page = pageMap[pageId];
|
||||
const controller = new PageController(pageId);
|
||||
if (!page) {
|
||||
try {
|
||||
page = await controller.getPage();
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
try {
|
||||
const page = await controller.getPage();
|
||||
const childPages = await controller.getChildPages();
|
||||
|
||||
dispatch(pagesActions.addChildPages({ id, childPages }));
|
||||
dispatch(pagesActions.expandPage(id));
|
||||
|
||||
setPagePath((prev) => [page, ...prev]);
|
||||
await loadPage(page.parentId);
|
||||
} catch (e) {
|
||||
Log.info(`${id} is workspace`);
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
setPagePath(prev => {
|
||||
return [
|
||||
page,
|
||||
...prev
|
||||
]
|
||||
});
|
||||
await loadPagePath(page.parentId);
|
||||
|
||||
}, [pageMap]);
|
||||
|
||||
useEffect(() => {
|
||||
setPagePath([]);
|
||||
if (!currentPageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
await loadPage(currentPageId);
|
||||
})();
|
||||
}, [currentPageId, dispatch, loadPage]);
|
||||
loadPagePath(currentPageId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTrash) {
|
||||
|
@ -5,12 +5,11 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { pageTypeMap } from '$app/constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||
|
||||
export function useLoadChildPages(pageId: string) {
|
||||
const dispatch = useAppDispatch();
|
||||
const childPages = useAppSelector((state) => state.pages.relationMap[pageId]);
|
||||
|
||||
const collapsed = useAppSelector((state) => !state.pages.expandedIdMap[pageId]);
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
if (collapsed) {
|
||||
@ -24,31 +23,21 @@ export function useLoadChildPages(pageId: string) {
|
||||
return new PageController(pageId);
|
||||
}, [pageId]);
|
||||
|
||||
const onChildPagesChanged = useCallback(
|
||||
(childPages: Page[]) => {
|
||||
const onPageChanged = useCallback(
|
||||
(page: Page, children: Page[]) => {
|
||||
dispatch(pagesActions.onPageChanged(page));
|
||||
dispatch(
|
||||
pagesActions.addChildPages({
|
||||
id: pageId,
|
||||
childPages,
|
||||
id: page.id,
|
||||
childPages: children,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, pageId]
|
||||
);
|
||||
|
||||
const onPageChanged = useCallback(
|
||||
(page: Page) => {
|
||||
dispatch(pagesActions.onPageChanged(page));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onPageCollapsed = useCallback(async () => {
|
||||
dispatch(pagesActions.removeChildPages(pageId));
|
||||
await controller.unsubscribe();
|
||||
}, [dispatch, pageId, controller]);
|
||||
|
||||
const onPageExpanded = useCallback(async () => {
|
||||
const loadPageChildren = useCallback(async (pageId: string) => {
|
||||
const childPages = await controller.getChildPages();
|
||||
|
||||
dispatch(
|
||||
@ -57,25 +46,22 @@ export function useLoadChildPages(pageId: string) {
|
||||
childPages,
|
||||
})
|
||||
);
|
||||
await controller.subscribe({
|
||||
onChildPagesChanged,
|
||||
|
||||
}, [controller, dispatch]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
void loadPageChildren(pageId);
|
||||
}, [loadPageChildren, pageId]);
|
||||
|
||||
useEffect(() => {
|
||||
controller.subscribe({
|
||||
onPageChanged,
|
||||
});
|
||||
}, [controller, dispatch, onChildPagesChanged, onPageChanged, pageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (collapsed) {
|
||||
onPageCollapsed();
|
||||
} else {
|
||||
onPageExpanded();
|
||||
}
|
||||
}, [collapsed, onPageCollapsed, onPageExpanded]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
controller.dispose();
|
||||
};
|
||||
}, [controller]);
|
||||
}, [controller, onPageChanged]);
|
||||
|
||||
return {
|
||||
toggleCollapsed,
|
||||
@ -86,7 +72,6 @@ export function useLoadChildPages(pageId: string) {
|
||||
|
||||
export function usePageActions(pageId: string) {
|
||||
const page = useAppSelector((state) => state.pages.pageMap[pageId]);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const controller = useMemo(() => {
|
||||
@ -103,7 +88,7 @@ export function usePageActions(pageId: string) {
|
||||
async (layout: ViewLayoutPB) => {
|
||||
const newViewId = await controller.createPage({
|
||||
layout,
|
||||
name: t('document.title.placeholder'),
|
||||
name: ""
|
||||
});
|
||||
|
||||
dispatch(pagesActions.expandPage(pageId));
|
||||
@ -111,7 +96,7 @@ export function usePageActions(pageId: string) {
|
||||
|
||||
navigate(`/page/${pageType}/${newViewId}`);
|
||||
},
|
||||
[t, controller, dispatch, navigate, pageId]
|
||||
[controller, dispatch, navigate, pageId]
|
||||
);
|
||||
|
||||
const onDeletePage = useCallback(async () => {
|
||||
@ -124,12 +109,9 @@ export function usePageActions(pageId: string) {
|
||||
|
||||
const onRenamePage = useCallback(
|
||||
async (name: string) => {
|
||||
await controller.updatePage({
|
||||
id: pageId,
|
||||
name,
|
||||
});
|
||||
await dispatch(updatePageName({ id: pageId, name }));
|
||||
},
|
||||
[controller, pageId]
|
||||
[dispatch, pageId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -152,3 +134,4 @@ export function useSelectedPage(pageId: string) {
|
||||
|
||||
return id === pageId;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import AddButton from './AddButton';
|
||||
import MoreButton from './MoreButton';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { useSelectedPage } from '$app/components/layout/NestedPage/NestedPage.hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function NestedPageTitle({
|
||||
pageId,
|
||||
@ -26,6 +27,7 @@ function NestedPageTitle({
|
||||
onDuplicate: () => Promise<void>;
|
||||
onRename: (newName: string) => Promise<void>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const page = useAppSelector((state) => {
|
||||
return state.pages.pageMap[pageId];
|
||||
});
|
||||
@ -47,7 +49,7 @@ function NestedPageTitle({
|
||||
toggleCollapsed();
|
||||
}}
|
||||
style={{
|
||||
transform: collapsed ? 'rotate(0deg)' : 'rotate(-90deg)',
|
||||
transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
||||
}}
|
||||
className={'flex h-[100%] w-8 items-center justify-center p-2'}
|
||||
>
|
||||
@ -55,7 +57,11 @@ function NestedPageTitle({
|
||||
<ArrowRightSvg />
|
||||
</div>
|
||||
</button>
|
||||
<div className={'flex-1 overflow-hidden text-ellipsis whitespace-nowrap'}>{page.name}</div>
|
||||
{page.icon ? <div className={'mr-1 h-5 w-5'}>{page.icon.value}</div> : null}
|
||||
|
||||
<div className={'flex-1 overflow-hidden text-ellipsis whitespace-nowrap'}>
|
||||
{page.name || t('menuAppHeader.defaultNewPageName')}
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className={'min:w-14 flex items-center justify-end'}>
|
||||
<AddButton isVisible={isHovering} onAddPage={onAddPage} />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
@ -21,6 +21,10 @@ function RenameDialog({
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(defaultValue);
|
||||
setError(false);
|
||||
}, [defaultValue]);
|
||||
return (
|
||||
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('menuAppHeader.renameDialog')}</DialogTitle>
|
||||
@ -37,6 +41,7 @@ function RenameDialog({
|
||||
variant='standard'
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('button.Cancel')}</Button>
|
||||
<Button
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import { TransitionGroup } from 'react-transition-group';
|
||||
import NestedPageTitle from '$app/components/layout/NestedPage/NestedPageTitle';
|
||||
@ -10,6 +10,10 @@ function NestedPage({ pageId }: { pageId: string }) {
|
||||
const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId);
|
||||
const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
|
||||
|
||||
const children = useMemo(() => {
|
||||
return collapsed ? [] : childPages;
|
||||
}, [collapsed, childPages]);
|
||||
|
||||
return (
|
||||
<BlockDraggable id={pageId} type={BlockDraggableType.PAGE} data-page-id={pageId}>
|
||||
<NestedPageTitle
|
||||
@ -27,7 +31,7 @@ function NestedPage({ pageId }: { pageId: string }) {
|
||||
|
||||
<div className={'pl-4 pt-[2px]'}>
|
||||
<TransitionGroup>
|
||||
{childPages?.map((pageId) => (
|
||||
{children?.map((pageId) => (
|
||||
<Collapse key={pageId}>
|
||||
<NestedPage key={pageId} pageId={pageId} />
|
||||
</Collapse>
|
||||
|
@ -16,6 +16,7 @@ function AppearanceSetting({
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const html = document.documentElement;
|
||||
|
||||
html?.setAttribute('data-dark-mode', String(themeMode === ThemeMode.Dark));
|
||||
|
@ -21,7 +21,7 @@ function NewPageButton({ workspaceId }: { workspaceId: string }) {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const { id } = await controller.createView({
|
||||
name: t('document.title.placeholder'),
|
||||
name: "",
|
||||
layout: ViewLayoutPB.Document,
|
||||
parent_view_id: workspaceId,
|
||||
});
|
||||
|
@ -63,17 +63,6 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
||||
return new WorkspaceController(id);
|
||||
}, [id]);
|
||||
|
||||
const onWorkspaceChanged = useCallback(
|
||||
(data: WorkspaceItem) => {
|
||||
dispatch(workspaceActions.onWorkspaceChanged(data));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onWorkspaceDeleted = useCallback(() => {
|
||||
dispatch(workspaceActions.onWorkspaceDeleted(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const openWorkspace = useCallback(async () => {
|
||||
await controller.open();
|
||||
}, [controller]);
|
||||
@ -96,7 +85,6 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
||||
|
||||
const initializeWorkspace = useCallback(async () => {
|
||||
const childPages = await controller.getChildPages();
|
||||
|
||||
dispatch(
|
||||
pagesActions.addChildPages({
|
||||
id,
|
||||
@ -107,11 +95,9 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
||||
|
||||
const subscribeToWorkspace = useCallback(async () => {
|
||||
await controller.subscribe({
|
||||
onWorkspaceChanged,
|
||||
onWorkspaceDeleted,
|
||||
onChildPagesChanged,
|
||||
});
|
||||
}, [controller, onChildPagesChanged, onWorkspaceChanged, onWorkspaceDeleted]);
|
||||
}, [controller, onChildPagesChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
|
@ -82,10 +82,13 @@ export interface ImageBlockData {
|
||||
align: Align;
|
||||
}
|
||||
|
||||
export enum CoverType {
|
||||
Image = 'image',
|
||||
Color = 'color',
|
||||
}
|
||||
export interface PageBlockData extends TextBlockData {
|
||||
cover?: string;
|
||||
icon?: string;
|
||||
coverType?: 'image' | 'color';
|
||||
coverType?: CoverType;
|
||||
}
|
||||
|
||||
export type BlockData<Type> = Type extends BlockType.HeadingBlock
|
||||
@ -303,10 +306,6 @@ export interface EditorProps {
|
||||
value?: Delta;
|
||||
selection?: RangeStaticNoId;
|
||||
decorateSelection?: RangeStaticNoId;
|
||||
linkDecorateSelection?: {
|
||||
selection?: RangeStaticNoId;
|
||||
placeholder?: string;
|
||||
};
|
||||
temporarySelection?: RangeStaticNoId;
|
||||
onSelectionChange?: (range: RangeStaticNoId | null, oldRange: RangeStaticNoId | null, source?: Sources) => void;
|
||||
onChange?: (delta: Delta, oldDelta: Delta, source?: Sources) => void;
|
||||
@ -319,15 +318,6 @@ export interface BlockCopyData {
|
||||
html: string;
|
||||
}
|
||||
|
||||
export interface LinkPopoverState {
|
||||
anchorPosition?: { top: number; left: number };
|
||||
id?: string;
|
||||
selection?: RangeStaticNoId;
|
||||
open?: boolean;
|
||||
href?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface TemporaryState {
|
||||
id: string;
|
||||
type: TemporaryType;
|
||||
@ -339,10 +329,11 @@ export interface TemporaryState {
|
||||
|
||||
export enum TemporaryType {
|
||||
Equation = 'equation',
|
||||
Link = 'link',
|
||||
}
|
||||
|
||||
export type TemporaryData = InlineEquationData;
|
||||
|
||||
export interface InlineEquationData {
|
||||
latex: string;
|
||||
export interface TemporaryData {
|
||||
latex?: string;
|
||||
href?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
|
||||
import { AppearanceSettingsPB, ThemeModePB } from '@/services/backend';
|
||||
import { AppearanceSettingsPB } from '@/services/backend';
|
||||
import { Theme, ThemeMode, UserSetting } from '$app/interfaces';
|
||||
|
||||
export class UserSettingController {
|
||||
private readonly backendService: UserBackendService;
|
||||
@ -17,11 +18,25 @@ export class UserSettingController {
|
||||
return {};
|
||||
};
|
||||
|
||||
getAppearanceSetting = async (): Promise<AppearanceSettingsPB | undefined> => {
|
||||
getAppearanceSetting = async (): Promise<Partial<UserSetting> | undefined> => {
|
||||
const appearanceSetting = await this.backendService.getAppearanceSettings();
|
||||
|
||||
if (appearanceSetting.ok) {
|
||||
return appearanceSetting.val;
|
||||
const res = appearanceSetting.val;
|
||||
const { locale, theme = Theme.Default, theme_mode = ThemeMode.Light } = res;
|
||||
let language = 'en';
|
||||
|
||||
if (locale.language_code && locale.country_code) {
|
||||
language = `${locale.language_code}-${locale.country_code}`;
|
||||
} else if (locale.language_code) {
|
||||
language = locale.language_code;
|
||||
}
|
||||
|
||||
return {
|
||||
themeMode: theme_mode,
|
||||
theme: theme as Theme,
|
||||
language: language,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -14,8 +14,11 @@ import {
|
||||
ImportPB,
|
||||
MoveNestedViewPayloadPB,
|
||||
FolderEventMoveNestedView,
|
||||
ViewIconPB,
|
||||
UpdateViewIconPayloadPB,
|
||||
FolderEventUpdateViewIcon,
|
||||
} from '@/services/backend/events/flowy-folder2';
|
||||
import { Page } from '$app_reducers/pages/slice';
|
||||
import { Page, PageIcon } from '$app_reducers/pages/slice';
|
||||
|
||||
export class PageBackendService {
|
||||
constructor() {
|
||||
@ -54,17 +57,23 @@ export class PageBackendService {
|
||||
payload.name = page.name;
|
||||
}
|
||||
|
||||
if (page.cover !== undefined) {
|
||||
payload.cover_url = page.cover;
|
||||
}
|
||||
|
||||
if (page.icon !== undefined) {
|
||||
payload.icon_url = page.icon;
|
||||
}
|
||||
|
||||
return FolderEventUpdateView(payload);
|
||||
};
|
||||
|
||||
updatePageIcon = async (viewId: string, icon?: PageIcon) => {
|
||||
const payload = new UpdateViewIconPayloadPB({
|
||||
view_id: viewId,
|
||||
icon: icon
|
||||
? new ViewIconPB({
|
||||
ty: icon.ty,
|
||||
value: icon.value,
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return FolderEventUpdateViewIcon(payload);
|
||||
};
|
||||
|
||||
deletePage = async (viewId: string) => {
|
||||
const payload = new RepeatedViewIdPB({
|
||||
items: [viewId],
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||
import { PageBackendService } from '$app/stores/effects/workspace/page/page_bd_svc';
|
||||
import { WorkspaceObserver } from '$app/stores/effects/workspace/workspace_observer';
|
||||
import { Page, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||
import { AsyncQueue } from '$app/utils/async_queue';
|
||||
import { Page, PageIcon, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||
|
||||
export class PageController {
|
||||
private readonly backendService: PageBackendService = new PageBackendService();
|
||||
|
||||
private readonly observer: WorkspaceObserver = new WorkspaceObserver();
|
||||
private onChangeQueue?: AsyncQueue;
|
||||
constructor(private readonly id: string) {
|
||||
//
|
||||
}
|
||||
@ -72,22 +70,15 @@ export class PageController {
|
||||
return this.getPage(parentPageId);
|
||||
};
|
||||
|
||||
subscribe = async (callbacks: {
|
||||
onChildPagesChanged?: (childPages: Page[]) => void;
|
||||
onPageChanged?: (page: Page) => void;
|
||||
}) => {
|
||||
const onChanged = async () => {
|
||||
const page = await this.getPage();
|
||||
const childPages = await this.getChildPages();
|
||||
|
||||
callbacks.onPageChanged?.(page);
|
||||
callbacks.onChildPagesChanged?.(childPages);
|
||||
subscribe = async (callbacks: { onPageChanged?: (page: Page, children: Page[]) => void }) => {
|
||||
const didUpdateView = (payload: Uint8Array) => {
|
||||
const res = ViewPB.deserializeBinary(payload);
|
||||
const page = parserViewPBToPage(ViewPB.deserializeBinary(payload));
|
||||
const childPages = res.child_views.map(parserViewPBToPage);
|
||||
callbacks.onPageChanged?.(page, childPages);
|
||||
};
|
||||
|
||||
this.onChangeQueue = new AsyncQueue(onChanged);
|
||||
await this.observer.subscribeView(this.id, {
|
||||
didUpdateChildViews: this.didUpdateChildPages,
|
||||
didUpdateView: this.didUpdateView,
|
||||
didUpdateView,
|
||||
});
|
||||
};
|
||||
|
||||
@ -105,6 +96,16 @@ export class PageController {
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
updatePageIcon = async (icon?: PageIcon) => {
|
||||
const result = await this.backendService.updatePageIcon(this.id, icon);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
deletePage = async () => {
|
||||
const result = await this.backendService.deletePage(this.id);
|
||||
|
||||
@ -125,12 +126,4 @@ export class PageController {
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
private didUpdateChildPages = (payload: Uint8Array) => {
|
||||
this.onChangeQueue?.enqueue(Math.random());
|
||||
};
|
||||
|
||||
private didUpdateView = (payload: Uint8Array) => {
|
||||
this.onChangeQueue?.enqueue(Math.random());
|
||||
};
|
||||
}
|
||||
|
@ -1,18 +1,13 @@
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/workspace/workspace_bd_svc';
|
||||
import { WorkspaceObserver } from '$app/stores/effects/workspace/workspace_observer';
|
||||
import { CreateViewPayloadPB } from '@/services/backend';
|
||||
import { WorkspaceItem } from '$app_reducers/workspace/slice';
|
||||
import { CreateViewPayloadPB, RepeatedViewPB } from "@/services/backend";
|
||||
import { PageBackendService } from '$app/stores/effects/workspace/page/page_bd_svc';
|
||||
import { Page, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||
import { AsyncQueue } from '$app/utils/async_queue';
|
||||
|
||||
export class WorkspaceController {
|
||||
private readonly observer: WorkspaceObserver = new WorkspaceObserver();
|
||||
private readonly pageBackendService: PageBackendService;
|
||||
private readonly backendService: WorkspaceBackendService;
|
||||
private onWorkspaceChanged?: (data: WorkspaceItem) => void;
|
||||
private onWorkspaceDeleted?: () => void;
|
||||
private onChangeQueue?: AsyncQueue;
|
||||
constructor(private readonly workspaceId: string) {
|
||||
this.pageBackendService = new PageBackendService();
|
||||
this.backendService = new WorkspaceBackendService();
|
||||
@ -43,23 +38,15 @@ export class WorkspaceController {
|
||||
};
|
||||
|
||||
subscribe = async (callbacks: {
|
||||
onWorkspaceChanged?: (data: WorkspaceItem) => void;
|
||||
onWorkspaceDeleted?: () => void;
|
||||
onChildPagesChanged?: (childPages: Page[]) => void;
|
||||
}) => {
|
||||
this.onWorkspaceChanged = callbacks.onWorkspaceChanged;
|
||||
this.onWorkspaceDeleted = callbacks.onWorkspaceDeleted;
|
||||
const onChildPagesChanged = async () => {
|
||||
const childPages = await this.getChildPages();
|
||||
|
||||
callbacks.onChildPagesChanged?.(childPages);
|
||||
};
|
||||
|
||||
this.onChangeQueue = new AsyncQueue(onChildPagesChanged);
|
||||
const didUpdateWorkspace = (payload: Uint8Array) => {
|
||||
const res = RepeatedViewPB.deserializeBinary(payload).items;
|
||||
callbacks.onChildPagesChanged?.(res.map(parserViewPBToPage));
|
||||
}
|
||||
await this.observer.subscribeWorkspace(this.workspaceId, {
|
||||
didUpdateWorkspace: this.didUpdateWorkspace,
|
||||
didDeleteWorkspace: this.didDeleteWorkspace,
|
||||
didUpdateChildViews: this.didUpdateChildPages,
|
||||
didUpdateWorkspace
|
||||
});
|
||||
};
|
||||
|
||||
@ -85,15 +72,5 @@ export class WorkspaceController {
|
||||
return [];
|
||||
};
|
||||
|
||||
private didUpdateWorkspace = (payload: Uint8Array) => {
|
||||
// this.onWorkspaceChanged?.(payload.toObject());
|
||||
};
|
||||
|
||||
private didDeleteWorkspace = (payload: Uint8Array) => {
|
||||
this.onWorkspaceDeleted?.();
|
||||
};
|
||||
|
||||
private didUpdateChildPages = (payload: Uint8Array) => {
|
||||
this.onChangeQueue?.enqueue(Math.random());
|
||||
};
|
||||
}
|
||||
|
@ -26,22 +26,22 @@ export class WorkspaceObserver {
|
||||
subscribeWorkspace = async (
|
||||
workspaceId: string,
|
||||
callbacks: {
|
||||
didUpdateChildViews: (payload: Uint8Array) => void;
|
||||
didUpdateWorkspace: (payload: Uint8Array) => void;
|
||||
didDeleteWorkspace: (payload: Uint8Array) => void;
|
||||
didUpdateChildViews?: (payload: Uint8Array) => void;
|
||||
didUpdateWorkspace?: (payload: Uint8Array) => void;
|
||||
didDeleteWorkspace?: (payload: Uint8Array) => void;
|
||||
}
|
||||
) => {
|
||||
this.listener = new WorkspaceNotificationObserver({
|
||||
id: workspaceId,
|
||||
parserHandler: (notification, result) => {
|
||||
switch (notification) {
|
||||
case FolderNotification.DidUpdateWorkspace:
|
||||
case FolderNotification.DidUpdateWorkspaceViews:
|
||||
if (!result.ok) break;
|
||||
callbacks.didUpdateWorkspace(result.val);
|
||||
callbacks.didUpdateWorkspace?.(result.val);
|
||||
break;
|
||||
case FolderNotification.DidUpdateChildViews:
|
||||
if (!result.ok) break;
|
||||
callbacks.didUpdateChildViews(result.val);
|
||||
callbacks.didUpdateChildViews?.(result.val);
|
||||
break;
|
||||
// case FolderNotification.DidDeleteWorkspace:
|
||||
// if (!result.ok) break;
|
||||
@ -58,8 +58,8 @@ export class WorkspaceObserver {
|
||||
subscribeView = async (
|
||||
viewId: string,
|
||||
callbacks: {
|
||||
didUpdateChildViews: (payload: Uint8Array) => void;
|
||||
didUpdateView: (payload: Uint8Array) => void;
|
||||
didUpdateChildViews?: (payload: Uint8Array) => void;
|
||||
didUpdateView?: (payload: Uint8Array) => void;
|
||||
}
|
||||
) => {
|
||||
this.listener = new WorkspaceNotificationObserver({
|
||||
@ -68,11 +68,11 @@ export class WorkspaceObserver {
|
||||
switch (notification) {
|
||||
case FolderNotification.DidUpdateChildViews:
|
||||
if (!result.ok) break;
|
||||
callbacks.didUpdateChildViews(result.val);
|
||||
callbacks.didUpdateChildViews?.(result.val);
|
||||
break;
|
||||
case FolderNotification.DidUpdateView:
|
||||
if (!result.ok) break;
|
||||
callbacks.didUpdateView(result.val);
|
||||
callbacks.didUpdateView?.(result.val);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -4,17 +4,33 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import Delta, { Op } from 'quill-delta';
|
||||
import { RootState } from '$app/stores/store';
|
||||
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||
import { getDeltaText } from '$app/utils/document/delta';
|
||||
|
||||
export const updateNodeDeltaThunk = createAsyncThunk(
|
||||
'document/updateNodeDelta',
|
||||
async (payload: { id: string; delta: Op[]; controller: DocumentController }, thunkAPI) => {
|
||||
const { id, delta, controller } = payload;
|
||||
const { getState } = thunkAPI;
|
||||
const { getState, dispatch } = thunkAPI;
|
||||
const state = getState() as RootState;
|
||||
const docId = controller.documentId;
|
||||
const docState = state[DOCUMENT_NAME][docId];
|
||||
const node = docState.nodes[id];
|
||||
const diffDelta = new Delta(delta).diff(new Delta(node.data.delta || []));
|
||||
const oldDelta = new Delta(node.data.delta);
|
||||
const newDelta = new Delta(delta);
|
||||
|
||||
// If the node is the root node, update the page name
|
||||
if (!node.parent) {
|
||||
await dispatch(
|
||||
updatePageName({
|
||||
id: docId,
|
||||
name: getDeltaText(newDelta),
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const diffDelta = newDelta.diff(oldDelta);
|
||||
|
||||
if (diffDelta.ops.length === 0) return;
|
||||
|
||||
|
@ -2,4 +2,3 @@ export * from './blocks';
|
||||
export * from './turn_to';
|
||||
export * from './keydown';
|
||||
export * from './range';
|
||||
export * from './link';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import { BlockType, RangeStatic, SplitRelationship } from '$app/interfaces/document';
|
||||
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/turn_to';
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { DocumentController } from "$app/stores/effects/document/document_controller";
|
||||
import { BlockType, RangeStatic, SplitRelationship } from "$app/interfaces/document";
|
||||
import { turnToTextBlockThunk } from "$app_reducers/document/async-actions/turn_to";
|
||||
import {
|
||||
findNextHasDeltaNode,
|
||||
findPrevHasDeltaNode,
|
||||
@ -9,23 +9,27 @@ import {
|
||||
getLeftCaretByRange,
|
||||
getRightCaretByRange,
|
||||
transformToNextLineCaret,
|
||||
transformToPrevLineCaret,
|
||||
} from '$app/utils/document/action';
|
||||
import Delta from 'quill-delta';
|
||||
import { indentNodeThunk, mergeDeltaThunk, outdentNodeThunk } from '$app_reducers/document/async-actions/blocks';
|
||||
import { rangeActions } from '$app_reducers/document/slice';
|
||||
import { RootState } from '$app/stores/store';
|
||||
import { blockConfig } from '$app/constants/document/config';
|
||||
import { Keyboard } from '$app/constants/document/keyboard';
|
||||
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||
import { getPreviousWordIndex } from '$app/utils/document/delta';
|
||||
transformToPrevLineCaret
|
||||
} from "$app/utils/document/action";
|
||||
import Delta from "quill-delta";
|
||||
import { indentNodeThunk, mergeDeltaThunk, outdentNodeThunk } from "$app_reducers/document/async-actions/blocks";
|
||||
import { rangeActions } from "$app_reducers/document/slice";
|
||||
import { RootState } from "$app/stores/store";
|
||||
import { blockConfig } from "$app/constants/document/config";
|
||||
import { Keyboard } from "$app/constants/document/keyboard";
|
||||
import { DOCUMENT_NAME, RANGE_NAME } from "$app/constants/document/name";
|
||||
import { getDeltaText, getPreviousWordIndex } from "$app/utils/document/delta";
|
||||
import { updatePageName } from "$app_reducers/pages/async_actions";
|
||||
import { newBlock } from "$app/utils/document/block";
|
||||
|
||||
|
||||
/**
|
||||
* Delete a block by backspace or delete key
|
||||
* 1. If the block is not a text block, turn it to a text block
|
||||
* 2. If the block is a text block
|
||||
* 2.1 If the block has next node or is top level, merge it to the previous line
|
||||
* 2.2 If the block has no next node and is not top level, outdent it
|
||||
- Deletes a block using the backspace or delete key.
|
||||
- If the block is not a text block, it is converted into a text block.
|
||||
- If the block is a text block:
|
||||
- - If the block is the first line, it is merged into the document title, and a new line is inserted.
|
||||
- - If the block is not the first line and it has a next sibling, it is merged into the previous line (including the previous sibling and its parent).
|
||||
- - If the block has no next sibling and is not a top-level block, it is outdented (moved to a higher level in the hierarchy).
|
||||
*/
|
||||
export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
|
||||
'document/backspaceDeleteActionForBlock',
|
||||
@ -49,11 +53,43 @@ export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
|
||||
}
|
||||
|
||||
const isTopLevel = parent.type === BlockType.PageBlock;
|
||||
const isFirstLine = isTopLevel && index === 0;
|
||||
|
||||
if (isTopLevel && isFirstLine) {
|
||||
// merge to document title and insert a new line
|
||||
const parentDelta = new Delta(parent.data.delta);
|
||||
const caretIndex = parentDelta.length();
|
||||
const caret = {
|
||||
id: parent.id,
|
||||
index: caretIndex,
|
||||
length: 0,
|
||||
};
|
||||
const titleDelta = parentDelta.concat(new Delta(node.data.delta));
|
||||
await dispatch(updatePageName({ id: docId, name: getDeltaText(titleDelta) }));
|
||||
const actions = [
|
||||
controller.getDeleteAction(node),
|
||||
]
|
||||
|
||||
if (!nextNodeId) {
|
||||
// insert a new line
|
||||
const block = newBlock<any>(BlockType.TextBlock, parent.id, {
|
||||
delta: [{ insert: "" }]
|
||||
});
|
||||
actions.push(controller.getInsertAction(block, null));
|
||||
}
|
||||
await controller.applyActions(actions);
|
||||
dispatch(rangeActions.initialState(docId));
|
||||
dispatch(
|
||||
rangeActions.setCaret({
|
||||
docId,
|
||||
caret,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isTopLevel || nextNodeId) {
|
||||
// merge to previous line
|
||||
const prevLine = findPrevHasDeltaNode(state, id);
|
||||
|
||||
if (!prevLine) return;
|
||||
const caretIndex = new Delta(prevLine.data.delta).length();
|
||||
const caret = {
|
||||
@ -104,19 +140,49 @@ export const enterActionForBlockThunk = createAsyncThunk(
|
||||
if (!node || !caret || caret.id !== id) return;
|
||||
const delta = new Delta(node.data.delta);
|
||||
|
||||
const nodeDelta = delta.slice(0, caret.index);
|
||||
|
||||
const insertNodeDelta = new Delta(node.data.delta).slice(caret.index + caret.length);
|
||||
|
||||
const isDocumentTitle = !node.parent;
|
||||
// update page title and insert a new line
|
||||
if (isDocumentTitle) {
|
||||
// update page title
|
||||
await dispatch(updatePageName({
|
||||
id: docId,
|
||||
name: getDeltaText(nodeDelta),
|
||||
}));
|
||||
// insert a new line
|
||||
const block = newBlock<any>(BlockType.TextBlock, node.id, {
|
||||
delta: insertNodeDelta.ops,
|
||||
});
|
||||
const insertNodeAction = controller.getInsertAction(block, null);
|
||||
await controller.applyActions([insertNodeAction]);
|
||||
dispatch(rangeActions.initialState(docId));
|
||||
dispatch(
|
||||
rangeActions.setCaret({
|
||||
docId,
|
||||
caret: {
|
||||
id: block.id,
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (delta.length() === 0 && node.type !== BlockType.TextBlock) {
|
||||
// If the node is not a text block, turn it to a text block
|
||||
await dispatch(turnToTextBlockThunk({ id, controller }));
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeDelta = delta.slice(0, caret.index);
|
||||
|
||||
const insertNodeDelta = new Delta(node.data.delta).slice(caret.index + caret.length);
|
||||
|
||||
const insertNodeAction = getInsertEnterNodeAction(node, insertNodeDelta, controller);
|
||||
|
||||
if (!insertNodeAction) return;
|
||||
|
||||
const updateNode = {
|
||||
...node,
|
||||
data: {
|
||||
|
@ -1,103 +0,0 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||
import Delta from 'quill-delta';
|
||||
import { linkPopoverActions, rangeActions } from '$app_reducers/document/slice';
|
||||
import { RootState } from '$app/stores/store';
|
||||
import { DOCUMENT_NAME, RANGE_NAME, TEXT_LINK_NAME } from '$app/constants/document/name';
|
||||
|
||||
export const formatLinkThunk = createAsyncThunk<
|
||||
boolean,
|
||||
{
|
||||
controller: DocumentController;
|
||||
}
|
||||
>('document/formatLink', async (payload, thunkAPI) => {
|
||||
const { controller } = payload;
|
||||
const { getState } = thunkAPI;
|
||||
const docId = controller.documentId;
|
||||
const state = getState() as RootState;
|
||||
const documentState = state[DOCUMENT_NAME][docId];
|
||||
const linkPopover = state[TEXT_LINK_NAME][docId];
|
||||
|
||||
if (!linkPopover) return false;
|
||||
const { selection, id, href, title = '' } = linkPopover;
|
||||
|
||||
if (!selection || !id) return false;
|
||||
const node = documentState.nodes[id];
|
||||
const nodeDelta = new Delta(node.data?.delta);
|
||||
const index = selection.index || 0;
|
||||
const length = selection.length || 0;
|
||||
const regex = new RegExp(/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/);
|
||||
|
||||
if (href && !regex.test(href)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const diffDelta = new Delta().retain(index).delete(length).insert(title, {
|
||||
href,
|
||||
});
|
||||
|
||||
const newDelta = nodeDelta.compose(diffDelta);
|
||||
|
||||
const updateAction = controller.getUpdateAction({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
delta: newDelta.ops,
|
||||
},
|
||||
});
|
||||
|
||||
await controller.applyActions([updateAction]);
|
||||
return true;
|
||||
});
|
||||
|
||||
export const newLinkThunk = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
docId: string;
|
||||
}
|
||||
>('document/newLink', async ({ docId }, thunkAPI) => {
|
||||
const { getState, dispatch } = thunkAPI;
|
||||
const state = getState() as RootState;
|
||||
const documentState = state[DOCUMENT_NAME][docId];
|
||||
const documentRange = state[RANGE_NAME][docId];
|
||||
|
||||
const { caret } = documentRange;
|
||||
|
||||
if (!caret) return;
|
||||
const { index, length, id } = caret;
|
||||
|
||||
const block = documentState.nodes[id];
|
||||
const delta = new Delta(block.data.delta).slice(index, index + length);
|
||||
const op = delta.ops.find((op) => op.attributes?.href);
|
||||
const href = op?.attributes?.href as string;
|
||||
|
||||
const domSelection = window.getSelection();
|
||||
|
||||
if (!domSelection) return;
|
||||
const domRange = domSelection.rangeCount > 0 ? domSelection.getRangeAt(0) : null;
|
||||
|
||||
if (!domRange) return;
|
||||
const title = domSelection.toString();
|
||||
const { top, left, height, width } = domRange.getBoundingClientRect();
|
||||
|
||||
dispatch(rangeActions.initialState(docId));
|
||||
dispatch(
|
||||
linkPopoverActions.setLinkPopover({
|
||||
docId,
|
||||
linkState: {
|
||||
anchorPosition: {
|
||||
top: top + height,
|
||||
left: left + width / 2,
|
||||
},
|
||||
id,
|
||||
selection: {
|
||||
index,
|
||||
length,
|
||||
},
|
||||
title,
|
||||
href,
|
||||
open: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
@ -33,14 +33,13 @@ export const createTemporary = createAsyncThunk(
|
||||
const rangeDelta = getDeltaByRange(nodeDelta, selection);
|
||||
const text = getDeltaText(rangeDelta);
|
||||
|
||||
const data = newDataWithTemporaryType(type, text);
|
||||
temporaryState = {
|
||||
id,
|
||||
selection,
|
||||
selectedText: text,
|
||||
type,
|
||||
data: {
|
||||
latex: text,
|
||||
},
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@ -51,6 +50,22 @@ export const createTemporary = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
function newDataWithTemporaryType(type: TemporaryType, text: string) {
|
||||
switch (type) {
|
||||
case TemporaryType.Equation:
|
||||
return {
|
||||
latex: text,
|
||||
};
|
||||
case TemporaryType.Link:
|
||||
return {
|
||||
href: '',
|
||||
text: text,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export const formatTemporary = createAsyncThunk(
|
||||
'document/temporary/format',
|
||||
async (payload: { controller: DocumentController }, thunkAPI) => {
|
||||
@ -69,7 +84,7 @@ export const formatTemporary = createAsyncThunk(
|
||||
const nodeDelta = new Delta(node.data?.delta);
|
||||
const { index, length } = selection;
|
||||
const diffDelta: Delta = new Delta();
|
||||
let newSelection;
|
||||
let newSelection = selection;
|
||||
|
||||
switch (type) {
|
||||
case TemporaryType.Equation: {
|
||||
@ -91,6 +106,21 @@ export const formatTemporary = createAsyncThunk(
|
||||
|
||||
break;
|
||||
}
|
||||
case TemporaryType.Link: {
|
||||
if (!data.text) return;
|
||||
if (!data.href) {
|
||||
diffDelta.retain(index).delete(length).insert(data.text);
|
||||
} else {
|
||||
diffDelta.retain(index).delete(length).insert(data.text, {
|
||||
href: data.href,
|
||||
});
|
||||
}
|
||||
newSelection = {
|
||||
index: selection.index,
|
||||
length: data.text.length,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
|
@ -5,21 +5,15 @@ import {
|
||||
SlashCommandState,
|
||||
RangeState,
|
||||
RangeStatic,
|
||||
LinkPopoverState,
|
||||
SlashCommandOption,
|
||||
} from '@/appflowy_app/interfaces/document';
|
||||
import { BlockEventPayloadPB } from '@/services/backend';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { parseValue, matchChange } from '$app/utils/document/subscribe';
|
||||
import { temporarySlice } from '$app_reducers/document/temporary_slice';
|
||||
import {
|
||||
DOCUMENT_NAME,
|
||||
RANGE_NAME,
|
||||
RECT_RANGE_NAME,
|
||||
SLASH_COMMAND_NAME,
|
||||
TEXT_LINK_NAME,
|
||||
} from '$app/constants/document/name';
|
||||
import { DOCUMENT_NAME, RANGE_NAME, RECT_RANGE_NAME, SLASH_COMMAND_NAME } from '$app/constants/document/name';
|
||||
import { blockEditSlice } from '$app_reducers/document/block_edit_slice';
|
||||
import { Op } from 'quill-delta';
|
||||
|
||||
const initialState: Record<string, DocumentState> = {};
|
||||
|
||||
@ -29,8 +23,6 @@ const rangeInitialState: Record<string, RangeState> = {};
|
||||
|
||||
const slashCommandInitialState: Record<string, SlashCommandState> = {};
|
||||
|
||||
const linkPopoverState: Record<string, LinkPopoverState> = {};
|
||||
|
||||
export const documentSlice = createSlice({
|
||||
name: DOCUMENT_NAME,
|
||||
initialState: initialState,
|
||||
@ -68,6 +60,22 @@ export const documentSlice = createSlice({
|
||||
children,
|
||||
};
|
||||
},
|
||||
|
||||
updateRootNodeDelta: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
docId: string;
|
||||
rootId: string;
|
||||
delta: Op[];
|
||||
}>
|
||||
) => {
|
||||
const { docId, delta, rootId } = action.payload;
|
||||
const documentState = state[docId];
|
||||
if (!documentState) return;
|
||||
const rootNode = documentState.nodes[rootId];
|
||||
if (!rootNode) return;
|
||||
rootNode.data.delta = delta;
|
||||
},
|
||||
/**
|
||||
This function listens for changes in the data layer triggered by the data API,
|
||||
and updates the UI state accordingly.
|
||||
@ -371,63 +379,11 @@ export const slashCommandSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const linkPopoverSlice = createSlice({
|
||||
name: TEXT_LINK_NAME,
|
||||
initialState: linkPopoverState,
|
||||
reducers: {
|
||||
initialState: (state, action: PayloadAction<string>) => {
|
||||
const docId = action.payload;
|
||||
|
||||
state[docId] = {
|
||||
open: false,
|
||||
};
|
||||
},
|
||||
clear: (state, action: PayloadAction<string>) => {
|
||||
const docId = action.payload;
|
||||
|
||||
delete state[docId];
|
||||
},
|
||||
setLinkPopover: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
docId: string;
|
||||
linkState: LinkPopoverState;
|
||||
}>
|
||||
) => {
|
||||
const { docId, linkState } = action.payload;
|
||||
|
||||
state[docId] = linkState;
|
||||
},
|
||||
updateLinkPopover: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
docId: string;
|
||||
linkState: LinkPopoverState;
|
||||
}>
|
||||
) => {
|
||||
const { docId, linkState } = action.payload;
|
||||
const { id } = linkState;
|
||||
|
||||
if (!state[docId].open || state[docId].id !== id) return;
|
||||
state[docId] = {
|
||||
...state[docId],
|
||||
...linkState,
|
||||
};
|
||||
},
|
||||
closeLinkPopover: (state, action: PayloadAction<string>) => {
|
||||
const docId = action.payload;
|
||||
|
||||
state[docId].open = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const documentReducers = {
|
||||
[documentSlice.name]: documentSlice.reducer,
|
||||
[rectSelectionSlice.name]: rectSelectionSlice.reducer,
|
||||
[rangeSlice.name]: rangeSlice.reducer,
|
||||
[slashCommandSlice.name]: slashCommandSlice.reducer,
|
||||
[linkPopoverSlice.name]: linkPopoverSlice.reducer,
|
||||
[temporarySlice.name]: temporarySlice.reducer,
|
||||
[blockEditSlice.name]: blockEditSlice.reducer,
|
||||
};
|
||||
@ -436,4 +392,3 @@ export const documentActions = documentSlice.actions;
|
||||
export const rectSelectionActions = rectSelectionSlice.actions;
|
||||
export const rangeActions = rangeSlice.actions;
|
||||
export const slashCommandActions = slashCommandSlice.actions;
|
||||
export const linkPopoverActions = linkPopoverSlice.actions;
|
||||
|
@ -2,6 +2,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { RootState } from '$app/stores/store';
|
||||
import { DragInsertType } from '$app_reducers/block-draggable/slice';
|
||||
import { PageController } from '$app/stores/effects/workspace/page/page_controller';
|
||||
import { PageIcon } from '$app_reducers/pages/slice';
|
||||
|
||||
export const movePageThunk = createAsyncThunk(
|
||||
'pages/movePage',
|
||||
@ -56,3 +57,36 @@ export const movePageThunk = createAsyncThunk(
|
||||
await controller.movePage({ parentId, prevId });
|
||||
}
|
||||
);
|
||||
|
||||
export const updatePageName = createAsyncThunk(
|
||||
'pages/updateName',
|
||||
async (
|
||||
payload: {
|
||||
id: string;
|
||||
name: string;
|
||||
},
|
||||
thunkAPI
|
||||
) => {
|
||||
const controller = new PageController(payload.id);
|
||||
|
||||
await controller.updatePage({
|
||||
id: payload.id,
|
||||
name: payload.name,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const updatePageIcon = createAsyncThunk(
|
||||
'pages/updateIcon',
|
||||
async (
|
||||
payload: {
|
||||
id: string;
|
||||
icon?: PageIcon;
|
||||
},
|
||||
thunkAPI
|
||||
) => {
|
||||
const controller = new PageController(payload.id);
|
||||
|
||||
await controller.updatePageIcon(payload.icon);
|
||||
}
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||
import { ViewIconTypePB, ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export interface Page {
|
||||
@ -6,18 +6,28 @@ export interface Page {
|
||||
parentId: string;
|
||||
name: string;
|
||||
layout: ViewLayoutPB;
|
||||
icon?: string;
|
||||
cover?: string;
|
||||
icon?: PageIcon;
|
||||
}
|
||||
|
||||
export function parserViewPBToPage(view: ViewPB) {
|
||||
export interface PageIcon {
|
||||
ty: ViewIconTypePB;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function parserViewPBToPage(view: ViewPB): Page {
|
||||
const icon = view.icon;
|
||||
|
||||
return {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
parentId: view.parent_view_id,
|
||||
layout: view.layout,
|
||||
cover: view.cover_url,
|
||||
icon: view.icon_url,
|
||||
icon: icon
|
||||
? {
|
||||
ty: icon.ty,
|
||||
value: icon.value,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -30,7 +40,10 @@ export interface PageState {
|
||||
export const initialState: PageState = {
|
||||
pageMap: {},
|
||||
relationMap: {},
|
||||
expandedIdMap: {},
|
||||
expandedIdMap: getExpandedPageIds().reduce((acc, id) => {
|
||||
acc[id] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>),
|
||||
};
|
||||
|
||||
export const pagesSlice = createSlice({
|
||||
@ -75,16 +88,29 @@ export const pagesSlice = createSlice({
|
||||
|
||||
expandPage(state, action: PayloadAction<string>) {
|
||||
const id = action.payload;
|
||||
|
||||
state.expandedIdMap[id] = true;
|
||||
const ids = Object.keys(state.expandedIdMap).filter(id => state.expandedIdMap[id]);
|
||||
storeExpandedPageIds(ids);
|
||||
},
|
||||
|
||||
collapsePage(state, action: PayloadAction<string>) {
|
||||
const id = action.payload;
|
||||
|
||||
state.expandedIdMap[id] = false;
|
||||
const ids = Object.keys(state.expandedIdMap).filter(id => state.expandedIdMap[id]);
|
||||
storeExpandedPageIds(ids);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const pagesActions = pagesSlice.actions;
|
||||
|
||||
function storeExpandedPageIds(expandedPageIds: string[]) {
|
||||
localStorage.setItem('expandedPageIds', JSON.stringify(expandedPageIds));
|
||||
}
|
||||
|
||||
function getExpandedPageIds(): string[] {
|
||||
const expandedPageIds = localStorage.getItem('expandedPageIds');
|
||||
|
||||
return expandedPageIds ? JSON.parse(expandedPageIds) : [];
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import emojiData, { EmojiMartData } from '@emoji-mart/data';
|
||||
|
||||
export const randomEmoji = () => {
|
||||
export const randomEmoji = (skin = 0) => {
|
||||
const emojis = (emojiData as EmojiMartData).emojis;
|
||||
const keys = Object.keys(emojis);
|
||||
const randomKey = keys[Math.floor(Math.random() * keys.length)];
|
||||
|
||||
return emojis[randomKey].skins[0].native;
|
||||
return emojis[randomKey].skins[skin].native;
|
||||
};
|
||||
|
@ -6,7 +6,6 @@ import { useAppDispatch } from '../stores/store';
|
||||
import { Log } from '../utils/log';
|
||||
import {
|
||||
documentActions,
|
||||
linkPopoverActions,
|
||||
rangeActions,
|
||||
rectSelectionActions,
|
||||
slashCommandActions,
|
||||
@ -34,7 +33,6 @@ export const useDocument = () => {
|
||||
dispatch(rangeActions.initialState(docId));
|
||||
dispatch(rectSelectionActions.initialState(docId));
|
||||
dispatch(slashCommandActions.initialState(docId));
|
||||
dispatch(linkPopoverActions.initialState(docId));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@ -46,7 +44,6 @@ export const useDocument = () => {
|
||||
dispatch(rangeActions.clear(docId));
|
||||
dispatch(rectSelectionActions.clear(docId));
|
||||
dispatch(slashCommandActions.clear(docId));
|
||||
dispatch(linkPopoverActions.clear(docId));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
@ -15,6 +15,10 @@
|
||||
background-color: var(--fill-list-active);
|
||||
}
|
||||
|
||||
.MuiList-root .Mui-focusVisible.MuiMenuItem-root:not(:hover, .Mui-selected) {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.MuiPaper-root.MuiMenu-paper.MuiPopover-paper {
|
||||
background-image: none;
|
||||
}
|
||||
|
@ -563,6 +563,9 @@
|
||||
},
|
||||
"inlineLink": {
|
||||
"placeholder": "Paste or type a link",
|
||||
"openInNewTab": "Open in new tab",
|
||||
"copyLink": "Copy link",
|
||||
"removeLink": "Remove link",
|
||||
"url": {
|
||||
"label": "Link URL",
|
||||
"placeholder": "Enter link URL"
|
||||
@ -651,7 +654,8 @@
|
||||
"objects": "Objects",
|
||||
"symbols": "Symbols",
|
||||
"flags": "Flags",
|
||||
"nature": "Nature"
|
||||
"nature": "Nature",
|
||||
"frequentlyUsed": "Frequently Used"
|
||||
}
|
||||
}
|
||||
}
|
112
frontend/rust-lib/Cargo.lock
generated
112
frontend/rust-lib/Cargo.lock
generated
@ -96,7 +96,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
[[package]]
|
||||
name = "appflowy-integrate"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -189,9 +189,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc00553f5f3c06ffd4510a9d576f92143618706c45ea6ff81e84ad9be9588abd"
|
||||
checksum = "bcdcf0d683fe9c23d32cf5b53c9918ea0a500375a9fb20109802552658e576c9"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-http",
|
||||
@ -219,9 +219,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cb57ac6088805821f78d282c0ba8aec809f11cbee10dda19a97b03ab040ccc2"
|
||||
checksum = "1fcdb2f7acbc076ff5ad05e7864bdb191ca70a6fd07668dc3a1a8bcd051de5ae"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
@ -233,9 +233,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-endpoint"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c5f6f84a4f46f95a9bb71d9300b73cd67eb868bc43ae84f66ad34752299f4ac"
|
||||
checksum = "8cce1c41a6cfaa726adee9ebb9a56fcd2bbfd8be49fd8a04c5e20fd968330b04"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
@ -247,9 +247,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-http"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a754683c322f7dc5167484266489fdebdcd04d26e53c162cad1f3f949f2c5671"
|
||||
checksum = "aadbc44e7a8f3e71c8b374e03ecd972869eb91dd2bc89ed018954a52ba84bc44"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-http",
|
||||
@ -292,9 +292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "0.27.0"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "babfd626348836a31785775e3c08a4c345a5ab4c6e06dfd9167f2bee0e6295d6"
|
||||
checksum = "c8b812340d86d4a766b2ca73f740dfd47a97c2dff0c06c8517a16d88241957e4"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-endpoint",
|
||||
@ -317,9 +317,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "0.27.0"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d0fbe3c2c342bc8dfea4bb43937405a8ec06f99140a0dcb9c7b59e54dfa93a1"
|
||||
checksum = "265fac131fbfc188e5c3d96652ea90ecc676a934e3174eaaee523c6cec040b3b"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-endpoint",
|
||||
@ -343,9 +343,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sig-auth"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84dc92a63ede3c2cbe43529cb87ffa58763520c96c6a46ca1ced80417afba845"
|
||||
checksum = "3b94acb10af0c879ecd5c7bdf51cda6679a0a4f4643ce630905a77673bfa3c61"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
@ -357,9 +357,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "392fefab9d6fcbd76d518eb3b1c040b84728ab50f58df0c3c53ada4bea9d327e"
|
||||
checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"form_urlencoded",
|
||||
@ -376,9 +376,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae23b9fe7a07d0919000116c4c5c0578303fbce6fc8d32efca1f7759d4c20faf"
|
||||
checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
@ -388,9 +388,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-client"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5230d25d244a51339273b8870f0f77874cd4449fb4f8f629b21188ae10cfc0ba"
|
||||
checksum = "0a86aa6e21e86c4252ad6a0e3e74da9617295d8d6e374d552be7d3059c41cedd"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
@ -412,9 +412,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b60e2133beb9fe6ffe0b70deca57aaeff0a35ad24a9c6fab2fd3b4f45b99fdb5"
|
||||
checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
@ -434,9 +434,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-tower"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a4d94f556c86a0dd916a5d7c39747157ea8cb909ca469703e20fee33e448b67"
|
||||
checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
@ -450,18 +450,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ce3d6e6ebb00b2cce379f079ad5ec508f9bcc3a9510d9b9c1840ed1d6f8af39"
|
||||
checksum = "23f9f42fbfa96d095194a632fbac19f60077748eba536eb0b9fecc28659807f8"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-query"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d58edfca32ef9bfbc1ca394599e17ea329cb52d6a07359827be74235b64b3298"
|
||||
checksum = "98819eb0b04020a1c791903533b638534ae6c12e2aceda3e6e6fba015608d51d"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"urlencoding",
|
||||
@ -469,9 +469,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58db46fc1f4f26be01ebdb821751b4e2482cd43aa2b64a0348fb89762defaffa"
|
||||
checksum = "16a3d0bf4f324f4ef9793b86a1701d9700fbcdbd12a846da45eed104c634c6e8"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"itoa",
|
||||
@ -482,18 +482,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-xml"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb557fe4995bd9ec87fb244bbb254666a971dc902a783e9da8b7711610e9664c"
|
||||
checksum = "b1b9d12875731bd07e767be7baad95700c3137b56730ec9ddeedb52a5e5ca63b"
|
||||
dependencies = [
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "0.55.2"
|
||||
version = "0.55.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0869598bfe46ec44ffe17e063ed33336e59df90356ca8ff0e8da6f7c1d994b"
|
||||
checksum = "6dd209616cc8d7bfb82f87811a5c655dc97537f592689b18743bddf5dc5c4829"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-async",
|
||||
@ -925,7 +925,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -943,7 +943,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-client-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab-sync",
|
||||
@ -961,7 +961,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -988,7 +988,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1000,7 +1000,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -1019,7 +1019,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -1039,7 +1039,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-persistence"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
@ -1059,7 +1059,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1089,7 +1089,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-sync"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9a50f#e9a50f780fa97b1e3fce6ffe7720e9585d674974"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab",
|
||||
@ -3450,9 +3450,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "postgrest"
|
||||
version = "1.5.0"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b"
|
||||
checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7"
|
||||
dependencies = [
|
||||
"reqwest",
|
||||
]
|
||||
@ -4108,9 +4108,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
@ -4229,15 +4229,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
|
||||
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.178"
|
||||
version = "1.0.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348"
|
||||
checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -4255,9 +4255,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.178"
|
||||
version = "1.0.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b"
|
||||
checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5163,9 +5163,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.2"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
|
@ -38,17 +38,17 @@ opt-level = 3
|
||||
incremental = false
|
||||
|
||||
[patch.crates-io]
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9a50f" }
|
||||
|
||||
#collab = { path = "../AppFlowy-Collab/collab" }
|
||||
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
||||
#collab-database= { path = "../AppFlowy-Collab/collab-database" }
|
||||
#collab-document = { path = "../AppFlowy-Collab/collab-document" }
|
||||
#collab-plugins = { path = "../AppFlowy-Collab/collab-plugins" }
|
||||
#appflowy-integrate = { path = "../AppFlowy-Collab/appflowy-integrate" }
|
||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "aac4e56" }
|
||||
#
|
||||
#collab = { path = "../../../AppFlowy-Collab/collab" }
|
||||
#collab-folder = { path = "../../../AppFlowy-Collab/collab-folder" }
|
||||
#collab-database= { path = "../../../AppFlowy-Collab/collab-database" }
|
||||
#collab-document = { path = "../../../AppFlowy-Collab/collab-document" }
|
||||
#collab-plugins = { path = "../../../AppFlowy-Collab/collab-plugins" }
|
||||
#appflowy-integrate = { path = "../../../AppFlowy-Collab/appflowy-integrate" }
|
||||
|
||||
|
85
frontend/rust-lib/flowy-folder2/src/entities/icon.rs
Normal file
85
frontend/rust-lib/flowy-folder2/src/entities/icon.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use crate::entities::parser::view::ViewIdentify;
|
||||
use collab_folder::core::{IconType, ViewIcon};
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
|
||||
#[derive(ProtoBuf_Enum, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ViewIconTypePB {
|
||||
#[default]
|
||||
Emoji = 0,
|
||||
Url = 1,
|
||||
Icon = 2,
|
||||
}
|
||||
|
||||
impl std::convert::From<ViewIconTypePB> for IconType {
|
||||
fn from(rev: ViewIconTypePB) -> Self {
|
||||
match rev {
|
||||
ViewIconTypePB::Emoji => IconType::Emoji,
|
||||
ViewIconTypePB::Url => IconType::Url,
|
||||
ViewIconTypePB::Icon => IconType::Icon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<ViewIconTypePB> for IconType {
|
||||
fn into(self) -> ViewIconTypePB {
|
||||
match self {
|
||||
IconType::Emoji => ViewIconTypePB::Emoji,
|
||||
IconType::Url => ViewIconTypePB::Url,
|
||||
IconType::Icon => ViewIconTypePB::Icon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ViewIconPB {
|
||||
#[pb(index = 1)]
|
||||
pub ty: ViewIconTypePB,
|
||||
#[pb(index = 2)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl std::convert::From<ViewIconPB> for ViewIcon {
|
||||
fn from(rev: ViewIconPB) -> Self {
|
||||
ViewIcon {
|
||||
ty: rev.ty.into(),
|
||||
value: rev.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<ViewIconPB> for ViewIcon {
|
||||
fn into(self) -> ViewIconPB {
|
||||
ViewIconPB {
|
||||
ty: self.ty.into(),
|
||||
value: self.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct UpdateViewIconPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub icon: Option<ViewIconPB>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UpdateViewIconParams {
|
||||
pub view_id: String,
|
||||
pub icon: Option<ViewIcon>,
|
||||
}
|
||||
|
||||
impl TryInto<UpdateViewIconParams> for UpdateViewIconPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<UpdateViewIconParams, Self::Error> {
|
||||
let view_id = ViewIdentify::parse(self.view_id)?.0;
|
||||
|
||||
let icon = self.icon.map(|icon| icon.into());
|
||||
|
||||
Ok(UpdateViewIconParams { view_id, icon })
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
pub mod icon;
|
||||
mod import;
|
||||
mod parser;
|
||||
pub mod trash;
|
||||
pub mod view;
|
||||
pub mod workspace;
|
||||
|
||||
pub use icon::*;
|
||||
pub use import::*;
|
||||
pub use trash::*;
|
||||
pub use view::*;
|
||||
|
@ -6,10 +6,6 @@ pub struct ViewName(pub String);
|
||||
|
||||
impl ViewName {
|
||||
pub fn parse(s: String) -> Result<ViewName, ErrorCode> {
|
||||
if s.trim().is_empty() {
|
||||
return Err(ErrorCode::ViewNameInvalid);
|
||||
}
|
||||
|
||||
if s.graphemes(true).count() > 256 {
|
||||
return Err(ErrorCode::ViewNameTooLong);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use collab_folder::core::{View, ViewLayout};
|
||||
|
||||
use crate::entities::icon::ViewIconPB;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
|
||||
@ -50,16 +51,11 @@ pub struct ViewPB {
|
||||
#[pb(index = 6)]
|
||||
pub layout: ViewLayoutPB,
|
||||
|
||||
/// The icon url of the view.
|
||||
/// It can be used to save the emoji icon of the view.
|
||||
/// The icon of the view.
|
||||
#[pb(index = 7, one_of)]
|
||||
pub icon_url: Option<String>,
|
||||
pub icon: Option<ViewIconPB>,
|
||||
|
||||
/// The cover url of the view.
|
||||
#[pb(index = 8, one_of)]
|
||||
pub cover_url: Option<String>,
|
||||
|
||||
#[pb(index = 9)]
|
||||
#[pb(index = 8)]
|
||||
pub is_favorite: bool,
|
||||
}
|
||||
|
||||
@ -71,9 +67,8 @@ pub fn view_pb_without_child_views(view: Arc<View>) -> ViewPB {
|
||||
create_time: view.created_at,
|
||||
child_views: Default::default(),
|
||||
layout: view.layout.clone().into(),
|
||||
icon_url: view.icon_url.clone(),
|
||||
cover_url: view.cover_url.clone(),
|
||||
is_favorite: view.is_favorite.clone(),
|
||||
icon: view.icon.clone().map(|icon| icon.into()),
|
||||
is_favorite: view.is_favorite,
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,9 +84,8 @@ pub fn view_pb_with_child_views(view: Arc<View>, child_views: Vec<Arc<View>>) ->
|
||||
.map(view_pb_without_child_views)
|
||||
.collect(),
|
||||
layout: view.layout.clone().into(),
|
||||
icon_url: view.icon_url.clone(),
|
||||
cover_url: view.cover_url.clone(),
|
||||
is_favorite: view.is_favorite.clone(),
|
||||
icon: view.icon.clone().map(|icon| icon.into()),
|
||||
is_favorite: view.is_favorite,
|
||||
}
|
||||
}
|
||||
|
||||
@ -318,12 +312,6 @@ pub struct UpdateViewPayloadPB {
|
||||
pub layout: Option<ViewLayoutPB>,
|
||||
|
||||
#[pb(index = 6, one_of)]
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
#[pb(index = 7, one_of)]
|
||||
pub cover_url: Option<String>,
|
||||
|
||||
#[pb(index = 8, one_of)]
|
||||
pub is_favorite: Option<bool>,
|
||||
}
|
||||
|
||||
@ -333,10 +321,8 @@ pub struct UpdateViewParams {
|
||||
pub name: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
pub icon_url: Option<String>,
|
||||
pub cover_url: Option<String>,
|
||||
pub is_favorite: Option<bool>,
|
||||
pub layout: Option<ViewLayout>,
|
||||
pub is_favorite: Option<bool>,
|
||||
}
|
||||
|
||||
impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
|
||||
@ -360,8 +346,6 @@ impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
|
||||
Some(thumbnail) => Some(ViewThumbnail::parse(thumbnail)?.0),
|
||||
};
|
||||
|
||||
let cover_url = self.cover_url;
|
||||
let icon_url = self.icon_url;
|
||||
let is_favorite = self.is_favorite;
|
||||
|
||||
Ok(UpdateViewParams {
|
||||
@ -369,8 +353,6 @@ impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
|
||||
name,
|
||||
desc,
|
||||
thumbnail,
|
||||
cover_url,
|
||||
icon_url,
|
||||
is_favorite,
|
||||
layout: self.layout.map(|ty| ty.into()),
|
||||
})
|
||||
|
@ -139,6 +139,17 @@ pub(crate) async fn update_view_handler(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, folder), err)]
|
||||
pub(crate) async fn update_view_icon_handler(
|
||||
data: AFPluginData<UpdateViewIconPayloadPB>,
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let folder = upgrade_folder(folder)?;
|
||||
let params: UpdateViewIconParams = data.into_inner().try_into()?;
|
||||
folder.update_view_icon_with_params(params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_view_handler(
|
||||
data: AFPluginData<RepeatedViewIdPB>,
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
@ -233,7 +244,7 @@ pub(crate) async fn read_favorites_handler(
|
||||
views.push(view);
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
return Err(err);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
|
||||
.event(FolderEvent::DeleteAllTrash, delete_all_trash_handler)
|
||||
.event(FolderEvent::ImportData, import_data_handler)
|
||||
.event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler)
|
||||
.event(FolderEvent::UpdateViewIcon, update_view_icon_handler)
|
||||
.event(FolderEvent::ReadFavorites, read_favorites_handler)
|
||||
.event(FolderEvent::ToggleFavorite, toggle_favorites_handler)
|
||||
}
|
||||
@ -149,4 +150,7 @@ pub enum FolderEvent {
|
||||
|
||||
#[event(input = "RepeatedViewIdPB")]
|
||||
ToggleFavorite = 34,
|
||||
|
||||
#[event(input = "UpdateViewIconPayloadPB")]
|
||||
UpdateViewIcon = 35,
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use collab::core::collab::{CollabRawData, MutexCollab};
|
||||
use collab::core::collab_state::SyncState;
|
||||
use collab_folder::core::{
|
||||
FavoritesInfo, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo,
|
||||
View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace,
|
||||
View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use tokio_stream::wrappers::WatchStream;
|
||||
@ -18,6 +18,7 @@ use tracing::{event, Level};
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_folder_deps::cloud::FolderCloudService;
|
||||
|
||||
use crate::entities::icon::UpdateViewIconParams;
|
||||
use crate::entities::{
|
||||
view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams,
|
||||
CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB,
|
||||
@ -448,7 +449,7 @@ impl FolderManager {
|
||||
pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> {
|
||||
self.with_folder((), |folder| {
|
||||
if let Some(view) = folder.views.get_view(view_id) {
|
||||
self.unfavorite_view_and_decendants(view.clone(), &folder);
|
||||
self.unfavorite_view_and_decendants(view.clone(), folder);
|
||||
folder.add_trash(vec![view_id.to_string()]);
|
||||
// notify the parent view that the view is moved to trash
|
||||
send_notification(view_id, FolderNotification::DidMoveViewToTrash)
|
||||
@ -587,34 +588,29 @@ impl FolderManager {
|
||||
/// Update the view with the given params.
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn update_view_with_params(&self, params: UpdateViewParams) -> FlowyResult<()> {
|
||||
let value = self.with_folder(None, |folder| {
|
||||
let old_view = folder.views.get_view(¶ms.view_id);
|
||||
let new_view = folder.views.update_view(¶ms.view_id, |update| {
|
||||
self
|
||||
.update_view(¶ms.view_id, |update| {
|
||||
update
|
||||
.set_name_if_not_none(params.name)
|
||||
.set_desc_if_not_none(params.desc)
|
||||
.set_layout_if_not_none(params.layout)
|
||||
.set_icon_url_if_not_none(params.icon_url)
|
||||
.set_cover_url_if_not_none(params.cover_url)
|
||||
.set_favorite_if_not_none(params.is_favorite)
|
||||
.done()
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
Some((old_view, new_view))
|
||||
});
|
||||
|
||||
if let Some((Some(old_view), Some(new_view))) = value {
|
||||
if let Ok(handler) = self.get_handler(&old_view.layout) {
|
||||
handler.did_update_view(&old_view, &new_view).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(view_pb) = self.get_view(¶ms.view_id).await {
|
||||
send_notification(&view_pb.id, FolderNotification::DidUpdateView)
|
||||
.payload(view_pb)
|
||||
.send();
|
||||
}
|
||||
Ok(())
|
||||
/// Update the icon of the view with the given params.
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn update_view_icon_with_params(
|
||||
&self,
|
||||
params: UpdateViewIconParams,
|
||||
) -> FlowyResult<()> {
|
||||
self
|
||||
.update_view(¶ms.view_id, |update| {
|
||||
update.set_icon(params.icon).done()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Duplicate the view with the given view id.
|
||||
@ -815,6 +811,32 @@ impl FolderManager {
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
/// Update the view with the provided view_id using the specified function.
|
||||
async fn update_view<F>(&self, view_id: &str, f: F) -> FlowyResult<()>
|
||||
where
|
||||
F: FnOnce(ViewUpdate) -> Option<View>,
|
||||
{
|
||||
let value = self.with_folder(None, |folder| {
|
||||
let old_view = folder.views.get_view(view_id);
|
||||
let new_view = folder.views.update_view(view_id, f);
|
||||
|
||||
Some((old_view, new_view))
|
||||
});
|
||||
|
||||
if let Some((Some(old_view), Some(new_view))) = value {
|
||||
if let Ok(handler) = self.get_handler(&old_view.layout) {
|
||||
handler.did_update_view(&old_view, &new_view).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(view_pb) = self.get_view(view_id).await {
|
||||
send_notification(&view_pb.id, FolderNotification::DidUpdateView)
|
||||
.payload(view_pb)
|
||||
.send();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a handler that implements the [FolderOperationHandler] trait
|
||||
fn get_handler(
|
||||
&self,
|
||||
|
@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
pub use collab_folder::core::View;
|
||||
use collab_folder::core::{RepeatedViewIdentifier, ViewIdentifier, ViewLayout};
|
||||
use collab_folder::core::{RepeatedViewIdentifier, ViewIcon, ViewIdentifier, ViewLayout};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use flowy_error::FlowyError;
|
||||
@ -55,8 +55,7 @@ pub struct ViewBuilder {
|
||||
layout: ViewLayout,
|
||||
child_views: Vec<ParentChildViews>,
|
||||
is_favorite: bool,
|
||||
icon_url: Option<String>,
|
||||
cover_url: Option<String>,
|
||||
icon: Option<ViewIcon>,
|
||||
}
|
||||
|
||||
impl ViewBuilder {
|
||||
@ -69,8 +68,7 @@ impl ViewBuilder {
|
||||
layout: ViewLayout::Document,
|
||||
child_views: vec![],
|
||||
is_favorite: false,
|
||||
icon_url: None,
|
||||
cover_url: None,
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,8 +112,7 @@ impl ViewBuilder {
|
||||
created_at: timestamp(),
|
||||
is_favorite: self.is_favorite,
|
||||
layout: self.layout,
|
||||
icon_url: self.icon_url,
|
||||
cover_url: self.cover_url,
|
||||
icon: self.icon,
|
||||
children: RepeatedViewIdentifier::new(
|
||||
self
|
||||
.child_views
|
||||
@ -257,8 +254,7 @@ pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View
|
||||
created_at: time,
|
||||
is_favorite: false,
|
||||
layout,
|
||||
cover_url: None,
|
||||
icon_url: None,
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::script::{FolderScript::*, FolderTest};
|
||||
use collab_folder::core::ViewLayout;
|
||||
use flowy_folder2::entities::icon::{ViewIconPB, ViewIconTypePB};
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_all_workspace_test() {
|
||||
@ -152,6 +153,32 @@ async fn view_update() {
|
||||
assert_eq!(test.child_view.name, new_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn view_icon_update_test() {
|
||||
let mut test = FolderTest::new().await;
|
||||
let view = test.child_view.clone();
|
||||
let new_icon = ViewIconPB {
|
||||
ty: ViewIconTypePB::Emoji,
|
||||
value: "👍".to_owned(),
|
||||
};
|
||||
assert!(view.icon.is_none());
|
||||
test
|
||||
.run_scripts(vec![
|
||||
UpdateViewIcon {
|
||||
icon: Some(new_icon.clone()),
|
||||
},
|
||||
ReadView(view.id.clone()),
|
||||
])
|
||||
.await;
|
||||
|
||||
assert_eq!(test.child_view.icon, Some(new_icon));
|
||||
|
||||
test
|
||||
.run_scripts(vec![UpdateViewIcon { icon: None }, ReadView(view.id)])
|
||||
.await;
|
||||
assert_eq!(test.child_view.icon, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic]
|
||||
async fn view_delete() {
|
||||
@ -263,8 +290,8 @@ async fn toggle_favorites() {
|
||||
ReadView(view.id.clone()),
|
||||
])
|
||||
.await;
|
||||
assert_eq!(test.child_view.is_favorite, true);
|
||||
assert!(test.favorites.len() != 0);
|
||||
assert!(test.child_view.is_favorite);
|
||||
assert_ne!(test.favorites.len(), 0);
|
||||
assert_eq!(test.favorites[0].id, view.id);
|
||||
|
||||
let view = test.child_view.clone();
|
||||
@ -293,12 +320,12 @@ async fn delete_favorites() {
|
||||
ReadView(view.id.clone()),
|
||||
])
|
||||
.await;
|
||||
assert_eq!(test.child_view.is_favorite, true);
|
||||
assert!(test.favorites.len() != 0);
|
||||
assert!(test.child_view.is_favorite);
|
||||
assert_ne!(test.favorites.len(), 0);
|
||||
assert_eq!(test.favorites[0].id, view.id);
|
||||
|
||||
test.run_scripts(vec![DeleteView, ReadFavorites]).await;
|
||||
assert!(test.favorites.len() == 0);
|
||||
assert_eq!(test.favorites.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -1,5 +1,6 @@
|
||||
use collab_folder::core::ViewLayout;
|
||||
|
||||
use flowy_folder2::entities::icon::{UpdateViewIconPayloadPB, ViewIconPB};
|
||||
use flowy_folder2::entities::*;
|
||||
use flowy_folder2::event_map::FolderEvent::*;
|
||||
use flowy_test::event_builder::EventBuilder;
|
||||
@ -42,6 +43,9 @@ pub enum FolderScript {
|
||||
desc: Option<String>,
|
||||
is_favorite: Option<bool>,
|
||||
},
|
||||
UpdateViewIcon {
|
||||
icon: Option<ViewIconPB>,
|
||||
},
|
||||
DeleteView,
|
||||
DeleteViews(Vec<String>),
|
||||
MoveView {
|
||||
@ -164,6 +168,9 @@ impl FolderTest {
|
||||
} => {
|
||||
update_view(sdk, &self.child_view.id, name, desc, is_favorite).await;
|
||||
},
|
||||
FolderScript::UpdateViewIcon { icon } => {
|
||||
update_view_icon(sdk, &self.child_view.id, icon).await;
|
||||
},
|
||||
FolderScript::DeleteView => {
|
||||
delete_view(sdk, vec![self.child_view.id.clone()]).await;
|
||||
},
|
||||
@ -333,6 +340,18 @@ pub async fn update_view(
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn update_view_icon(sdk: &FlowyCoreTest, view_id: &str, icon: Option<ViewIconPB>) {
|
||||
let request = UpdateViewIconPayloadPB {
|
||||
view_id: view_id.to_string(),
|
||||
icon,
|
||||
};
|
||||
EventBuilder::new(sdk.clone())
|
||||
.event(UpdateViewIcon)
|
||||
.payload(request)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn delete_view(sdk: &FlowyCoreTest, view_ids: Vec<String>) {
|
||||
let request = RepeatedViewIdPB { items: view_ids };
|
||||
EventBuilder::new(sdk.clone())
|
||||
|
@ -16,6 +16,7 @@ use flowy_database2::entities::*;
|
||||
use flowy_database2::event_map::DatabaseEvent;
|
||||
use flowy_document2::entities::{DocumentDataPB, OpenDocumentPayloadPB};
|
||||
use flowy_document2::event_map::DocumentEvent;
|
||||
use flowy_folder2::entities::icon::UpdateViewIconPayloadPB;
|
||||
use flowy_folder2::entities::*;
|
||||
use flowy_folder2::event_map::FolderEvent;
|
||||
use flowy_notification::entities::SubscribeObject;
|
||||
@ -184,6 +185,15 @@ impl FlowyCoreTest {
|
||||
.error()
|
||||
}
|
||||
|
||||
pub async fn update_view_icon(&self, payload: UpdateViewIconPayloadPB) -> Option<FlowyError> {
|
||||
EventBuilder::new(self.clone())
|
||||
.event(FolderEvent::UpdateViewIcon)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await
|
||||
.error()
|
||||
}
|
||||
|
||||
pub async fn create_view(&self, parent_id: &str, name: String) -> ViewPB {
|
||||
let payload = CreateViewPayloadPB {
|
||||
parent_view_id: parent_id.to_string(),
|
||||
@ -797,7 +807,7 @@ impl Cleaner {
|
||||
Cleaner(dir)
|
||||
}
|
||||
|
||||
fn cleanup(dir: &PathBuf) {
|
||||
fn cleanup(_dir: &PathBuf) {
|
||||
// let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use flowy_folder2::entities::icon::{UpdateViewIconPayloadPB, ViewIconPB, ViewIconTypePB};
|
||||
use flowy_folder2::entities::*;
|
||||
use flowy_test::event_builder::EventBuilder;
|
||||
use flowy_test::FlowyCoreTest;
|
||||
@ -83,45 +84,27 @@ async fn update_view_event_with_name_test() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_view_event_with_icon_url_test() {
|
||||
async fn update_view_icon_event_test() {
|
||||
let test = FlowyCoreTest::new_with_guest_user().await;
|
||||
let current_workspace = test.get_current_workspace().await.workspace;
|
||||
let view = test
|
||||
.create_view(¤t_workspace.id, "My first view".to_string())
|
||||
.await;
|
||||
|
||||
let new_icon = ViewIconPB {
|
||||
ty: ViewIconTypePB::Emoji,
|
||||
value: "👍".to_owned(),
|
||||
};
|
||||
let error = test
|
||||
.update_view(UpdateViewPayloadPB {
|
||||
.update_view_icon(UpdateViewIconPayloadPB {
|
||||
view_id: view.id.clone(),
|
||||
icon_url: Some("appflowy.io".to_string()),
|
||||
..Default::default()
|
||||
icon: Some(new_icon.clone()),
|
||||
})
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let view = test.get_view(&view.id).await;
|
||||
assert_eq!(view.icon_url.unwrap(), "appflowy.io");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_view_event_with_cover_url_test() {
|
||||
let test = FlowyCoreTest::new_with_guest_user().await;
|
||||
let current_workspace = test.get_current_workspace().await.workspace;
|
||||
let view = test
|
||||
.create_view(¤t_workspace.id, "My first view".to_string())
|
||||
.await;
|
||||
|
||||
let error = test
|
||||
.update_view(UpdateViewPayloadPB {
|
||||
view_id: view.id.clone(),
|
||||
cover_url: Some("appflowy.io".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let view = test.get_view(&view.id).await;
|
||||
assert_eq!(view.cover_url.unwrap(), "appflowy.io");
|
||||
assert_eq!(view.icon, Some(new_icon));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -42,19 +42,18 @@ impl UserLocalDataMigration {
|
||||
pub fn run(self, migrations: Vec<Box<dyn UserDataMigration>>) -> FlowyResult<Vec<String>> {
|
||||
let mut applied_migrations = vec![];
|
||||
let conn = self.sqlite_pool.get()?;
|
||||
let record = get_all_records(&*conn)?;
|
||||
let record = get_all_records(&conn)?;
|
||||
let mut duplicated_names = vec![];
|
||||
for migration in migrations {
|
||||
if record
|
||||
if !record
|
||||
.iter()
|
||||
.find(|record| record.migration_name == migration.name())
|
||||
.is_none()
|
||||
.any(|record| record.migration_name == migration.name())
|
||||
{
|
||||
let migration_name = migration.name().to_string();
|
||||
if !duplicated_names.contains(&migration_name) {
|
||||
migration.run(&self.session, &self.collab_db)?;
|
||||
applied_migrations.push(migration.name().to_string());
|
||||
save_record(&*conn, &migration_name);
|
||||
save_record(&conn, &migration_name);
|
||||
duplicated_names.push(migration_name);
|
||||
} else {
|
||||
tracing::error!("Duplicated migration name: {}", migration_name);
|
||||
|
@ -86,7 +86,7 @@ impl UserSession {
|
||||
.run(vec![Box::new(HistoricalEmptyDocumentMigration)])
|
||||
{
|
||||
Ok(applied_migrations) => {
|
||||
if applied_migrations.len() > 0 {
|
||||
if !applied_migrations.is_empty() {
|
||||
tracing::info!("Did apply migrations: {:?}", applied_migrations);
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user