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