mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support custom scrollbar for document (#4936)
feat: support keywords for slash list to search feat: support right-click to copy,pasted,cut fix: the hint text should follow the align setting feat: support get/set latest view feat: support to show snackbar after delete page fix: some bugs
This commit is contained in:
parent
40b710d140
commit
370f8a6558
@ -52,6 +52,7 @@
|
|||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-big-calendar": "^1.8.5",
|
"react-big-calendar": "^1.8.5",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
|
"react-custom-scrollbars": "^4.2.1",
|
||||||
"react-datepicker": "^4.23.0",
|
"react-datepicker": "^4.23.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
@ -79,8 +80,8 @@
|
|||||||
"yjs": "^13.5.51"
|
"yjs": "^13.5.51"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.5.6",
|
|
||||||
"@svgr/plugin-svgo": "^8.0.1",
|
"@svgr/plugin-svgo": "^8.0.1",
|
||||||
|
"@tauri-apps/cli": "^1.5.6",
|
||||||
"@types/google-protobuf": "^3.15.12",
|
"@types/google-protobuf": "^3.15.12",
|
||||||
"@types/is-hotkey": "^0.1.7",
|
"@types/is-hotkey": "^0.1.7",
|
||||||
"@types/jest": "^29.5.3",
|
"@types/jest": "^29.5.3",
|
||||||
@ -92,6 +93,7 @@
|
|||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-beautiful-dnd": "^13.1.3",
|
"@types/react-beautiful-dnd": "^13.1.3",
|
||||||
"@types/react-color": "^3.0.6",
|
"@types/react-color": "^3.0.6",
|
||||||
|
"@types/react-custom-scrollbars": "^4.0.13",
|
||||||
"@types/react-datepicker": "^4.19.3",
|
"@types/react-datepicker": "^4.19.3",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
|
@ -103,6 +103,9 @@ dependencies:
|
|||||||
react-color:
|
react-color:
|
||||||
specifier: ^2.19.3
|
specifier: ^2.19.3
|
||||||
version: 2.19.3(react@18.2.0)
|
version: 2.19.3(react@18.2.0)
|
||||||
|
react-custom-scrollbars:
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
react-datepicker:
|
react-datepicker:
|
||||||
specifier: ^4.23.0
|
specifier: ^4.23.0
|
||||||
version: 4.23.0(react-dom@18.2.0)(react@18.2.0)
|
version: 4.23.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -219,6 +222,9 @@ devDependencies:
|
|||||||
'@types/react-color':
|
'@types/react-color':
|
||||||
specifier: ^3.0.6
|
specifier: ^3.0.6
|
||||||
version: 3.0.6
|
version: 3.0.6
|
||||||
|
'@types/react-custom-scrollbars':
|
||||||
|
specifier: ^4.0.13
|
||||||
|
version: 4.0.13
|
||||||
'@types/react-datepicker':
|
'@types/react-datepicker':
|
||||||
specifier: ^4.19.3
|
specifier: ^4.19.3
|
||||||
version: 4.19.3(react-dom@18.2.0)(react@18.2.0)
|
version: 4.19.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -2346,6 +2352,12 @@ packages:
|
|||||||
'@types/reactcss': 1.2.6
|
'@types/reactcss': 1.2.6
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/react-custom-scrollbars@4.0.13:
|
||||||
|
resolution: {integrity: sha512-t+15reWgAE1jXlrhaZoxjuH/SQf+EG0rzAzSCzTIkSiP5CDT7KhoExNPwIa6uUxtPkjc3gdW/ry7GetLEwCfGA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.6
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0):
|
/@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==}
|
resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2655,6 +2667,10 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/add-px-to-style@1.0.0:
|
||||||
|
resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/agent-base@6.0.2:
|
/agent-base@6.0.2:
|
||||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
@ -3343,6 +3359,14 @@ packages:
|
|||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/dom-css@2.1.0:
|
||||||
|
resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==}
|
||||||
|
dependencies:
|
||||||
|
add-px-to-style: 1.0.0
|
||||||
|
prefix-style: 2.0.1
|
||||||
|
to-camel-case: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dom-helpers@5.2.1:
|
/dom-helpers@5.2.1:
|
||||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5426,6 +5450,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/performance-now@2.1.0:
|
||||||
|
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/picocolors@1.0.0:
|
/picocolors@1.0.0:
|
||||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||||
|
|
||||||
@ -5518,6 +5546,10 @@ packages:
|
|||||||
source-map-js: 1.0.2
|
source-map-js: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/prefix-style@2.0.1:
|
||||||
|
resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/prelude-ls@1.2.1:
|
/prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -5686,6 +5718,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
|
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/raf@3.4.1:
|
||||||
|
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
||||||
|
dependencies:
|
||||||
|
performance-now: 2.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0):
|
/react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
|
resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -5746,6 +5784,19 @@ packages:
|
|||||||
tinycolor2: 1.6.0
|
tinycolor2: 1.6.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-custom-scrollbars@4.2.1(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^0.14.0 || ^15.0.0 || ^16.0.0
|
||||||
|
react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0
|
||||||
|
dependencies:
|
||||||
|
dom-css: 2.1.0
|
||||||
|
prop-types: 15.8.1
|
||||||
|
raf: 3.4.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0):
|
/react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==}
|
resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -6627,16 +6678,32 @@ packages:
|
|||||||
/tmpl@1.0.5:
|
/tmpl@1.0.5:
|
||||||
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
|
||||||
|
|
||||||
|
/to-camel-case@1.0.0:
|
||||||
|
resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==}
|
||||||
|
dependencies:
|
||||||
|
to-space-case: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-fast-properties@2.0.0:
|
/to-fast-properties@2.0.0:
|
||||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
/to-no-case@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-regex-range@5.0.1:
|
/to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
/to-space-case@1.0.0:
|
||||||
|
resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==}
|
||||||
|
dependencies:
|
||||||
|
to-no-case: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tough-cookie@4.1.3:
|
/tough-cookie@4.1.3:
|
||||||
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
|
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
FolderEventMoveNestedView,
|
FolderEventMoveNestedView,
|
||||||
FolderEventUpdateView,
|
FolderEventUpdateView,
|
||||||
FolderEventUpdateViewIcon,
|
FolderEventUpdateViewIcon,
|
||||||
|
FolderEventSetLatestView,
|
||||||
} from '@/services/backend/events/flowy-folder';
|
} from '@/services/backend/events/flowy-folder';
|
||||||
|
|
||||||
export async function getPage(id: string) {
|
export async function getPage(id: string) {
|
||||||
@ -149,3 +150,17 @@ export const updatePageIcon = async (viewId: string, icon?: PageIcon) => {
|
|||||||
|
|
||||||
return Promise.reject(result.err);
|
return Promise.reject(result.err);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function setLatestOpenedPage(id: string) {
|
||||||
|
const payload = new ViewIdPB({
|
||||||
|
value: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await FolderEventSetLatestView(payload);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return res.val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(res.err);
|
||||||
|
}
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Scrollbars } from 'react-custom-scrollbars';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface AFScrollerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
overflowXHidden?: boolean;
|
||||||
|
overflowYHidden?: boolean;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
export const AFScroller = ({ style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps) => {
|
||||||
|
return (
|
||||||
|
<Scrollbars
|
||||||
|
autoHide
|
||||||
|
renderThumbHorizontal={(props) => <div {...props} className='appflowy-scrollbar-thumb-horizontal' />}
|
||||||
|
renderThumbVertical={(props) => <div {...props} className='appflowy-scrollbar-thumb-vertical' />}
|
||||||
|
{...(overflowXHidden && {
|
||||||
|
renderTrackHorizontal: (props) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
display: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
{...(overflowYHidden && {
|
||||||
|
renderTrackVertical: (props) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
display: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
style={style}
|
||||||
|
renderView={(props) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
...props.style,
|
||||||
|
overflowX: overflowXHidden ? 'hidden' : 'auto',
|
||||||
|
overflowY: overflowYHidden ? 'hidden' : 'auto',
|
||||||
|
marginRight: 0,
|
||||||
|
marginBottom: 0,
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Scrollbars>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './AFScroller';
|
@ -2,18 +2,17 @@ import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup';
|
import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useAuth } from '$app/components/auth/auth.hooks';
|
import { useAuth } from '$app/components/auth/auth.hooks';
|
||||||
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
export const Welcome = () => {
|
export const Welcome = () => {
|
||||||
const { signInAsAnonymous } = useAuth();
|
const { signInAsAnonymous } = useAuth();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={(e) => e.preventDefault()} method='POST'>
|
<form onSubmit={(e) => e.preventDefault()} method='POST'>
|
||||||
<div className='relative flex h-screen w-screen flex-col items-center justify-center gap-12 bg-bg-body text-center text-text-title text-text-title'>
|
<div className='relative flex h-screen w-screen flex-col items-center justify-center gap-12 bg-bg-body text-center text-text-title'>
|
||||||
<div className='flex justify-center' id='appflowy'>
|
<div className='flex justify-center' id='appflowy'>
|
||||||
<AppflowyLogo className={'h-16 w-16'} />
|
<AppflowyLogo className={'h-16 w-16'} />
|
||||||
</div>
|
</div>
|
||||||
@ -33,9 +32,8 @@ export const Welcome = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await signInAsAnonymous();
|
await signInAsAnonymous();
|
||||||
navigate('/');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
Log.error(e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
import { currentUserActions, LoginState, parseWorkspaceSettingPBToSetting } from '$app_reducers/current-user/slice';
|
||||||
import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user';
|
import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user';
|
||||||
import { UserService } from '$app/application/user/user.service';
|
import { UserService } from '$app/application/user/user.service';
|
||||||
import { AuthService } from '$app/application/user/auth.service';
|
import { AuthService } from '$app/application/user/auth.service';
|
||||||
@ -48,7 +48,7 @@ export const useAuth = () => {
|
|||||||
displayName: userProfile.name,
|
displayName: userProfile.name,
|
||||||
iconUrl: userProfile.icon_url,
|
iconUrl: userProfile.icon_url,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
workspaceSetting: workspaceSetting,
|
workspaceSetting: workspaceSetting ? parseWorkspaceSettingPBToSetting(workspaceSetting) : undefined,
|
||||||
isLocal,
|
isLocal,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,6 @@ function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const filtersCount = useFiltersCount();
|
const filtersCount = useFiltersCount();
|
||||||
const highlight = filtersCount > 0;
|
const highlight = filtersCount > 0;
|
||||||
|
|
||||||
const [filterAnchorEl, setFilterAnchorEl] = useState<null | HTMLElement>(null);
|
const [filterAnchorEl, setFilterAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const open = Boolean(filterAnchorEl);
|
const open = Boolean(filterAnchorEl);
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ function PlaceholderContent({ node, ...attributes }: { node: Element; className?
|
|||||||
}, [editor, node]);
|
}, [editor, node]);
|
||||||
|
|
||||||
const className = useMemo(() => {
|
const className = useMemo(() => {
|
||||||
return `text-placeholder ${attributes.className ?? ''}`;
|
return `text-placeholder select-none ${attributes.className ?? ''}`;
|
||||||
}, [attributes.className]);
|
}, [attributes.className]);
|
||||||
|
|
||||||
const unSelectedPlaceholder = useMemo(() => {
|
const unSelectedPlaceholder = useMemo(() => {
|
||||||
|
@ -20,7 +20,6 @@ export const Text = memo(
|
|||||||
>
|
>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
<Placeholder isEmpty={isEmpty} node={node} />
|
<Placeholder isEmpty={isEmpty} node={node} />
|
||||||
|
|
||||||
<span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span>
|
<span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -12,8 +12,9 @@ export const CollaborativeEditor = memo(
|
|||||||
const [sharedType, setSharedType] = useState<YXmlText | null>(null);
|
const [sharedType, setSharedType] = useState<YXmlText | null>(null);
|
||||||
const provider = useMemo(() => {
|
const provider = useMemo(() => {
|
||||||
setSharedType(null);
|
setSharedType(null);
|
||||||
return new Provider(id, showTitle);
|
|
||||||
}, [id, showTitle]);
|
return new Provider(id);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
const root = useMemo(() => {
|
const root = useMemo(() => {
|
||||||
if (!showTitle || !sharedType || !sharedType.doc) return null;
|
if (!showTitle || !sharedType || !sharedType.doc) return null;
|
||||||
@ -70,17 +71,18 @@ export const CollaborativeEditor = memo(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
provider.connect();
|
provider.connect();
|
||||||
|
|
||||||
const handleConnected = () => {
|
const handleConnected = () => {
|
||||||
setSharedType(provider.sharedType);
|
setSharedType(provider.sharedType);
|
||||||
};
|
};
|
||||||
|
|
||||||
provider.on('ready', handleConnected);
|
provider.on('ready', handleConnected);
|
||||||
|
void provider.initialDocument(showTitle);
|
||||||
return () => {
|
return () => {
|
||||||
setSharedType(null);
|
|
||||||
provider.off('ready', handleConnected);
|
provider.off('ready', handleConnected);
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
};
|
};
|
||||||
}, [provider]);
|
}, [provider, showTitle]);
|
||||||
|
|
||||||
if (!sharedType || id !== provider.id) {
|
if (!sharedType || id !== provider.id) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -47,6 +47,20 @@ export function useEditor(sharedType: Y.XmlText) {
|
|||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const handleOnClickEnd = useCallback(() => {
|
const handleOnClickEnd = useCallback(() => {
|
||||||
|
const path = [editor.children.length - 1];
|
||||||
|
const node = Editor.node(editor, path) as NodeEntry<Element>;
|
||||||
|
const latestNodeIsEmpty = CustomEditor.isEmptyText(editor, node[0]);
|
||||||
|
|
||||||
|
if (latestNodeIsEmpty) {
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
editor.select(path);
|
||||||
|
editor.collapse({
|
||||||
|
edge: 'end',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CustomEditor.insertEmptyLineAtEnd(editor);
|
CustomEditor.insertEmptyLineAtEnd(editor);
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { memo, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
useDecorateCodeHighlight,
|
useDecorateCodeHighlight,
|
||||||
useEditor,
|
useEditor,
|
||||||
@ -90,4 +90,4 @@ function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(Editor);
|
export default Editor;
|
||||||
|
@ -2,8 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import { Mention, MentionPage } from '$app/application/document/document.types';
|
import { Mention, MentionPage } from '$app/application/document/document.types';
|
||||||
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
|
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { pageTypeMap } from '$app_reducers/pages/slice';
|
|
||||||
import { getPage } from '$app/application/folder/page.service';
|
import { getPage } from '$app/application/folder/page.service';
|
||||||
import { useSelected, useSlate } from 'slate-react';
|
import { useSelected, useSlate } from 'slate-react';
|
||||||
import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg';
|
import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg';
|
||||||
@ -11,15 +9,17 @@ import { notify } from 'src/appflowy_app/components/_shared/notify';
|
|||||||
import { subscribeNotifications } from '$app/application/notification';
|
import { subscribeNotifications } from '$app/application/notification';
|
||||||
import { FolderNotification } from '@/services/backend';
|
import { FolderNotification } from '@/services/backend';
|
||||||
import { Editor, Range } from 'slate';
|
import { Editor, Range } from 'slate';
|
||||||
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
|
import { openPage } from '$app_reducers/pages/async_actions';
|
||||||
|
|
||||||
export function MentionLeaf({ mention }: { mention: Mention }) {
|
export function MentionLeaf({ mention }: { mention: Mention }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [page, setPage] = useState<MentionPage | null>(null);
|
const [page, setPage] = useState<MentionPage | null>(null);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
const navigate = useNavigate();
|
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
const selected = useSelected();
|
const selected = useSelected();
|
||||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selected && isCollapsed && page) {
|
if (selected && isCollapsed && page) {
|
||||||
@ -56,16 +56,14 @@ export function MentionLeaf({ mention }: { mention: Mention }) {
|
|||||||
void loadPage();
|
void loadPage();
|
||||||
}, [loadPage]);
|
}, [loadPage]);
|
||||||
|
|
||||||
const openPage = useCallback(() => {
|
const handleOpenPage = useCallback(() => {
|
||||||
if (!page) {
|
if (!page) {
|
||||||
notify.error(t('document.mention.deletedContent'));
|
notify.error(t('document.mention.deletedContent'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageType = pageTypeMap[page.layout];
|
void dispatch(openPage(page.id));
|
||||||
|
}, [page, dispatch, t]);
|
||||||
navigate(`/page/${pageType}/${page.id}`);
|
|
||||||
}, [navigate, page, t]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
@ -117,7 +115,7 @@ export function MentionLeaf({ mention }: { mention: Mention }) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`mention-inline mx-1 inline-flex select-none items-center gap-1`}
|
className={`mention-inline mx-1 inline-flex select-none items-center gap-1`}
|
||||||
onClick={openPage}
|
onClick={handleOpenPage}
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
backgroundColor: selected ? 'var(--content-blue-100)' : undefined,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { RefObject, useCallback, useEffect, useState } from 'react';
|
import { RefObject, useCallback, useEffect, useState } from 'react';
|
||||||
import { ReactEditor, useSlate } from 'slate-react';
|
import { ReactEditor, useSlate } from 'slate-react';
|
||||||
import { findEventRange, getBlockActionsPosition } from '$app/components/editor/components/tools/block_actions/utils';
|
import { findEventNode, getBlockActionsPosition } from '$app/components/editor/components/tools/block_actions/utils';
|
||||||
import { Element, Editor, Range } from 'slate';
|
import { Element, Editor, Range } from 'slate';
|
||||||
import { EditorNodeType } from '$app/application/document/document.types';
|
import { EditorNodeType } from '$app/application/document/document.types';
|
||||||
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
export function useBlockActionsToolbar(ref: RefObject<HTMLDivElement>, contextMenuVisible: boolean) {
|
export function useBlockActionsToolbar(ref: RefObject<HTMLDivElement>, contextMenuVisible: boolean) {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
@ -45,37 +46,55 @@ export function useBlockActionsToolbar(ref: RefObject<HTMLDivElement>, contextMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
let range: Range | null = null;
|
let range: Range | null = null;
|
||||||
|
let node;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
range = ReactEditor.findEventRange(editor, e);
|
range = ReactEditor.findEventRange(editor, e);
|
||||||
} catch {
|
} catch {
|
||||||
const editorDom = ReactEditor.toDOMNode(editor, editor);
|
const editorDom = ReactEditor.toDOMNode(editor, editor);
|
||||||
|
const rect = editorDom.getBoundingClientRect();
|
||||||
|
const isOverLeftBoundary = e.clientX < rect.left + 64;
|
||||||
|
const isOverRightBoundary = e.clientX > rect.right - 64;
|
||||||
|
let newX = e.clientX;
|
||||||
|
|
||||||
range = findEventRange(editor, {
|
if (isOverLeftBoundary) {
|
||||||
...e,
|
newX = rect.left + 64;
|
||||||
clientX: e.clientX + editorDom.offsetWidth / 2,
|
}
|
||||||
clientY: e.clientY,
|
|
||||||
|
if (isOverRightBoundary) {
|
||||||
|
newX = rect.right - 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = findEventNode(editor, {
|
||||||
|
x: newX,
|
||||||
|
y: e.clientY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!range) {
|
if (!range && !node) {
|
||||||
|
Log.warn('No range and node found');
|
||||||
return;
|
return;
|
||||||
|
} else if (range) {
|
||||||
|
const match = editor.above({
|
||||||
|
match: (n) => {
|
||||||
|
return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined;
|
||||||
|
},
|
||||||
|
at: range,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = match[0] as Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = editor.above({
|
if (!node) {
|
||||||
match: (n) => {
|
|
||||||
return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined;
|
|
||||||
},
|
|
||||||
at: range,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = match[0] as Element;
|
|
||||||
|
|
||||||
if (node.type === EditorNodeType.Page) return;
|
if (node.type === EditorNodeType.Page) return;
|
||||||
const blockElement = ReactEditor.toDOMNode(editor, node);
|
const blockElement = ReactEditor.toDOMNode(editor, node);
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ import { PopoverProps } from '@mui/material/Popover';
|
|||||||
|
|
||||||
import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected';
|
import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected';
|
||||||
import withErrorBoundary from '$app/components/_shared/error_boundary/withError';
|
import withErrorBoundary from '$app/components/_shared/error_boundary/withError';
|
||||||
|
import { CustomEditor } from '$app/components/editor/command';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { Range } from 'slate';
|
||||||
|
|
||||||
const Toolbar = () => {
|
const Toolbar = () => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
@ -38,10 +41,42 @@ const Toolbar = () => {
|
|||||||
if (!node) return;
|
if (!node) return;
|
||||||
const nodeDom = ReactEditor.toDOMNode(editor, node);
|
const nodeDom = ReactEditor.toDOMNode(editor, node);
|
||||||
const onContextMenu = (e: MouseEvent) => {
|
const onContextMenu = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const { clientX, clientY } = e;
|
const { clientX, clientY } = e;
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const { selection } = editor;
|
||||||
|
|
||||||
|
const editorRange = ReactEditor.findEventRange(editor, e);
|
||||||
|
|
||||||
|
if (!editorRange || !selection) return;
|
||||||
|
|
||||||
|
const rangeBlock = CustomEditor.getBlock(editor, editorRange);
|
||||||
|
const selectedBlock = CustomEditor.getBlock(editor, selection);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Range.intersection(selection, editorRange) ||
|
||||||
|
(rangeBlock && selectedBlock && isEqual(rangeBlock[1], selectedBlock[1]))
|
||||||
|
) {
|
||||||
|
const windowSelection = window.getSelection();
|
||||||
|
const range = windowSelection?.rangeCount ? windowSelection?.getRangeAt(0) : null;
|
||||||
|
const isCollapsed = windowSelection?.isCollapsed;
|
||||||
|
|
||||||
|
if (windowSelection && !isCollapsed) {
|
||||||
|
if (range && range.endOffset === 0 && range.startContainer !== range.endContainer) {
|
||||||
|
const newRange = range.cloneRange();
|
||||||
|
|
||||||
|
newRange.setEnd(range.startContainer, range.startOffset);
|
||||||
|
windowSelection.removeAllRanges();
|
||||||
|
windowSelection.addRange(newRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
popoverPropsRef.current = {
|
popoverPropsRef.current = {
|
||||||
transformOrigin: {
|
transformOrigin: {
|
||||||
vertical: 'top',
|
vertical: 'top',
|
||||||
|
@ -2,7 +2,6 @@ import { ReactEditor } from 'slate-react';
|
|||||||
import { getEditorDomNode, getHeadingCssProperty } from '$app/components/editor/plugins/utils';
|
import { getEditorDomNode, getHeadingCssProperty } from '$app/components/editor/plugins/utils';
|
||||||
import { Element } from 'slate';
|
import { Element } from 'slate';
|
||||||
import { EditorNodeType, HeadingNode } from '$app/application/document/document.types';
|
import { EditorNodeType, HeadingNode } from '$app/application/document/document.types';
|
||||||
import { Log } from '$app/utils/log';
|
|
||||||
|
|
||||||
export function getBlockActionsPosition(editor: ReactEditor, blockElement: HTMLElement) {
|
export function getBlockActionsPosition(editor: ReactEditor, blockElement: HTMLElement) {
|
||||||
const editorDom = getEditorDomNode(editor);
|
const editorDom = getEditorDomNode(editor);
|
||||||
@ -35,41 +34,25 @@ export function getBlockCssProperty(node: Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve can not find the range when the drop occurs on the icon.
|
|
||||||
* @param editor
|
* @param editor
|
||||||
* @param e
|
* @param e
|
||||||
*/
|
*/
|
||||||
export function findEventRange(editor: ReactEditor, e: MouseEvent) {
|
export function findEventNode(
|
||||||
const { clientX: x, clientY: y } = e;
|
editor: ReactEditor,
|
||||||
|
{
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const element = document.elementFromPoint(x, y);
|
||||||
|
const nodeDom = element?.closest('[data-block-type]');
|
||||||
|
|
||||||
// Else resolve a range from the caret position where the drop occured.
|
if (nodeDom) {
|
||||||
let domRange;
|
return ReactEditor.toSlateNode(editor, nodeDom) as Element;
|
||||||
const { document } = ReactEditor.getWindow(editor);
|
|
||||||
|
|
||||||
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
|
||||||
if (document.caretRangeFromPoint) {
|
|
||||||
domRange = document.caretRangeFromPoint(x, y);
|
|
||||||
} else if ('caretPositionFromPoint' in document && typeof document.caretPositionFromPoint === 'function') {
|
|
||||||
const position = document.caretPositionFromPoint(x, y);
|
|
||||||
|
|
||||||
if (position) {
|
|
||||||
domRange = document.createRange();
|
|
||||||
domRange.setStart(position.offsetNode, position.offset);
|
|
||||||
domRange.setEnd(position.offsetNode, position.offset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!domRange) {
|
return null;
|
||||||
Log.warn('Could not find a range from the caret position.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return ReactEditor.toSlateRange(editor, domRange, {
|
|
||||||
exactMatch: false,
|
|
||||||
suppressThrow: false,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,87 +20,16 @@ import { CustomEditor } from '$app/components/editor/command';
|
|||||||
import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||||
import { YjsEditor } from '@slate-yjs/core';
|
import { YjsEditor } from '@slate-yjs/core';
|
||||||
import { useEditorBlockDispatch } from '$app/components/editor/stores/block';
|
import { useEditorBlockDispatch } from '$app/components/editor/stores/block';
|
||||||
|
import {
|
||||||
enum SlashCommandPanelTab {
|
headingTypes,
|
||||||
BASIC = 'basic',
|
headingTypeToLevelMap,
|
||||||
MEDIA = 'media',
|
reorderSlashOptions,
|
||||||
DATABASE = 'database',
|
SlashAliases,
|
||||||
ADVANCED = 'advanced',
|
SlashCommandPanelTab,
|
||||||
}
|
slashOptionGroup,
|
||||||
|
slashOptionMapToEditorNodeType,
|
||||||
export enum SlashOptionType {
|
SlashOptionType,
|
||||||
Paragraph,
|
} from '$app/components/editor/components/tools/command_panel/slash_command_panel/const';
|
||||||
TodoList,
|
|
||||||
Heading1,
|
|
||||||
Heading2,
|
|
||||||
Heading3,
|
|
||||||
BulletedList,
|
|
||||||
NumberedList,
|
|
||||||
Quote,
|
|
||||||
ToggleList,
|
|
||||||
Divider,
|
|
||||||
Callout,
|
|
||||||
Code,
|
|
||||||
Grid,
|
|
||||||
MathEquation,
|
|
||||||
Image,
|
|
||||||
}
|
|
||||||
const slashOptionGroup = [
|
|
||||||
{
|
|
||||||
key: SlashCommandPanelTab.BASIC,
|
|
||||||
options: [
|
|
||||||
SlashOptionType.Paragraph,
|
|
||||||
SlashOptionType.TodoList,
|
|
||||||
SlashOptionType.Heading1,
|
|
||||||
SlashOptionType.Heading2,
|
|
||||||
SlashOptionType.Heading3,
|
|
||||||
SlashOptionType.BulletedList,
|
|
||||||
SlashOptionType.NumberedList,
|
|
||||||
SlashOptionType.Quote,
|
|
||||||
SlashOptionType.ToggleList,
|
|
||||||
SlashOptionType.Divider,
|
|
||||||
SlashOptionType.Callout,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SlashCommandPanelTab.MEDIA,
|
|
||||||
options: [SlashOptionType.Code, SlashOptionType.Image],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SlashCommandPanelTab.DATABASE,
|
|
||||||
options: [SlashOptionType.Grid],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SlashCommandPanelTab.ADVANCED,
|
|
||||||
options: [SlashOptionType.MathEquation],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const slashOptionMapToEditorNodeType = {
|
|
||||||
[SlashOptionType.Paragraph]: EditorNodeType.Paragraph,
|
|
||||||
[SlashOptionType.TodoList]: EditorNodeType.TodoListBlock,
|
|
||||||
[SlashOptionType.Heading1]: EditorNodeType.HeadingBlock,
|
|
||||||
[SlashOptionType.Heading2]: EditorNodeType.HeadingBlock,
|
|
||||||
[SlashOptionType.Heading3]: EditorNodeType.HeadingBlock,
|
|
||||||
[SlashOptionType.BulletedList]: EditorNodeType.BulletedListBlock,
|
|
||||||
[SlashOptionType.NumberedList]: EditorNodeType.NumberedListBlock,
|
|
||||||
[SlashOptionType.Quote]: EditorNodeType.QuoteBlock,
|
|
||||||
[SlashOptionType.ToggleList]: EditorNodeType.ToggleListBlock,
|
|
||||||
[SlashOptionType.Divider]: EditorNodeType.DividerBlock,
|
|
||||||
[SlashOptionType.Callout]: EditorNodeType.CalloutBlock,
|
|
||||||
[SlashOptionType.Code]: EditorNodeType.CodeBlock,
|
|
||||||
[SlashOptionType.Grid]: EditorNodeType.GridBlock,
|
|
||||||
[SlashOptionType.MathEquation]: EditorNodeType.EquationBlock,
|
|
||||||
[SlashOptionType.Image]: EditorNodeType.ImageBlock,
|
|
||||||
};
|
|
||||||
|
|
||||||
const headingTypeToLevelMap: Record<string, number> = {
|
|
||||||
[SlashOptionType.Heading1]: 1,
|
|
||||||
[SlashOptionType.Heading2]: 2,
|
|
||||||
[SlashOptionType.Heading3]: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const headingTypes = [SlashOptionType.Heading1, SlashOptionType.Heading2, SlashOptionType.Heading3];
|
|
||||||
|
|
||||||
export function useSlashCommandPanel({
|
export function useSlashCommandPanel({
|
||||||
searchText,
|
searchText,
|
||||||
@ -281,6 +210,7 @@ export function useSlashCommandPanel({
|
|||||||
key: group.key,
|
key: group.key,
|
||||||
content: <div className={'px-3 pb-1 pt-2 text-sm'}>{groupTypeToLabelMap[group.key]}</div>,
|
content: <div className={'px-3 pb-1 pt-2 text-sm'}>{groupTypeToLabelMap[group.key]}</div>,
|
||||||
children: group.options
|
children: group.options
|
||||||
|
|
||||||
.map((type) => {
|
.map((type) => {
|
||||||
return {
|
return {
|
||||||
key: type,
|
key: type,
|
||||||
@ -297,8 +227,12 @@ export function useSlashCommandPanel({
|
|||||||
newSearchText = searchText.slice(1);
|
newSearchText = searchText.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return label.toLowerCase().includes(newSearchText.toLowerCase());
|
return (
|
||||||
}),
|
label.toLowerCase().includes(newSearchText.toLowerCase()) ||
|
||||||
|
SlashAliases[option.key].some((alias) => alias.startsWith(newSearchText.toLowerCase()))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort(reorderSlashOptions(searchText)),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((group) => group.children.length > 0);
|
.filter((group) => group.children.length > 0);
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||||
import {
|
import { useSlashCommandPanel } from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks';
|
||||||
SlashOptionType,
|
|
||||||
useSlashCommandPanel,
|
|
||||||
} from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks';
|
|
||||||
import { useSlateStatic } from 'slate-react';
|
import { useSlateStatic } from 'slate-react';
|
||||||
|
import { SlashOptionType } from '$app/components/editor/components/tools/command_panel/slash_command_panel/const';
|
||||||
|
|
||||||
const noResultBuffer = 2;
|
const noResultBuffer = 2;
|
||||||
|
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
import { EditorNodeType } from '$app/application/document/document.types';
|
||||||
|
|
||||||
|
export enum SlashCommandPanelTab {
|
||||||
|
BASIC = 'basic',
|
||||||
|
MEDIA = 'media',
|
||||||
|
DATABASE = 'database',
|
||||||
|
ADVANCED = 'advanced',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SlashOptionType {
|
||||||
|
Paragraph,
|
||||||
|
TodoList,
|
||||||
|
Heading1,
|
||||||
|
Heading2,
|
||||||
|
Heading3,
|
||||||
|
BulletedList,
|
||||||
|
NumberedList,
|
||||||
|
Quote,
|
||||||
|
ToggleList,
|
||||||
|
Divider,
|
||||||
|
Callout,
|
||||||
|
Code,
|
||||||
|
Grid,
|
||||||
|
MathEquation,
|
||||||
|
Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const slashOptionGroup = [
|
||||||
|
{
|
||||||
|
key: SlashCommandPanelTab.BASIC,
|
||||||
|
options: [
|
||||||
|
SlashOptionType.Paragraph,
|
||||||
|
SlashOptionType.TodoList,
|
||||||
|
SlashOptionType.Heading1,
|
||||||
|
SlashOptionType.Heading2,
|
||||||
|
SlashOptionType.Heading3,
|
||||||
|
SlashOptionType.BulletedList,
|
||||||
|
SlashOptionType.NumberedList,
|
||||||
|
SlashOptionType.Quote,
|
||||||
|
SlashOptionType.ToggleList,
|
||||||
|
SlashOptionType.Divider,
|
||||||
|
SlashOptionType.Callout,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SlashCommandPanelTab.MEDIA,
|
||||||
|
options: [SlashOptionType.Code, SlashOptionType.Image],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SlashCommandPanelTab.DATABASE,
|
||||||
|
options: [SlashOptionType.Grid],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SlashCommandPanelTab.ADVANCED,
|
||||||
|
options: [SlashOptionType.MathEquation],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const slashOptionMapToEditorNodeType = {
|
||||||
|
[SlashOptionType.Paragraph]: EditorNodeType.Paragraph,
|
||||||
|
[SlashOptionType.TodoList]: EditorNodeType.TodoListBlock,
|
||||||
|
[SlashOptionType.Heading1]: EditorNodeType.HeadingBlock,
|
||||||
|
[SlashOptionType.Heading2]: EditorNodeType.HeadingBlock,
|
||||||
|
[SlashOptionType.Heading3]: EditorNodeType.HeadingBlock,
|
||||||
|
[SlashOptionType.BulletedList]: EditorNodeType.BulletedListBlock,
|
||||||
|
[SlashOptionType.NumberedList]: EditorNodeType.NumberedListBlock,
|
||||||
|
[SlashOptionType.Quote]: EditorNodeType.QuoteBlock,
|
||||||
|
[SlashOptionType.ToggleList]: EditorNodeType.ToggleListBlock,
|
||||||
|
[SlashOptionType.Divider]: EditorNodeType.DividerBlock,
|
||||||
|
[SlashOptionType.Callout]: EditorNodeType.CalloutBlock,
|
||||||
|
[SlashOptionType.Code]: EditorNodeType.CodeBlock,
|
||||||
|
[SlashOptionType.Grid]: EditorNodeType.GridBlock,
|
||||||
|
[SlashOptionType.MathEquation]: EditorNodeType.EquationBlock,
|
||||||
|
[SlashOptionType.Image]: EditorNodeType.ImageBlock,
|
||||||
|
};
|
||||||
|
export const headingTypeToLevelMap: Record<string, number> = {
|
||||||
|
[SlashOptionType.Heading1]: 1,
|
||||||
|
[SlashOptionType.Heading2]: 2,
|
||||||
|
[SlashOptionType.Heading3]: 3,
|
||||||
|
};
|
||||||
|
export const headingTypes = [SlashOptionType.Heading1, SlashOptionType.Heading2, SlashOptionType.Heading3];
|
||||||
|
|
||||||
|
export const SlashAliases = {
|
||||||
|
[SlashOptionType.Paragraph]: ['paragraph', 'text', 'block', 'textblock'],
|
||||||
|
[SlashOptionType.TodoList]: [
|
||||||
|
'list',
|
||||||
|
'todo',
|
||||||
|
'todolist',
|
||||||
|
'checkbox',
|
||||||
|
'block',
|
||||||
|
'todoblock',
|
||||||
|
'checkboxblock',
|
||||||
|
'todolistblock',
|
||||||
|
],
|
||||||
|
[SlashOptionType.Heading1]: ['h1', 'heading1', 'block', 'headingblock', 'h1block'],
|
||||||
|
[SlashOptionType.Heading2]: ['h2', 'heading2', 'block', 'headingblock', 'h2block'],
|
||||||
|
[SlashOptionType.Heading3]: ['h3', 'heading3', 'block', 'headingblock', 'h3block'],
|
||||||
|
[SlashOptionType.BulletedList]: [
|
||||||
|
'list',
|
||||||
|
'bulleted',
|
||||||
|
'block',
|
||||||
|
'bulletedlist',
|
||||||
|
'bulletedblock',
|
||||||
|
'listblock',
|
||||||
|
'bulletedlistblock',
|
||||||
|
'bulletelist',
|
||||||
|
],
|
||||||
|
[SlashOptionType.NumberedList]: [
|
||||||
|
'list',
|
||||||
|
'numbered',
|
||||||
|
'block',
|
||||||
|
'numberedlist',
|
||||||
|
'numberedblock',
|
||||||
|
'listblock',
|
||||||
|
'numberedlistblock',
|
||||||
|
'numberlist',
|
||||||
|
],
|
||||||
|
[SlashOptionType.Quote]: ['quote', 'block', 'quoteblock'],
|
||||||
|
[SlashOptionType.ToggleList]: ['list', 'toggle', 'block', 'togglelist', 'toggleblock', 'listblock', 'togglelistblock'],
|
||||||
|
[SlashOptionType.Divider]: ['divider', 'hr', 'block', 'dividerblock', 'line', 'lineblock'],
|
||||||
|
[SlashOptionType.Callout]: ['callout', 'info', 'block', 'calloutblock'],
|
||||||
|
[SlashOptionType.Code]: ['code', 'code', 'block', 'codeblock', 'media'],
|
||||||
|
[SlashOptionType.Grid]: ['grid', 'table', 'block', 'gridblock', 'database'],
|
||||||
|
[SlashOptionType.MathEquation]: [
|
||||||
|
'math',
|
||||||
|
'equation',
|
||||||
|
'block',
|
||||||
|
'mathblock',
|
||||||
|
'mathequation',
|
||||||
|
'mathequationblock',
|
||||||
|
'advanced',
|
||||||
|
],
|
||||||
|
[SlashOptionType.Image]: ['img', 'image', 'block', 'imageblock', 'media'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reorderSlashOptions = (searchText: string) => {
|
||||||
|
return (
|
||||||
|
a: {
|
||||||
|
key: SlashOptionType;
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
key: SlashOptionType;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const compareIndex = (option: SlashOptionType) => {
|
||||||
|
const aliases = SlashAliases[option];
|
||||||
|
|
||||||
|
if (aliases) {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
if (alias.startsWith(searchText)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareLength = (option: SlashOptionType) => {
|
||||||
|
const aliases = SlashAliases[option];
|
||||||
|
|
||||||
|
if (aliases) {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
if (alias.length < searchText.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return compareIndex(a.key) - compareIndex(b.key) || compareLength(a.key) - compareLength(b.key);
|
||||||
|
};
|
||||||
|
};
|
@ -108,19 +108,61 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-placeholder {
|
.text-placeholder {
|
||||||
|
@apply absolute left-[5px] w-full transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap;
|
||||||
&:after {
|
&:after {
|
||||||
@apply text-text-placeholder absolute left-[5px] top-1/2 transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap;
|
@apply text-text-placeholder absolute top-0;
|
||||||
content: (attr(placeholder));
|
content: (attr(placeholder));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-start-icon > .text-placeholder {
|
.block-align-center {
|
||||||
&:after {
|
.text-placeholder {
|
||||||
@apply left-[29px];
|
&:after {
|
||||||
|
@apply left-[calc(50%-5px)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.has-start-icon .text-placeholder {
|
||||||
|
&:after {
|
||||||
|
@apply left-[calc(50%+7px)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-align-left {
|
||||||
|
.text-placeholder {
|
||||||
|
&:after {
|
||||||
|
@apply left-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.has-start-icon .text-placeholder {
|
||||||
|
&:after {
|
||||||
|
@apply left-[24px];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-align-right {
|
||||||
|
|
||||||
|
.text-placeholder {
|
||||||
|
|
||||||
|
@apply relative w-fit order-2;
|
||||||
|
&:after {
|
||||||
|
@apply relative top-1/2 left-[-6px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text-content {
|
||||||
|
@apply order-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-start-icon .text-placeholder {
|
||||||
|
&:after {
|
||||||
|
@apply left-[-6px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.formula-inline {
|
.formula-inline {
|
||||||
&.selected {
|
&.selected {
|
||||||
@apply rounded bg-content-blue-100;
|
@apply rounded bg-content-blue-100;
|
||||||
|
@ -71,8 +71,13 @@ export function withBlockDelete(editor: ReactEditor) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the current node is not a paragraph, convert it to a paragraph
|
// if the current node is not a paragraph, convert it to a paragraph(except code block and callout block)
|
||||||
if (node.type !== EditorNodeType.Paragraph && node.type !== EditorNodeType.Page) {
|
if (
|
||||||
|
![EditorNodeType.Paragraph, EditorNodeType.CalloutBlock, EditorNodeType.CodeBlock].includes(
|
||||||
|
node.type as EditorNodeType
|
||||||
|
) &&
|
||||||
|
node.type !== EditorNodeType.Page
|
||||||
|
) {
|
||||||
CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph });
|
CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ describe('Transform events to actions', () => {
|
|||||||
let provider: Provider;
|
let provider: Provider;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
provider = new Provider(generateId());
|
provider = new Provider(generateId());
|
||||||
|
provider.initialDocument(true);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
applyActions.mockClear();
|
applyActions.mockClear();
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@ describe('Provider connected', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
provider = new Provider(generateId());
|
provider = new Provider(generateId());
|
||||||
|
provider.initialDocument(true);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
applyActions.mockClear();
|
applyActions.mockClear();
|
||||||
});
|
});
|
||||||
|
@ -13,10 +13,9 @@ export class Provider extends EventEmitter {
|
|||||||
dataClient: DataClient;
|
dataClient: DataClient;
|
||||||
// get origin data after document updated
|
// get origin data after document updated
|
||||||
backupDoc: Y.Doc = new Y.Doc();
|
backupDoc: Y.Doc = new Y.Doc();
|
||||||
constructor(public id: string, includeRoot?: boolean) {
|
constructor(public id: string) {
|
||||||
super();
|
super();
|
||||||
this.dataClient = new DataClient(id);
|
this.dataClient = new DataClient(id);
|
||||||
void this.initialDocument(includeRoot);
|
|
||||||
this.document.on('update', this.documentUpdate);
|
this.document.on('update', this.documentUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,24 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react';
|
import React, { ReactNode, useEffect, useMemo } from 'react';
|
||||||
import SideBar from '$app/components/layout/side_bar/SideBar';
|
import SideBar from '$app/components/layout/side_bar/SideBar';
|
||||||
import TopBar from '$app/components/layout/top_bar/TopBar';
|
import TopBar from '$app/components/layout/top_bar/TopBar';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import './layout.scss';
|
import './layout.scss';
|
||||||
|
import { AFScroller } from '../_shared/scroller';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { pageTypeMap } from '$app_reducers/pages/slice';
|
||||||
|
|
||||||
function Layout({ children }: { children: ReactNode }) {
|
function Layout({ children }: { children: ReactNode }) {
|
||||||
const { isCollapsed, width } = useAppSelector((state) => state.sidebar);
|
const { isCollapsed, width } = useAppSelector((state) => state.sidebar);
|
||||||
|
const currentUser = useAppSelector((state) => state.currentUser);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id: latestOpenViewId, layout } = useMemo(
|
||||||
|
() =>
|
||||||
|
currentUser?.workspaceSetting?.latestView || {
|
||||||
|
id: undefined,
|
||||||
|
layout: undefined,
|
||||||
|
},
|
||||||
|
[currentUser?.workspaceSetting?.latestView]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
@ -19,6 +32,14 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
window.removeEventListener('keydown', onKeyDown);
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (latestOpenViewId) {
|
||||||
|
const pageType = pageTypeMap[layout];
|
||||||
|
|
||||||
|
navigate(`/page/${pageType}/${latestOpenViewId}`);
|
||||||
|
}
|
||||||
|
}, [latestOpenViewId, navigate, layout]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex h-screen w-[100%] select-none text-sm text-text-title'>
|
<div className='flex h-screen w-[100%] select-none text-sm text-text-title'>
|
||||||
@ -30,14 +51,15 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<div
|
<AFScroller
|
||||||
|
overflowXHidden
|
||||||
style={{
|
style={{
|
||||||
height: 'calc(100vh - 64px)',
|
height: 'calc(100vh - 64px)',
|
||||||
}}
|
}}
|
||||||
className={'appflowy-layout appflowy-scroll-container select-none overflow-y-auto overflow-x-hidden'}
|
className={'appflowy-layout appflowy-scroll-container select-none'}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</AFScroller>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -3,23 +3,22 @@ import { useLoadExpandedPages } from '$app/components/layout/bread_crumb/Breadcr
|
|||||||
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Page, pageTypeMap } from '$app_reducers/pages/slice';
|
import { Page } from '$app_reducers/pages/slice';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getPageIcon } from '$app/hooks/page.hooks';
|
import { getPageIcon } from '$app/hooks/page.hooks';
|
||||||
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
|
import { openPage } from '$app_reducers/pages/async_actions';
|
||||||
|
|
||||||
function Breadcrumb() {
|
function Breadcrumb() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isTrash, pagePath, currentPage } = useLoadExpandedPages();
|
const { isTrash, pagePath, currentPage } = useLoadExpandedPages();
|
||||||
const navigate = useNavigate();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const navigateToPage = useCallback(
|
const navigateToPage = useCallback(
|
||||||
(page: Page) => {
|
(page: Page) => {
|
||||||
const pageType = pageTypeMap[page.layout];
|
void dispatch(openPage(page.id));
|
||||||
|
|
||||||
navigate(`/page/${pageType}/${page.id}`);
|
|
||||||
},
|
},
|
||||||
[navigate]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentPage) {
|
if (!currentPage) {
|
||||||
|
@ -32,10 +32,16 @@
|
|||||||
|
|
||||||
.appflowy-scroll-container {
|
.appflowy-scroll-container {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical {
|
||||||
|
background-color: var(--scrollbar-thumb);
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
.workspaces {
|
.workspaces {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { pagesActions, pageTypeMap, parserViewPBToPage } from '$app_reducers/pages/slice';
|
import { pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { FolderNotification, ViewLayoutPB } from '@/services/backend';
|
import { FolderNotification, ViewLayoutPB } from '@/services/backend';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
import { openPage, updatePageName } from '$app_reducers/pages/async_actions';
|
||||||
import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service';
|
import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service';
|
||||||
import { subscribeNotifications } from '$app/application/notification';
|
import { subscribeNotifications } from '$app/application/notification';
|
||||||
|
|
||||||
@ -82,14 +82,10 @@ export function usePageActions(pageId: string) {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const currentPageId = params.id;
|
const currentPageId = params.id;
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onPageClick = useCallback(() => {
|
const onPageClick = useCallback(() => {
|
||||||
if (!page) return;
|
void dispatch(openPage(pageId));
|
||||||
const pageType = pageTypeMap[page.layout];
|
}, [dispatch, pageId]);
|
||||||
|
|
||||||
navigate(`/page/${pageType}/${pageId}`);
|
|
||||||
}, [navigate, page, pageId]);
|
|
||||||
|
|
||||||
const onAddPage = useCallback(
|
const onAddPage = useCallback(
|
||||||
async (layout: ViewLayoutPB) => {
|
async (layout: ViewLayoutPB) => {
|
||||||
@ -112,21 +108,19 @@ export function usePageActions(pageId: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
dispatch(pagesActions.expandPage(pageId));
|
dispatch(pagesActions.expandPage(pageId));
|
||||||
const pageType = pageTypeMap[layout];
|
await dispatch(openPage(newViewId));
|
||||||
|
|
||||||
navigate(`/page/${pageType}/${newViewId}`);
|
|
||||||
},
|
},
|
||||||
[dispatch, navigate, pageId]
|
[dispatch, pageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDeletePage = useCallback(async () => {
|
const onDeletePage = useCallback(async () => {
|
||||||
if (currentPageId === pageId) {
|
if (currentPageId === pageId) {
|
||||||
navigate(`/`);
|
dispatch(pagesActions.setTrashSnackbar(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
await deletePage(pageId);
|
await deletePage(pageId);
|
||||||
dispatch(pagesActions.deletePages([pageId]));
|
dispatch(pagesActions.deletePages([pageId]));
|
||||||
}, [dispatch, currentPageId, navigate, pageId]);
|
}, [dispatch, pageId, currentPageId]);
|
||||||
|
|
||||||
const onDuplicatePage = useCallback(async () => {
|
const onDuplicatePage = useCallback(async () => {
|
||||||
await duplicatePage(page);
|
await duplicatePage(page);
|
||||||
|
@ -43,7 +43,7 @@ function Resizer() {
|
|||||||
<div
|
<div
|
||||||
onMouseDown={onResizeStart}
|
onMouseDown={onResizeStart}
|
||||||
style={{
|
style={{
|
||||||
left: `${width - 8}px`,
|
left: `${width - 4}px`,
|
||||||
}}
|
}}
|
||||||
className={'fixed top-0 z-10 h-screen cursor-col-resize'}
|
className={'fixed top-0 z-10 h-screen cursor-col-resize'}
|
||||||
>
|
>
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Alert, Snackbar } from '@mui/material';
|
||||||
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { pagesActions } from '$app_reducers/pages/slice';
|
||||||
|
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { useTrashActions } from '$app/components/trash/Trash.hooks';
|
||||||
|
import { openPage } from '$app_reducers/pages/async_actions';
|
||||||
|
|
||||||
|
function SlideTransition(props: SlideProps) {
|
||||||
|
return <Slide {...props} direction='down' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeletePageSnackbar() {
|
||||||
|
const firstViewId = useAppSelector((state) => {
|
||||||
|
const workspaceId = state.workspace.currentWorkspaceId;
|
||||||
|
const children = workspaceId ? state.pages.relationMap[workspaceId] : undefined;
|
||||||
|
|
||||||
|
if (!children) return null;
|
||||||
|
|
||||||
|
return children[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const showTrashSnackbar = useAppSelector((state) => state.pages.showTrashSnackbar);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { onPutback, onDelete } = useTrashActions();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(pagesActions.setTrashSnackbar(false));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (firstViewId) {
|
||||||
|
void dispatch(openPage(firstViewId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (toBack = true) => {
|
||||||
|
dispatch(pagesActions.setTrashSnackbar(false));
|
||||||
|
if (toBack) {
|
||||||
|
handleBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestore = () => {
|
||||||
|
if (!id) return;
|
||||||
|
void onPutback(id);
|
||||||
|
handleClose(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (!id) return;
|
||||||
|
void onDelete([id]);
|
||||||
|
|
||||||
|
if (!firstViewId) {
|
||||||
|
handleClose(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center',
|
||||||
|
}}
|
||||||
|
open={showTrashSnackbar}
|
||||||
|
onClose={() => handleClose()}
|
||||||
|
TransitionComponent={SlideTransition}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
className={'flex items-center'}
|
||||||
|
onClose={() => handleClose()}
|
||||||
|
severity='info'
|
||||||
|
variant='standard'
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
'.MuiAlert-action': {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={'flex h-full w-full items-center justify-center gap-3'}>
|
||||||
|
<span>{t('deletePagePrompt.text')}</span>
|
||||||
|
<Button onClick={handleRestore} size={'small'} color={'primary'} variant={'text'}>
|
||||||
|
{t('deletePagePrompt.restore')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDelete} size={'small'} color={'error'} variant={'text'}>
|
||||||
|
{t('deletePagePrompt.deletePermanent')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeletePageSnackbar;
|
@ -2,12 +2,13 @@ import React from 'react';
|
|||||||
import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton';
|
import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb';
|
import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb';
|
||||||
|
import DeletePageSnackbar from '$app/components/layout/top_bar/DeletePageSnackbar';
|
||||||
|
|
||||||
function TopBar() {
|
function TopBar() {
|
||||||
const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed);
|
const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-[64px] select-none border-b border-line-divider p-4'}>
|
<div className={'appflowy-top-bar flex h-[64px] select-none border-b border-line-divider p-4'}>
|
||||||
{sidebarIsCollapsed && (
|
{sidebarIsCollapsed && (
|
||||||
<div className={'mr-2 pt-[3px]'}>
|
<div className={'mr-2 pt-[3px]'}>
|
||||||
<CollapseMenuButton />
|
<CollapseMenuButton />
|
||||||
@ -18,6 +19,7 @@ function TopBar() {
|
|||||||
<Breadcrumb />
|
<Breadcrumb />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DeletePageSnackbar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { subscribeNotifications } from '$app/application/notification';
|
|||||||
import { FolderNotification, ViewLayoutPB } from '@/services/backend';
|
import { FolderNotification, ViewLayoutPB } from '@/services/backend';
|
||||||
import * as workspaceService from '$app/application/folder/workspace.service';
|
import * as workspaceService from '$app/application/folder/workspace.service';
|
||||||
import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service';
|
import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { openPage } from '$app_reducers/pages/async_actions';
|
||||||
|
|
||||||
export function useLoadWorkspaces() {
|
export function useLoadWorkspaces() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -108,8 +108,7 @@ export function useLoadWorkspace(workspace: WorkspaceItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkspaceActions(workspaceId: string) {
|
export function useWorkspaceActions(workspaceId: string) {
|
||||||
const navigate = useNavigate();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const newPage = useCallback(async () => {
|
const newPage = useCallback(async () => {
|
||||||
const { id } = await createCurrentWorkspaceChildView({
|
const { id } = await createCurrentWorkspaceChildView({
|
||||||
name: '',
|
name: '',
|
||||||
@ -117,8 +116,19 @@ export function useWorkspaceActions(workspaceId: string) {
|
|||||||
parent_view_id: workspaceId,
|
parent_view_id: workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(`/page/document/${id}`);
|
dispatch(
|
||||||
}, [navigate, workspaceId]);
|
pagesActions.addPage({
|
||||||
|
page: {
|
||||||
|
id: id,
|
||||||
|
parentId: workspaceId,
|
||||||
|
layout: ViewLayoutPB.Document,
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
isLast: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
void dispatch(openPage(id));
|
||||||
|
}, [dispatch, workspaceId]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newPage,
|
newPage,
|
||||||
|
@ -5,6 +5,7 @@ import Workspace from './Workspace';
|
|||||||
import TrashButton from '$app/components/layout/workspace_manager/TrashButton';
|
import TrashButton from '$app/components/layout/workspace_manager/TrashButton';
|
||||||
import { useAppSelector } from '@/appflowy_app/stores/store';
|
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
import { LoginState } from '$app_reducers/current-user/slice';
|
import { LoginState } from '$app_reducers/current-user/slice';
|
||||||
|
import { AFScroller } from '$app/components/_shared/scroller';
|
||||||
|
|
||||||
function WorkspaceManager() {
|
function WorkspaceManager() {
|
||||||
const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces();
|
const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces();
|
||||||
@ -19,13 +20,13 @@ function WorkspaceManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'workspaces flex h-full select-none flex-col justify-between'}>
|
<div className={'workspaces flex h-full select-none flex-col justify-between'}>
|
||||||
<div className={'mt-4 flex w-full flex-1 select-none flex-col overflow-y-auto overflow-x-hidden'}>
|
<AFScroller overflowXHidden className={'mt-4 flex w-full flex-1 select-none flex-col'}>
|
||||||
<div className={'flex-1'}>
|
<div className={'flex-1'}>
|
||||||
{workspaces.map((workspace) => (
|
{workspaces.map((workspace) => (
|
||||||
<Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} />
|
<Workspace opened={currentWorkspace?.id === workspace.id} key={workspace.id} workspace={workspace} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AFScroller>
|
||||||
<div className={'flex w-[100%] items-center px-2'}>
|
<div className={'flex w-[100%] items-center px-2'}>
|
||||||
<TrashButton />
|
<TrashButton />
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,6 @@ import { Login } from '$app/components/settings/Login';
|
|||||||
import SwipeableViews from 'react-swipeable-views';
|
import SwipeableViews from 'react-swipeable-views';
|
||||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||||
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
import { currentUserActions, LoginState } from '$app_reducers/current-user/slice';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const SettingsDialog = (props: DialogProps) => {
|
export const SettingsDialog = (props: DialogProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -44,17 +43,14 @@ export const SettingsDialog = (props: DialogProps) => {
|
|||||||
|
|
||||||
const currentRoute = routes[routes.length - 1];
|
const currentRoute = routes[routes.length - 1];
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) {
|
if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) {
|
||||||
navigate('/');
|
|
||||||
handleClose();
|
handleClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastLoginStateRef.current = loginState;
|
lastLoginStateRef.current = loginState;
|
||||||
}, [loginState, handleClose, navigate]);
|
}, [loginState, handleClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace';
|
import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace';
|
||||||
import { ThemeModePB as ThemeMode } from '@/services/backend';
|
import { ThemeModePB as ThemeMode } from '@/services/backend';
|
||||||
|
import { Page, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||||
|
|
||||||
export { ThemeMode };
|
export { ThemeMode };
|
||||||
|
|
||||||
@ -23,6 +24,20 @@ export enum LoginState {
|
|||||||
Error = 'error',
|
Error = 'error',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserWorkspaceSetting {
|
||||||
|
workspaceId: string;
|
||||||
|
latestView?: Page;
|
||||||
|
hasLatestView: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWorkspaceSettingPBToSetting(workspaceSetting: WorkspaceSettingPB): UserWorkspaceSetting {
|
||||||
|
return {
|
||||||
|
workspaceId: workspaceSetting.workspace_id,
|
||||||
|
latestView: workspaceSetting.latest_view ? parserViewPBToPage(workspaceSetting.latest_view) : undefined,
|
||||||
|
hasLatestView: !!workspaceSetting.latest_view,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICurrentUser {
|
export interface ICurrentUser {
|
||||||
id?: number;
|
id?: number;
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
@ -31,7 +46,7 @@ export interface ICurrentUser {
|
|||||||
token?: string;
|
token?: string;
|
||||||
iconUrl?: string;
|
iconUrl?: string;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
workspaceSetting?: WorkspaceSettingPB;
|
workspaceSetting?: UserWorkspaceSetting;
|
||||||
userSetting: UserSetting;
|
userSetting: UserSetting;
|
||||||
isLocal: boolean;
|
isLocal: boolean;
|
||||||
loginState?: LoginState;
|
loginState?: LoginState;
|
||||||
@ -71,6 +86,13 @@ export const currentUserSlice = createSlice({
|
|||||||
resetLoginState: (state) => {
|
resetLoginState: (state) => {
|
||||||
state.loginState = undefined;
|
state.loginState = undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setLatestView: (state, action: PayloadAction<Page>) => {
|
||||||
|
if (state.workspaceSetting) {
|
||||||
|
state.workspaceSetting.latestView = action.payload;
|
||||||
|
state.workspaceSetting.hasLatestView = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { RootState } from '$app/stores/store';
|
import { RootState } from '$app/stores/store';
|
||||||
import { pagesActions } from '$app_reducers/pages/slice';
|
import { pagesActions } from '$app_reducers/pages/slice';
|
||||||
import { movePage, updatePage } from '$app/application/folder/page.service';
|
import { movePage, setLatestOpenedPage, updatePage } from '$app/application/folder/page.service';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
|
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||||
|
|
||||||
export const movePageThunk = createAsyncThunk(
|
export const movePageThunk = createAsyncThunk(
|
||||||
'pages/movePage',
|
'pages/movePage',
|
||||||
@ -91,3 +92,15 @@ export const updatePageName = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const openPage = createAsyncThunk('pages/openPage', async (id: string, thunkAPI) => {
|
||||||
|
const { dispatch, getState } = thunkAPI;
|
||||||
|
const { pageMap } = (getState() as RootState).pages;
|
||||||
|
|
||||||
|
const page = pageMap[id];
|
||||||
|
|
||||||
|
if (!page) return;
|
||||||
|
|
||||||
|
dispatch(currentUserActions.setLatestView(page));
|
||||||
|
await setLatestOpenedPage(id);
|
||||||
|
});
|
||||||
|
@ -56,6 +56,7 @@ export interface PageState {
|
|||||||
pageMap: Record<string, Page>;
|
pageMap: Record<string, Page>;
|
||||||
relationMap: Record<string, string[] | undefined>;
|
relationMap: Record<string, string[] | undefined>;
|
||||||
expandedIdMap: Record<string, boolean>;
|
expandedIdMap: Record<string, boolean>;
|
||||||
|
showTrashSnackbar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialState: PageState = {
|
export const initialState: PageState = {
|
||||||
@ -65,6 +66,7 @@ export const initialState: PageState = {
|
|||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, boolean>),
|
}, {} as Record<string, boolean>),
|
||||||
|
showTrashSnackbar: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pagesSlice = createSlice({
|
export const pagesSlice = createSlice({
|
||||||
@ -201,6 +203,10 @@ export const pagesSlice = createSlice({
|
|||||||
|
|
||||||
storeExpandedPageIds(ids);
|
storeExpandedPageIds(ids);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setTrashSnackbar(state, action: PayloadAction<boolean>) {
|
||||||
|
state.showTrashSnackbar = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Do not edit directly
|
* Do not edit directly
|
||||||
* Generated on Mon, 29 Jan 2024 03:52:24 GMT
|
* Generated on Tue, 19 Mar 2024 03:48:58 GMT
|
||||||
* Generated from $pnpm css:variables
|
* Generated from $pnpm css:variables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -36,7 +36,7 @@
|
|||||||
--base-light-color-light-green: #ddffd6;
|
--base-light-color-light-green: #ddffd6;
|
||||||
--base-light-color-light-aqua: #defff1;
|
--base-light-color-light-aqua: #defff1;
|
||||||
--base-light-color-light-blue: #e1fbff;
|
--base-light-color-light-blue: #e1fbff;
|
||||||
--base-light-color-light-red: #ffe7ee;
|
--base-light-color-light-red: #ffdddd;
|
||||||
--base-black-neutral-100: #252F41;
|
--base-black-neutral-100: #252F41;
|
||||||
--base-black-neutral-200: #313c51;
|
--base-black-neutral-200: #313c51;
|
||||||
--base-black-neutral-300: #3c4557;
|
--base-black-neutral-300: #3c4557;
|
||||||
@ -88,7 +88,7 @@
|
|||||||
--fill-hover: #005174;
|
--fill-hover: #005174;
|
||||||
--fill-toolbar: #0F111C;
|
--fill-toolbar: #0F111C;
|
||||||
--fill-selector: #232b38;
|
--fill-selector: #232b38;
|
||||||
--fill-list-active: #252F41;
|
--fill-list-active: #3c4557;
|
||||||
--fill-list-hover: #005174;
|
--fill-list-hover: #005174;
|
||||||
--content-blue-400: #00bcf0;
|
--content-blue-400: #00bcf0;
|
||||||
--content-blue-300: #52d1f4;
|
--content-blue-300: #52d1f4;
|
||||||
@ -116,4 +116,6 @@
|
|||||||
--tint-aqua: #1B3849;
|
--tint-aqua: #1B3849;
|
||||||
--tint-orange: #5E3C3C;
|
--tint-orange: #5E3C3C;
|
||||||
--shadow: 0px 0px 25px 0px rgba(0,0,0,0.3);
|
--shadow: 0px 0px 25px 0px rgba(0,0,0,0.3);
|
||||||
|
--scrollbar-track: #252F41;
|
||||||
|
--scrollbar-thumb: #3c4557;
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Do not edit directly
|
* Do not edit directly
|
||||||
* Generated on Mon, 29 Jan 2024 03:52:24 GMT
|
* Generated on Tue, 19 Mar 2024 03:48:58 GMT
|
||||||
* Generated from $pnpm css:variables
|
* Generated from $pnpm css:variables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -36,7 +36,7 @@
|
|||||||
--base-light-color-light-green: #ddffd6;
|
--base-light-color-light-green: #ddffd6;
|
||||||
--base-light-color-light-aqua: #defff1;
|
--base-light-color-light-aqua: #defff1;
|
||||||
--base-light-color-light-blue: #e1fbff;
|
--base-light-color-light-blue: #e1fbff;
|
||||||
--base-light-color-light-red: #ffe7ee;
|
--base-light-color-light-red: #ffdddd;
|
||||||
--base-black-neutral-100: #252F41;
|
--base-black-neutral-100: #252F41;
|
||||||
--base-black-neutral-200: #313c51;
|
--base-black-neutral-200: #313c51;
|
||||||
--base-black-neutral-300: #3c4557;
|
--base-black-neutral-300: #3c4557;
|
||||||
@ -92,7 +92,7 @@
|
|||||||
--fill-active: #e0f8ff;
|
--fill-active: #e0f8ff;
|
||||||
--fill-list-hover: #e0f8ff;
|
--fill-list-hover: #e0f8ff;
|
||||||
--fill-list-active: #edeef2;
|
--fill-list-active: #edeef2;
|
||||||
--content-blue-400: rgb(0, 188, 240);
|
--content-blue-400: #00bcf0;
|
||||||
--content-blue-300: #52d1f4;
|
--content-blue-300: #52d1f4;
|
||||||
--content-blue-600: #009fd1;
|
--content-blue-600: #009fd1;
|
||||||
--content-blue-100: #e0f8ff;
|
--content-blue-100: #e0f8ff;
|
||||||
@ -119,4 +119,6 @@
|
|||||||
--tint-orange: #ffefe3;
|
--tint-orange: #ffefe3;
|
||||||
--tint-yellow: #fff2cd;
|
--tint-yellow: #fff2cd;
|
||||||
--shadow: 0px 0px 10px 0px rgba(0,0,0,0.1);
|
--shadow: 0px 0px 10px 0px rgba(0,0,0,0.1);
|
||||||
|
--scrollbar-thumb: #bdbdbd;
|
||||||
|
--scrollbar-track: #edeef2;
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Do not edit directly
|
* Do not edit directly
|
||||||
* Generated on Mon, 29 Jan 2024 03:52:24 GMT
|
* Generated on Tue, 19 Mar 2024 03:48:58 GMT
|
||||||
* Generated from $pnpm css:variables
|
* Generated from $pnpm css:variables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Do not edit directly
|
* Do not edit directly
|
||||||
* Generated on Mon, 29 Jan 2024 03:52:24 GMT
|
* Generated on Tue, 19 Mar 2024 03:48:58 GMT
|
||||||
* Generated from $pnpm css:variables
|
* Generated from $pnpm css:variables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -67,5 +67,9 @@ module.exports = {
|
|||||||
"lime": "var(--tint-lime)",
|
"lime": "var(--tint-lime)",
|
||||||
"aqua": "var(--tint-aqua)",
|
"aqua": "var(--tint-aqua)",
|
||||||
"orange": "var(--tint-orange)"
|
"orange": "var(--tint-orange)"
|
||||||
|
},
|
||||||
|
"scrollbar": {
|
||||||
|
"track": "var(--scrollbar-track)",
|
||||||
|
"thumb": "var(--scrollbar-thumb)"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -80,7 +80,7 @@
|
|||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"active": {
|
"active": {
|
||||||
"value": "{Base.black.neutral.100}",
|
"value": "{Base.black.neutral.300}",
|
||||||
"type": "color"
|
"type": "color"
|
||||||
},
|
},
|
||||||
"hover": {
|
"hover": {
|
||||||
@ -207,5 +207,15 @@
|
|||||||
"type": "innerShadow"
|
"type": "innerShadow"
|
||||||
},
|
},
|
||||||
"type": "boxShadow"
|
"type": "boxShadow"
|
||||||
|
},
|
||||||
|
"scrollbar": {
|
||||||
|
"track": {
|
||||||
|
"value": "{Base.black.neutral.100}",
|
||||||
|
"type": "color"
|
||||||
|
},
|
||||||
|
"thumb": {
|
||||||
|
"value": "{Base.black.neutral.300}",
|
||||||
|
"type": "color"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -219,5 +219,15 @@
|
|||||||
"type": "dropShadow"
|
"type": "dropShadow"
|
||||||
},
|
},
|
||||||
"type": "boxShadow"
|
"type": "boxShadow"
|
||||||
|
},
|
||||||
|
"scrollbar": {
|
||||||
|
"thumb": {
|
||||||
|
"value": "{Base.Light.neutral.500}",
|
||||||
|
"type": "color"
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"value": "{Base.Light.neutral.100}",
|
||||||
|
"type": "color"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user