mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: support publish tauri app (#4829)
* chore: support publish tauri app * chore: fixed others bugs * fix: code review * fix: code review * fix: tauri ci
This commit is contained in:
parent
0f13f86917
commit
a180cfcdc2
77
.github/workflows/tauri_ci.yaml
vendored
77
.github/workflows/tauri_ci.yaml
vendored
@ -25,32 +25,33 @@ jobs:
|
||||
platform: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Cache Rust Dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
key: rust-dependencies-${{ runner.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_tauri/src-tauri
|
||||
|
||||
- name: Cache Node.js dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ runner.os }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_tauri/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
@ -60,15 +61,23 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
|
||||
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_tauri/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
vcpkg integrate install
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
npm install -g pnpm@${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
@ -76,35 +85,29 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
cargo install --force cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
npm install -g pnpm@${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: install dependencies (macOS only)
|
||||
if: matrix.platform == 'macos-latest'
|
||||
- name: install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
npm install -g pnpm@${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Build
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- name: frontend tests and linting
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
pnpm test
|
||||
pnpm test:errors
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
run: |
|
||||
diff_files=$(git status --porcelain)
|
||||
if [ -n "$diff_files" ]; then
|
||||
echo "There are uncommitted changes in the working tree. Please commit them before pushing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
153
.github/workflows/tauri_release.yml
vendored
Normal file
153
.github/workflows/tauri_release.yml
vendored
Normal file
@ -0,0 +1,153 @@
|
||||
name: Publish Tauri Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'The branch to release'
|
||||
required: true
|
||||
default: 'main'
|
||||
version:
|
||||
description: 'The version to release'
|
||||
required: true
|
||||
default: '0.0.0'
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.75"
|
||||
|
||||
jobs:
|
||||
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- platform: windows-latest
|
||||
args: "--verbose"
|
||||
target: "windows-x86_64"
|
||||
- platform: macos-latest
|
||||
args: "--target x86_64-apple-darwin"
|
||||
target: "macos-x86_64"
|
||||
- platform: ubuntu-latest
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
target: "linux-x86_64"
|
||||
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force duckscript_cli
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }}
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- uses: tauri-apps/tauri-action@dev
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }}
|
||||
APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }}
|
||||
CI: true
|
||||
with:
|
||||
args: ${{ matrix.settings.args }}
|
||||
appVersion: ${{ github.event.inputs.version }}
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
|
||||
- name: Upload EXE package(windows only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.exe
|
||||
path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe
|
||||
|
||||
- name: Upload DMG package(macos only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'macos-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.dmg
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg
|
||||
|
||||
- name: Upload Deb package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.deb
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
||||
|
||||
- name: Upload AppImage package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
@ -79,8 +79,8 @@
|
||||
"yjs": "^13.5.51"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/plugin-svgo": "^8.0.1",
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"@svgr/plugin-svgo": "^8.0.1",
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"@types/is-hotkey": "^0.1.7",
|
||||
"@types/jest": "^29.5.3",
|
||||
|
31
frontend/appflowy_tauri/scripts/update_version.cjs
Normal file
31
frontend/appflowy_tauri/scripts/update_version.cjs
Normal file
@ -0,0 +1,31 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error('Usage: node update-tauri-version.js <version>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const newVersion = process.argv[2];
|
||||
|
||||
const tauriConfigPath = path.join(__dirname, '../src-tauri', 'tauri.conf.json');
|
||||
|
||||
fs.readFile(tauriConfigPath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error reading tauri.conf.json:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = JSON.parse(data);
|
||||
|
||||
config.package.version = newVersion;
|
||||
|
||||
fs.writeFile(tauriConfigPath, JSON.stringify(config, null, 2), 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error('Error writing tauri.conf.json:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Tauri version updated to ${newVersion} successfully.`);
|
||||
});
|
||||
});
|
@ -60,6 +60,7 @@ function DeleteConfirmDialog({ open, title, onOk, onCancel, onClose, okText, can
|
||||
<Button
|
||||
className={'w-full'}
|
||||
variant={'outlined'}
|
||||
color={'inherit'}
|
||||
onClick={() => {
|
||||
onCancel?.();
|
||||
onClose();
|
||||
|
@ -67,7 +67,7 @@ function RenameDialog({
|
||||
</DialogContent>
|
||||
<Divider className={'mb-1'} />
|
||||
<DialogActions className={'mb-1 px-4'}>
|
||||
<Button variant={'outlined'} onClick={onClose}>
|
||||
<Button color={'inherit'} variant={'outlined'} onClick={onClose}>
|
||||
{t('button.cancel')}
|
||||
</Button>
|
||||
<Button variant={'contained'} onClick={onDone}>
|
||||
|
@ -49,6 +49,7 @@ export interface KeyboardNavigationProps<T> {
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
itemClassName?: string;
|
||||
itemStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
function KeyboardNavigation<T>({
|
||||
@ -67,6 +68,7 @@ function KeyboardNavigation<T>({
|
||||
onBlur,
|
||||
onFocus,
|
||||
itemClassName,
|
||||
itemStyle,
|
||||
}: KeyboardNavigationProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -232,6 +234,7 @@ function KeyboardNavigation<T>({
|
||||
}
|
||||
}}
|
||||
selected={isFocused}
|
||||
style={itemStyle}
|
||||
className={`ml-0 flex w-full items-center justify-start rounded-none px-2 py-1 text-xs ${
|
||||
!isFocused ? 'hover:bg-transparent' : ''
|
||||
} ${itemClassName ?? ''}`}
|
||||
@ -246,7 +249,7 @@ function KeyboardNavigation<T>({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[itemClassName, focusedKey, onConfirm, onFocus]
|
||||
[itemClassName, focusedKey, onConfirm, onFocus, itemStyle]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -284,10 +287,16 @@ function KeyboardNavigation<T>({
|
||||
onBlur={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const target = e.relatedTarget as HTMLElement;
|
||||
|
||||
if (target?.closest('.keyboard-navigation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
onBlur?.();
|
||||
}}
|
||||
autoFocus={!disableFocus}
|
||||
className={'flex w-full flex-col gap-1 outline-none'}
|
||||
className={'keyboard-navigation flex w-full flex-col gap-1 outline-none'}
|
||||
ref={ref}
|
||||
>
|
||||
{options.length > 0 ? (
|
||||
|
@ -22,7 +22,7 @@ function ViewBanner({
|
||||
<div className={'view-banner flex w-full flex-col'}>
|
||||
{showCover && cover && <ViewCover cover={cover} onUpdateCover={onUpdateCover} />}
|
||||
|
||||
<div className={'relative min-h-[65px] px-16 pt-4'}>
|
||||
<div className={`relative min-h-[65px] ${showCover ? 'px-16' : ''} pt-4`}>
|
||||
<div
|
||||
style={{
|
||||
display: icon ? 'flex' : 'none',
|
||||
|
@ -22,6 +22,9 @@ function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon
|
||||
const onEmojiSelect = useCallback(
|
||||
(emoji: string) => {
|
||||
onUpdateIcon(emoji);
|
||||
if (!emoji) {
|
||||
setAnchorPosition(undefined);
|
||||
}
|
||||
},
|
||||
[onUpdateIcon]
|
||||
);
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { t } from 'i18next';
|
||||
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useLogin } from '$app/components/auth/get_started/useLogin';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const GetStarted = () => {
|
||||
const { onAutoSignInClick } = useLogin();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -195,8 +195,8 @@ export const useConnectDatabase = (viewId: string) => {
|
||||
return database;
|
||||
};
|
||||
|
||||
const DatabaseRenderedContext = createContext<() => void>(() => {
|
||||
// do nothing
|
||||
const DatabaseRenderedContext = createContext<(viewId: string) => void>(() => {
|
||||
return;
|
||||
});
|
||||
|
||||
export const DatabaseRenderedProvider = DatabaseRenderedContext.Provider;
|
||||
|
@ -58,12 +58,14 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
||||
}
|
||||
}, [viewId]);
|
||||
|
||||
const parentId = page?.parentId;
|
||||
|
||||
useEffect(() => {
|
||||
void handleGetPage();
|
||||
void handleResetDatabaseViews(viewId);
|
||||
const unsubscribePromise = subscribeNotifications(
|
||||
{
|
||||
const unsubscribePromise = subscribeNotifications({
|
||||
[FolderNotification.DidUpdateView]: (changeset) => {
|
||||
if (changeset.parent_view_id !== viewId && changeset.id !== viewId) return;
|
||||
setChildViews((prev) => {
|
||||
const index = prev.findIndex((view) => view.id === changeset.id);
|
||||
|
||||
@ -82,41 +84,17 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
||||
});
|
||||
},
|
||||
[FolderNotification.DidUpdateChildViews]: (changeset) => {
|
||||
if (changeset.parent_view_id !== viewId && changeset.parent_view_id !== parentId) return;
|
||||
if (changeset.create_child_views.length === 0 && changeset.delete_child_views.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void handleResetDatabaseViews(viewId);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: viewId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return () => void unsubscribePromise.then((unsubscribe) => unsubscribe());
|
||||
}, [handleGetPage, handleResetDatabaseViews, viewId]);
|
||||
|
||||
useEffect(() => {
|
||||
const parentId = page?.parentId;
|
||||
|
||||
if (!parentId) return;
|
||||
|
||||
const unsubscribePromise = subscribeNotifications(
|
||||
{
|
||||
[FolderNotification.DidUpdateChildViews]: (changeset) => {
|
||||
if (changeset.delete_child_views.includes(viewId)) {
|
||||
setNotFound(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: parentId,
|
||||
}
|
||||
);
|
||||
|
||||
return () => void unsubscribePromise.then((unsubscribe) => unsubscribe());
|
||||
}, [page, viewId]);
|
||||
}, [handleGetPage, handleResetDatabaseViews, viewId, parentId]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return Math.max(
|
||||
@ -183,7 +161,13 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
|
||||
index={value}
|
||||
>
|
||||
{childViews.map((view, index) => (
|
||||
<TabPanel className={'flex h-full w-full flex-col'} key={view.id} index={index} value={value}>
|
||||
<TabPanel
|
||||
data-view-id={view.id}
|
||||
className={'flex h-full w-full flex-col'}
|
||||
key={view.id}
|
||||
index={index}
|
||||
value={value}
|
||||
>
|
||||
<DatabaseLoader viewId={view.id}>
|
||||
{selectedViewId === view.id && (
|
||||
<>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { FormEventHandler, useCallback } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const DatabaseTitle = () => {
|
||||
const viewId = useViewId();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const pageName = useAppSelector((state) => state.pages.pageMap[viewId]?.name || '');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -9,12 +9,12 @@ import Popover from '@mui/material/Popover';
|
||||
|
||||
const initialAnchorOrigin: PopoverOrigin = {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
horizontal: 'center',
|
||||
};
|
||||
|
||||
const initialTransformOrigin: PopoverOrigin = {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
horizontal: 'center',
|
||||
};
|
||||
const SelectCellActions = lazy(
|
||||
() => import('$app/components/database/components/field_types/select/select_cell_actions/SelectCellActions')
|
||||
|
@ -18,7 +18,7 @@ function DatabaseSettings(props: Props) {
|
||||
<div className='flex h-[39px] items-center gap-2 border-b border-line-divider'>
|
||||
<FilterSettings {...props} />
|
||||
<SortSettings {...props} />
|
||||
<TextButton color='inherit' onClick={(e) => setSettingAnchorEl(e.currentTarget)}>
|
||||
<TextButton className={'min-w-fit'} color='inherit' onClick={(e) => setSettingAnchorEl(e.currentTarget)}>
|
||||
{t('settings.title')}
|
||||
</TextButton>
|
||||
<SettingsMenu
|
||||
|
@ -23,7 +23,7 @@ function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextButton onClick={handleClick} color={highlight ? 'primary' : 'inherit'}>
|
||||
<TextButton className={'min-w-fit'} onClick={handleClick} color={highlight ? 'primary' : 'inherit'}>
|
||||
{t('grid.settings.filter')}
|
||||
</TextButton>
|
||||
<FilterFieldsMenu
|
||||
|
@ -31,7 +31,7 @@ function SortSettings({ onToggleCollection }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextButton className={'p-1'} color={highlight ? 'primary' : 'inherit'} onClick={handleClick}>
|
||||
<TextButton className={'min-w-fit p-1'} color={highlight ? 'primary' : 'inherit'} onClick={handleClick}>
|
||||
{t('grid.settings.sort')}
|
||||
</TextButton>
|
||||
<SortFieldsMenu
|
||||
|
@ -5,6 +5,7 @@ import dayjs from 'dayjs';
|
||||
import { ReactComponent as LeftSvg } from '$app/assets/arrow-left.svg';
|
||||
import { ReactComponent as RightSvg } from '$app/assets/arrow-right.svg';
|
||||
import { IconButton } from '@mui/material';
|
||||
import './calendar.scss';
|
||||
|
||||
function CustomCalendar({
|
||||
handleChange,
|
||||
|
@ -0,0 +1,82 @@
|
||||
|
||||
.react-datepicker__month-container {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
.react-datepicker__header {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border-bottom: 0;
|
||||
|
||||
}
|
||||
.react-datepicker__day-names {
|
||||
border: none;
|
||||
}
|
||||
.react-datepicker__day-name {
|
||||
color: var(--text-caption);
|
||||
}
|
||||
.react-datepicker__month {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.react-datepicker__day {
|
||||
border: none;
|
||||
color: var(--text-title);
|
||||
border-radius: 100%;
|
||||
}
|
||||
.react-datepicker__day:hover {
|
||||
border-radius: 100%;
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
.react-datepicker__day--outside-month {
|
||||
color: var(--text-caption);
|
||||
}
|
||||
.react-datepicker__day--in-range {
|
||||
background: var(--fill-hover);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
|
||||
.react-datepicker__day--today {
|
||||
border: 1px solid var(--fill-default);
|
||||
color: var(--text-title);
|
||||
border-radius: 100%;
|
||||
background: transparent;
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
.react-datepicker__day--today:hover{
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
.react-datepicker__day--in-selecting-range, .react-datepicker__day--today.react-datepicker__day--in-range {
|
||||
background: var(--fill-hover);
|
||||
color: var(--content-on-fill);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.react-datepicker__day--keyboard-selected {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected {
|
||||
&.react-datepicker__day--today {
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
background: var(--fill-default) !important;
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected:hover {
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
.react-swipeable-view-container {
|
||||
height: 100%;
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { FC, useMemo, useRef, useState } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { Divider, ListSubheader, MenuItem, MenuList, MenuProps, OutlinedInput } from '@mui/material';
|
||||
import { SelectOptionColorPB } from '@/services/backend';
|
||||
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
|
||||
@ -14,6 +13,7 @@ import {
|
||||
import { useViewId } from '$app/hooks';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SelectOptionMenuProps {
|
||||
fieldId: string;
|
||||
@ -34,6 +34,7 @@ const Colors = [
|
||||
];
|
||||
|
||||
export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, option, MenuProps: menuProps }) => {
|
||||
const { t } = useTranslation();
|
||||
const [tagName, setTagName] = useState(option.name);
|
||||
const viewId = useViewId();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -83,6 +84,9 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
|
||||
horizontal: -32,
|
||||
}}
|
||||
{...menuProps}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClose={onClose}
|
||||
onMouseDown={(e) => {
|
||||
const isInput = inputRef.current?.contains(e.target as Node);
|
||||
@ -95,6 +99,9 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
|
||||
<ListSubheader className='my-2 leading-tight'>
|
||||
<OutlinedInput
|
||||
inputRef={inputRef}
|
||||
spellCheck={false}
|
||||
autoCorrect={'off'}
|
||||
autoCapitalize={'off'}
|
||||
value={tagName}
|
||||
onChange={(e) => {
|
||||
setTagName(e.target.value);
|
||||
@ -108,6 +115,9 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
autoFocus={true}
|
||||
placeholder={t('grid.selectOption.tagName')}
|
||||
size='small'
|
||||
@ -139,6 +149,7 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
|
||||
}}
|
||||
key={color}
|
||||
value={color}
|
||||
className={'px-1.5'}
|
||||
>
|
||||
<span className={`mr-2 inline-flex h-4 w-4 rounded-full ${SelectOptionColorMap[color]}`} />
|
||||
<span className='flex-1'>{t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)}</span>
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { MenuItem, MenuItemProps } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { Tag } from '../Tag';
|
||||
|
||||
export interface CreateOptionProps {
|
||||
label: React.ReactNode;
|
||||
onClick?: MenuItemProps['onClick'];
|
||||
}
|
||||
|
||||
export const CreateOption: FC<CreateOptionProps> = ({ label, onClick }) => {
|
||||
return (
|
||||
<MenuItem className='px-2' onClick={onClick}>
|
||||
<Tag className='ml-2' size='small' label={label} />
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
@ -1,18 +1,17 @@
|
||||
import React, { FormEvent, useCallback } from 'react';
|
||||
import { OutlinedInput } from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function SearchInput({
|
||||
setNewOptionName,
|
||||
newOptionName,
|
||||
onEnter,
|
||||
onEscape,
|
||||
inputRef,
|
||||
}: {
|
||||
newOptionName: string;
|
||||
setNewOptionName: (value: string) => void;
|
||||
onEnter: () => void;
|
||||
onEscape?: () => void;
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const handleInput = useCallback(
|
||||
(event: FormEvent) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
@ -27,20 +26,10 @@ function SearchInput({
|
||||
size='small'
|
||||
className={'mx-4'}
|
||||
autoFocus={true}
|
||||
inputRef={inputRef}
|
||||
value={newOptionName}
|
||||
onInput={handleInput}
|
||||
spellCheck={false}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onEnter();
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onEscape?.();
|
||||
}
|
||||
}}
|
||||
placeholder={t('grid.selectOption.searchOrCreateOption')}
|
||||
/>
|
||||
);
|
||||
|
@ -1,7 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { MenuItem } from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
import { CreateOption } from '$app/components/database/components/field_types/select/select_cell_actions/CreateOption';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { SelectOptionItem } from '$app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem';
|
||||
import { cellService, SelectCell as SelectCellType, SelectField, SelectTypeOption } from '$app/application/database';
|
||||
import { useViewId } from '$app/hooks';
|
||||
@ -12,6 +9,13 @@ import {
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { useTypeOption } from '$app/components/database';
|
||||
import SearchInput from './SearchInput';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import KeyboardNavigation, {
|
||||
KeyboardNavigationOption,
|
||||
} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||
import { Tag } from '$app/components/database/components/field_types/select/Tag';
|
||||
|
||||
const CREATE_OPTION_KEY = 'createOption';
|
||||
|
||||
function SelectCellActions({
|
||||
field,
|
||||
@ -24,22 +28,43 @@ function SelectCellActions({
|
||||
onUpdated?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const rowId = cell?.rowId;
|
||||
const viewId = useViewId();
|
||||
const typeOption = useTypeOption<SelectTypeOption>(field.id);
|
||||
const options = useMemo(() => typeOption.options ?? [], [typeOption.options]);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const selectedOptionIds = useMemo(() => cell?.data?.selectedOptionIds ?? [], [cell]);
|
||||
const [newOptionName, setNewOptionName] = useState('');
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
options.filter((option) => {
|
||||
return option.name.toLowerCase().includes(newOptionName.toLowerCase());
|
||||
}),
|
||||
[options, newOptionName]
|
||||
);
|
||||
|
||||
const shouldCreateOption = !!newOptionName && filteredOptions.length === 0;
|
||||
const filteredOptions: KeyboardNavigationOption[] = useMemo(() => {
|
||||
const result = options
|
||||
.filter((option) => {
|
||||
return option.name.toLowerCase().includes(newOptionName.toLowerCase());
|
||||
})
|
||||
.map((option) => ({
|
||||
key: option.id,
|
||||
content: (
|
||||
<SelectOptionItem
|
||||
isSelected={selectedOptionIds?.includes(option.id)}
|
||||
fieldId={cell?.fieldId || ''}
|
||||
option={option}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
if (result.length === 0) {
|
||||
result.push({
|
||||
key: CREATE_OPTION_KEY,
|
||||
content: <Tag size='small' label={newOptionName} />,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [newOptionName, options, selectedOptionIds, cell?.fieldId]);
|
||||
|
||||
const shouldCreateOption = filteredOptions.length === 1 && filteredOptions[0].key === 'createOption';
|
||||
|
||||
const updateCell = useCallback(
|
||||
async (optionIds: string[]) => {
|
||||
@ -65,90 +90,67 @@ function SelectCellActions({
|
||||
return option;
|
||||
}, [viewId, field.id, newOptionName]);
|
||||
|
||||
const handleClickOption = useCallback(
|
||||
(optionId: string) => {
|
||||
const onConfirm = useCallback(
|
||||
async (key: string) => {
|
||||
let optionId = key;
|
||||
|
||||
if (key === CREATE_OPTION_KEY) {
|
||||
const option = await createOption();
|
||||
|
||||
optionId = option?.id || '';
|
||||
}
|
||||
|
||||
if (!optionId) return;
|
||||
|
||||
if (field.type === FieldType.SingleSelect) {
|
||||
void updateCell([optionId]);
|
||||
const newOptionIds = [optionId];
|
||||
|
||||
if (selectedOptionIds?.includes(optionId)) {
|
||||
newOptionIds.pop();
|
||||
}
|
||||
|
||||
void updateCell(newOptionIds);
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = selectedOptionIds;
|
||||
let newOptionIds = [];
|
||||
|
||||
if (!prev) {
|
||||
if (!selectedOptionIds) {
|
||||
newOptionIds.push(optionId);
|
||||
} else {
|
||||
const isSelected = prev.includes(optionId);
|
||||
const isSelected = selectedOptionIds.includes(optionId);
|
||||
|
||||
if (isSelected) {
|
||||
newOptionIds = prev.filter((id) => id !== optionId);
|
||||
newOptionIds = selectedOptionIds.filter((id) => id !== optionId);
|
||||
} else {
|
||||
newOptionIds = [...prev, optionId];
|
||||
newOptionIds = [...selectedOptionIds, optionId];
|
||||
}
|
||||
}
|
||||
|
||||
void updateCell(newOptionIds);
|
||||
},
|
||||
[field.type, selectedOptionIds, updateCell]
|
||||
[createOption, field.type, selectedOptionIds, updateCell]
|
||||
);
|
||||
|
||||
const handleNewTagClick = useCallback(async () => {
|
||||
if (!cell || !rowId) return;
|
||||
const option = await createOption();
|
||||
|
||||
if (!option) return;
|
||||
handleClickOption(option.id);
|
||||
}, [cell, createOption, handleClickOption, rowId]);
|
||||
|
||||
const handleEnter = useCallback(() => {
|
||||
if (shouldCreateOption) {
|
||||
void handleNewTagClick();
|
||||
} else {
|
||||
if (field.type === FieldType.SingleSelect) {
|
||||
const firstOption = filteredOptions[0];
|
||||
|
||||
if (!firstOption) return;
|
||||
|
||||
void updateCell([firstOption.id]);
|
||||
} else {
|
||||
void updateCell(filteredOptions.map((option) => option.id));
|
||||
}
|
||||
}
|
||||
|
||||
setNewOptionName('');
|
||||
}, [field.type, filteredOptions, handleNewTagClick, shouldCreateOption, updateCell]);
|
||||
|
||||
return (
|
||||
<div className={'flex h-full flex-col overflow-hidden'}>
|
||||
<SearchInput
|
||||
onEscape={onClose}
|
||||
setNewOptionName={setNewOptionName}
|
||||
newOptionName={newOptionName}
|
||||
onEnter={handleEnter}
|
||||
/>
|
||||
<SearchInput inputRef={inputRef} setNewOptionName={setNewOptionName} newOptionName={newOptionName} />
|
||||
|
||||
<div className='mx-4 mb-2 mt-4 text-xs'>
|
||||
{shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')}
|
||||
</div>
|
||||
<div className={'mx-1 flex-1 overflow-y-auto overflow-x-hidden'}>
|
||||
{shouldCreateOption ? (
|
||||
<CreateOption label={newOptionName} onClick={handleNewTagClick} />
|
||||
) : (
|
||||
<div className={' px-2'}>
|
||||
{filteredOptions.map((option) => (
|
||||
<MenuItem className={'px-2'} key={option.id} value={option.id}>
|
||||
<SelectOptionItem
|
||||
onClick={() => {
|
||||
handleClickOption(option.id);
|
||||
<div ref={scrollRef} className={'mx-1 flex-1 overflow-y-auto overflow-x-hidden px-1'}>
|
||||
<KeyboardNavigation
|
||||
scrollRef={scrollRef}
|
||||
focusRef={inputRef}
|
||||
options={filteredOptions}
|
||||
disableFocus={true}
|
||||
onConfirm={onConfirm}
|
||||
onEscape={onClose}
|
||||
itemStyle={{
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
isSelected={selectedOptionIds?.includes(option.id)}
|
||||
fieldId={cell?.fieldId || ''}
|
||||
option={option}
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,10 +10,9 @@ export interface SelectOptionItemProps {
|
||||
option: SelectOption;
|
||||
fieldId: string;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const SelectOptionItem: FC<SelectOptionItemProps> = ({ onClick, isSelected, fieldId, option }) => {
|
||||
export const SelectOptionItem: FC<SelectOptionItemProps> = ({ isSelected, fieldId, option }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const anchorEl = useRef<HTMLDivElement | null>(null);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
@ -25,7 +24,6 @@ export const SelectOptionItem: FC<SelectOptionItemProps> = ({ onClick, isSelecte
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={onClick}
|
||||
ref={anchorEl}
|
||||
className={'flex w-full items-center justify-between'}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
|
@ -15,12 +15,12 @@ export interface FieldProps {
|
||||
|
||||
const initialAnchorOrigin: PopoverOrigin = {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
horizontal: 'right',
|
||||
};
|
||||
|
||||
const initialTransformOrigin: PopoverOrigin = {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
horizontal: 'center',
|
||||
};
|
||||
|
||||
export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOpened }) => {
|
||||
@ -54,7 +54,7 @@ export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOp
|
||||
}, [menuOpened]);
|
||||
|
||||
const { paperHeight, paperWidth, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({
|
||||
initialPaperWidth: 369,
|
||||
initialPaperWidth: 300,
|
||||
initialPaperHeight: 400,
|
||||
anchorPosition,
|
||||
initialAnchorOrigin,
|
||||
@ -81,7 +81,7 @@ export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOp
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxHeight: paperHeight,
|
||||
maxWidth: paperWidth,
|
||||
width: paperWidth,
|
||||
height: 'auto',
|
||||
},
|
||||
className: 'flex h-full flex-col overflow-hidden',
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { t } from 'i18next';
|
||||
import { FC, useMemo, useRef, useState } from 'react';
|
||||
import { SortConditionPB } from '@/services/backend';
|
||||
import KeyboardNavigation, {
|
||||
@ -6,11 +5,13 @@ import KeyboardNavigation, {
|
||||
} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
|
||||
import { Popover } from '@mui/material';
|
||||
import { ReactComponent as DropDownSvg } from '$app/assets/more.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const SortConditionSelect: FC<{
|
||||
onChange?: (value: SortConditionPB) => void;
|
||||
value?: SortConditionPB;
|
||||
}> = ({ onChange, value }) => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleClose = () => {
|
||||
@ -28,7 +29,7 @@ export const SortConditionSelect: FC<{
|
||||
content: t('grid.sort.descending'),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const onConfirm = (optionKey: SortConditionPB) => {
|
||||
onChange?.(optionKey);
|
||||
|
@ -84,6 +84,7 @@ function ViewActions({ view, pageId, ...props }: { pageId: string; view: Page }
|
||||
updatePageName({
|
||||
id: viewId,
|
||||
name: val,
|
||||
immediate: true,
|
||||
})
|
||||
);
|
||||
setOpenRenameDialog(false);
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useDatabaseVisibilityRows } from '$app/components/database';
|
||||
import { Field } from '$app/application/database';
|
||||
import { DEFAULT_FIELD_WIDTH, GRID_ACTIONS_WIDTH } from '$app/components/database/grid/constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
field: Field;
|
||||
@ -13,6 +14,7 @@ export function GridCalculate({ field, index }: Props) {
|
||||
const rowMetas = useDatabaseVisibilityRows();
|
||||
const count = rowMetas.length;
|
||||
const width = index === 0 ? GRID_ACTIONS_WIDTH : field.width ?? DEFAULT_FIELD_WIDTH;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -23,7 +25,7 @@ export function GridCalculate({ field, index }: Props) {
|
||||
>
|
||||
{field.isPrimary ? (
|
||||
<>
|
||||
<span className={'mr-2 text-text-caption'}>Count</span>
|
||||
<span className={'mr-2 text-text-caption'}>{t('grid.calculationTypeLabel.count')}</span>
|
||||
<span>{count}</span>
|
||||
</>
|
||||
) : null}
|
||||
|
@ -133,6 +133,10 @@ export const GridField: FC<GridFieldProps> = memo(
|
||||
className='relative flex h-full w-full items-center rounded-none px-0'
|
||||
disableRipple
|
||||
onContextMenu={(event) => {
|
||||
if (propertyMenuOpened || menuOpened || open) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleClick();
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { rowService } from '$app/application/database';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { t } from 'i18next';
|
||||
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
@ -15,6 +15,7 @@ const CSS_HIGHLIGHT_PROPERTY = 'bg-content-blue-50';
|
||||
function GridNewRow({ index, groupId, getContainerRef }: Props) {
|
||||
const viewId = useViewId();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleClick = useCallback(() => {
|
||||
void rowService.createRow(viewId, {
|
||||
groupId,
|
||||
|
@ -10,6 +10,7 @@ import { useGridColumn, useGridRow } from './GridTable.hooks';
|
||||
import GridStickyHeader from '$app/components/database/grid/grid_sticky_header/GridStickyHeader';
|
||||
import GridTableOverlay from '$app/components/database/grid/grid_overlay/GridTableOverlay';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useViewId } from '$app/hooks';
|
||||
|
||||
export interface GridTableProps {
|
||||
onEditRecord: (rowId: string) => void;
|
||||
@ -30,6 +31,7 @@ export const GridTable: FC<GridTableProps> = React.memo(({ onEditRecord }) => {
|
||||
columns,
|
||||
ref as React.MutableRefObject<Grid<GridColumn[] | { columns: GridColumn[]; renderRows: RenderRow[] }> | null>
|
||||
);
|
||||
const viewId = useViewId();
|
||||
const { rowHeight } = useGridRow();
|
||||
const onRendered = useDatabaseRendered();
|
||||
|
||||
@ -139,7 +141,7 @@ export const GridTable: FC<GridTableProps> = React.memo(({ onEditRecord }) => {
|
||||
className={'grid-scroll-container'}
|
||||
outerRef={(el) => {
|
||||
scrollElementRef.current = el;
|
||||
onRendered();
|
||||
onRendered(viewId);
|
||||
}}
|
||||
innerRef={containerRef}
|
||||
>
|
||||
|
@ -3,8 +3,19 @@ import { ViewLayoutPB } from '@/services/backend';
|
||||
|
||||
export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) {
|
||||
const list = useAppSelector((state) => {
|
||||
const workspaces = state.workspace.workspaces.map((item) => item.id) ?? [];
|
||||
|
||||
return Object.values(state.pages.pageMap).filter((page) => {
|
||||
if (page.layout !== layout) return false;
|
||||
const parentId = page.parentId;
|
||||
|
||||
if (!parentId) return false;
|
||||
|
||||
const parent = state.pages.pageMap[parentId];
|
||||
const parentLayout = parent?.layout;
|
||||
|
||||
if (!workspaces.includes(parentId) && parentLayout !== ViewLayoutPB.Document) return false;
|
||||
|
||||
return page.name.toLowerCase().includes(searchText.toLowerCase());
|
||||
});
|
||||
});
|
||||
|
@ -7,17 +7,19 @@ function GridView({ viewId }: { viewId: string }) {
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [rendered, setRendered] = useState(false);
|
||||
const [rendered, setRendered] = useState<{ viewId: string; rendered: boolean } | undefined>(undefined);
|
||||
|
||||
// delegate wheel event to layout when grid is scrolled to top or bottom
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
|
||||
if (!element) {
|
||||
const viewId = rendered?.viewId;
|
||||
|
||||
if (!viewId || !element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gridScroller = element.querySelector('.grid-scroll-container') as HTMLDivElement;
|
||||
const gridScroller = element.querySelector(`[data-view-id="${viewId}"] .grid-scroll-container`) as HTMLDivElement;
|
||||
|
||||
const scrollLayout = gridScroller?.closest('.appflowy-scroll-container') as HTMLDivElement;
|
||||
|
||||
@ -29,7 +31,7 @@ function GridView({ viewId }: { viewId: string }) {
|
||||
const deltaY = event.deltaY;
|
||||
const deltaX = event.deltaX;
|
||||
|
||||
if (deltaX > 10) {
|
||||
if (Math.abs(deltaX) > 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -50,8 +52,11 @@ function GridView({ viewId }: { viewId: string }) {
|
||||
};
|
||||
}, [rendered]);
|
||||
|
||||
const onRendered = useCallback(() => {
|
||||
setRendered(true);
|
||||
const onRendered = useCallback((viewId: string) => {
|
||||
setRendered({
|
||||
viewId,
|
||||
rendered: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -133,9 +133,9 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul
|
||||
<div ref={scrollRef} className={'mt-1 flex w-full flex-col items-start'}>
|
||||
{isActivated && (
|
||||
<KeyboardNavigation
|
||||
options={editOptions}
|
||||
disableFocus={!focusMenu}
|
||||
scrollRef={scrollRef}
|
||||
options={editOptions}
|
||||
onConfirm={onConfirm}
|
||||
onFocus={() => {
|
||||
setFocusMenu(true);
|
||||
@ -143,8 +143,8 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul
|
||||
onBlur={() => {
|
||||
setFocusMenu(false);
|
||||
}}
|
||||
onEscape={onClose}
|
||||
disableSelect={!focusMenu}
|
||||
onEscape={onClose}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isHotkey('Tab', e)) {
|
||||
|
@ -40,7 +40,7 @@ export function LinkEditPopover({
|
||||
initialAnchorOrigin,
|
||||
initialTransformOrigin,
|
||||
initialPaperWidth: 340,
|
||||
initialPaperHeight: 180,
|
||||
initialPaperHeight: 200,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -160,7 +160,7 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro
|
||||
}, [renderColorItem, t]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={'flex h-full max-h-[360px] w-full flex-col overflow-y-auto'}>
|
||||
<div ref={ref} className={'flex h-full max-h-[420px] w-full flex-col overflow-y-auto'}>
|
||||
<KeyboardNavigation
|
||||
disableFocus={disableFocus}
|
||||
onPressLeft={onEscape}
|
||||
|
@ -14,9 +14,13 @@ const initialOrigin: {
|
||||
anchorOrigin?: PopoverOrigin;
|
||||
} = {
|
||||
anchorOrigin: {
|
||||
vertical: 'top',
|
||||
vertical: 'center',
|
||||
horizontal: 'right',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'center',
|
||||
horizontal: 'left',
|
||||
},
|
||||
};
|
||||
|
||||
export function Color({
|
||||
|
@ -12,7 +12,7 @@ const ActionButton = forwardRef<
|
||||
} & IconButtonProps
|
||||
>(({ tooltip, onClick, disabled, children, active, className, ...props }, ref) => {
|
||||
return (
|
||||
<Tooltip placement={'top'} title={tooltip}>
|
||||
<Tooltip disableInteractive={true} placement={'top'} title={tooltip}>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
|
@ -47,7 +47,7 @@ function ColorPopover({
|
||||
|
||||
const { paperHeight, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({
|
||||
initialPaperWidth: 200,
|
||||
initialPaperHeight: 360,
|
||||
initialPaperHeight: 420,
|
||||
anchorEl,
|
||||
initialAnchorOrigin: initialOrigin.anchorOrigin,
|
||||
initialTransformOrigin: initialOrigin.transformOrigin,
|
||||
|
@ -112,13 +112,14 @@ export function withPasted(editor: ReactEditor) {
|
||||
if (isText && parent) {
|
||||
const [parentNode, parentPath] = parent as NodeEntry<Element>;
|
||||
const pastedNodeIsPage = parentNode.type === EditorNodeType.Page;
|
||||
const pastedNodeIsNotList = !LIST_TYPES.includes(parentNode.type as EditorNodeType);
|
||||
const clonedFragment = transFragment(editor, fragment);
|
||||
|
||||
const [firstNode, ...otherNodes] = clonedFragment;
|
||||
const lastNode = getLastNode(otherNodes[otherNodes.length - 1]);
|
||||
const firstIsEmbed = editor.isEmbed(firstNode);
|
||||
const insertNodes: Element[] = [...otherNodes];
|
||||
const needMoveChildren = parentNode.children.length > 1 && !pastedNodeIsPage;
|
||||
const needMoveChildren = parentNode.children.length > 1 && !pastedNodeIsPage && !pastedNodeIsNotList;
|
||||
let moveStartIndex = 0;
|
||||
|
||||
if (firstIsEmbed) {
|
||||
@ -138,7 +139,7 @@ export function withPasted(editor: ReactEditor) {
|
||||
});
|
||||
|
||||
if (children.length > 0) {
|
||||
if (pastedNodeIsPage) {
|
||||
if (pastedNodeIsPage || pastedNodeIsNotList) {
|
||||
// lift the children of the first fragment node to current node
|
||||
insertNodes.unshift(...children);
|
||||
} else {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useLoadExpandedPages } from '$app/components/layout/bread_crumb/Breadcrumb.hooks';
|
||||
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import Link from '@mui/material/Link';
|
||||
@ -13,7 +13,6 @@ function Breadcrumb() {
|
||||
const { isTrash, pagePath, currentPage } = useLoadExpandedPages();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const parentPages = useMemo(() => pagePath.slice(1, -1).filter(Boolean) as Page[], [pagePath]);
|
||||
const navigateToPage = useCallback(
|
||||
(page: Page) => {
|
||||
const pageType = pageTypeMap[page.layout];
|
||||
@ -33,7 +32,17 @@ function Breadcrumb() {
|
||||
|
||||
return (
|
||||
<Breadcrumbs aria-label='breadcrumb'>
|
||||
{parentPages?.map((page: Page) => (
|
||||
{pagePath?.map((page: Page, index) => {
|
||||
if (index === pagePath.length - 1) {
|
||||
return (
|
||||
<div key={page.id} className={'flex select-none gap-1 text-text-title'}>
|
||||
<div className={'select-none'}>{getPageIcon(page)}</div>
|
||||
{page.name || t('menuAppHeader.defaultNewPageName')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={page.id}
|
||||
className={'flex cursor-pointer select-none gap-1'}
|
||||
@ -47,12 +56,8 @@ function Breadcrumb() {
|
||||
|
||||
{page.name || t('document.title.placeholder')}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className={'flex select-none gap-1 text-text-title'}>
|
||||
<div className={'select-none'}>{getPageIcon(currentPage)}</div>
|
||||
{currentPage.name || t('menuAppHeader.defaultNewPageName')}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
);
|
||||
}
|
||||
|
@ -1,73 +1,35 @@
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { Page } from '$app_reducers/pages/slice';
|
||||
import { getPage } from '$app/application/folder/page.service';
|
||||
|
||||
export function useLoadExpandedPages() {
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]);
|
||||
const currentPageId = params.id;
|
||||
const pageMap = useAppSelector((state) => state.pages.pageMap);
|
||||
const currentPage = currentPageId ? pageMap[currentPageId] : null;
|
||||
const currentPage = useAppSelector((state) => (currentPageId ? state.pages.pageMap[currentPageId] : undefined));
|
||||
|
||||
const [pagePath, setPagePath] = useState<
|
||||
(
|
||||
| Page
|
||||
| {
|
||||
name: string;
|
||||
const pagePath = useAppSelector((state) => {
|
||||
const result: Page[] = [];
|
||||
|
||||
if (!currentPage) return result;
|
||||
|
||||
const findParent = (page: Page) => {
|
||||
if (!page.parentId) return;
|
||||
const parent = state.pages.pageMap[page.parentId];
|
||||
|
||||
if (parent) {
|
||||
result.unshift(parent);
|
||||
findParent(parent);
|
||||
}
|
||||
)[]
|
||||
>([]);
|
||||
};
|
||||
|
||||
const loadPagePath = useCallback(
|
||||
async (pageId: string) => {
|
||||
let page = pageMap[pageId];
|
||||
|
||||
if (!page) {
|
||||
try {
|
||||
page = await getPage(pageId);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setPagePath((prev) => {
|
||||
return [page, ...prev];
|
||||
findParent(currentPage);
|
||||
result.push(currentPage);
|
||||
return result;
|
||||
});
|
||||
|
||||
if (page.parentId) {
|
||||
await loadPagePath(page.parentId);
|
||||
}
|
||||
},
|
||||
[pageMap]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPagePath([]);
|
||||
if (!currentPageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void loadPagePath(currentPageId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPageId]);
|
||||
|
||||
useEffect(() => {
|
||||
setPagePath((prev) => {
|
||||
return prev.map((page, index) => {
|
||||
if (!page) return page;
|
||||
if (index === 0) return page;
|
||||
return 'id' in page && page.id ? pageMap[page.id] : page;
|
||||
});
|
||||
});
|
||||
}, [pageMap]);
|
||||
|
||||
return {
|
||||
pagePath,
|
||||
currentPage,
|
||||
|
@ -1,7 +1,34 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
|
||||
.sketch-picker {
|
||||
background-color: var(--bg-body) !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.sketch-picker .flexbox-fix {
|
||||
border-color: var(--line-divider) !important;
|
||||
}
|
||||
.sketch-picker [id^='rc-editable-input'] {
|
||||
background-color: var(--bg-body) !important;
|
||||
border-color: var(--line-divider) !important;
|
||||
color: var(--text-title) !important;
|
||||
box-shadow: var(--line-border) 0px 0px 0px 1px inset !important;
|
||||
}
|
||||
|
||||
.appflowy-date-picker-calendar {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.grid-sticky-header::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.grid-scroll-container::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
|
||||
.appflowy-scroll-container {
|
||||
&::-webkit-scrollbar {
|
||||
|
@ -12,7 +12,16 @@ function NestedPage({ pageId }: { pageId: string }) {
|
||||
const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId);
|
||||
const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
|
||||
const dispatch = useAppDispatch();
|
||||
const page = useAppSelector((state) => state.pages.pageMap[pageId]);
|
||||
const { page, parentLayout } = useAppSelector((state) => {
|
||||
const page = state.pages.pageMap[pageId];
|
||||
const parent = state.pages.pageMap[page?.parentId || ''];
|
||||
|
||||
return {
|
||||
page,
|
||||
parentLayout: parent?.layout,
|
||||
};
|
||||
});
|
||||
|
||||
const disableChildren = useAppSelector((state) => {
|
||||
if (!page) return true;
|
||||
const layout = state.pages.pageMap[page.parentId]?.layout;
|
||||
@ -65,6 +74,9 @@ function NestedPage({ pageId }: { pageId: string }) {
|
||||
}
|
||||
}, [dropPosition, isDragging, isDraggingOver, page?.layout]);
|
||||
|
||||
// Only allow dragging if the parent layout is undefined or a document
|
||||
const draggable = parentLayout === undefined || parentLayout === ViewLayoutPB.Document;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
@ -73,7 +85,7 @@ function NestedPage({ pageId }: { pageId: string }) {
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
draggable={true}
|
||||
draggable={draggable}
|
||||
data-drop-enabled={page?.layout === ViewLayoutPB.Document}
|
||||
data-dragging={isDragging}
|
||||
data-page-id={pageId}
|
||||
|
@ -2,6 +2,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { RootState } from '$app/stores/store';
|
||||
import { pagesActions } from '$app_reducers/pages/slice';
|
||||
import { movePage, updatePage } from '$app/application/folder/page.service';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
|
||||
export const movePageThunk = createAsyncThunk(
|
||||
'pages/movePage',
|
||||
@ -61,12 +62,14 @@ export const movePageThunk = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
const debounceUpdateName = debounce(updatePage, 1000);
|
||||
|
||||
export const updatePageName = createAsyncThunk(
|
||||
'pages/updateName',
|
||||
async (payload: { id: string; name: string }, thunkAPI) => {
|
||||
async (payload: { id: string; name: string; immediate?: boolean }, thunkAPI) => {
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const { pageMap } = (getState() as RootState).pages;
|
||||
const { id, name } = payload;
|
||||
const { id, name, immediate } = payload;
|
||||
const page = pageMap[id];
|
||||
|
||||
if (name === page.name) return;
|
||||
@ -78,9 +81,13 @@ export const updatePageName = createAsyncThunk(
|
||||
})
|
||||
);
|
||||
|
||||
await updatePage({
|
||||
if (immediate) {
|
||||
await updatePage({ id, name });
|
||||
} else {
|
||||
await debounceUpdateName({
|
||||
id,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -1,5 +1,10 @@
|
||||
@import './variables/index.css';
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* stop body from scrolling */
|
||||
html,
|
||||
body {
|
||||
@ -53,114 +58,3 @@ th {
|
||||
@apply text-left font-normal;
|
||||
}
|
||||
|
||||
|
||||
.sketch-picker {
|
||||
background-color: var(--bg-body) !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.sketch-picker .flexbox-fix {
|
||||
border-color: var(--line-divider) !important;
|
||||
}
|
||||
.sketch-picker [id^='rc-editable-input'] {
|
||||
background-color: var(--bg-body) !important;
|
||||
border-color: var(--line-divider) !important;
|
||||
color: var(--text-title) !important;
|
||||
box-shadow: var(--line-border) 0px 0px 0px 1px inset !important;
|
||||
}
|
||||
|
||||
.appflowy-date-picker-calendar {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.react-datepicker__month-container {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
.react-datepicker__header {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border-bottom: 0;
|
||||
|
||||
}
|
||||
.react-datepicker__day-names {
|
||||
border: none;
|
||||
}
|
||||
.react-datepicker__day-name {
|
||||
color: var(--text-caption);
|
||||
}
|
||||
.react-datepicker__month {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.react-datepicker__day {
|
||||
border: none;
|
||||
color: var(--text-title);
|
||||
border-radius: 100%;
|
||||
}
|
||||
.react-datepicker__day:hover {
|
||||
border-radius: 100%;
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
.react-datepicker__day--outside-month {
|
||||
color: var(--text-caption);
|
||||
}
|
||||
.react-datepicker__day--in-range {
|
||||
background: var(--fill-hover);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
|
||||
.react-datepicker__day--today {
|
||||
border: 1px solid var(--fill-default);
|
||||
color: var(--text-title);
|
||||
border-radius: 100%;
|
||||
background: transparent;
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
.react-datepicker__day--today:hover{
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
.react-datepicker__day--in-selecting-range, .react-datepicker__day--today.react-datepicker__day--in-range {
|
||||
background: var(--fill-hover);
|
||||
color: var(--content-on-fill);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.react-datepicker__day--keyboard-selected {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected {
|
||||
&.react-datepicker__day--today {
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
background: var(--fill-default) !important;
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected:hover {
|
||||
background: var(--fill-default);
|
||||
color: var(--content-on-fill);
|
||||
}
|
||||
|
||||
.react-swipeable-view-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grid-sticky-header::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.grid-scroll-container::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user