mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support duplicate UI on web
This commit is contained in:
parent
6334255e15
commit
6fde3b9e3e
@ -28,7 +28,6 @@ export function withSignIn() {
|
||||
|
||||
saveRedirectTo(redirectTo);
|
||||
|
||||
console.log('=====saveRedirectTo', redirectTo);
|
||||
try {
|
||||
await originalMethod.apply(this, [args]);
|
||||
} catch (e) {
|
||||
|
12
frontend/appflowy_web_app/src/application/types.ts
Normal file
12
frontend/appflowy_web_app/src/application/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface Workspace {
|
||||
icon: string;
|
||||
id: string;
|
||||
name: string;
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
export interface SpaceView {
|
||||
id: string;
|
||||
extra?: string;
|
||||
name: string;
|
||||
}
|
4
frontend/appflowy_web_app/src/assets/close.svg
Normal file
4
frontend/appflowy_web_app/src/assets/close.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10.4751" y="4.81812" width="1" height="8" rx="0.5" transform="rotate(45 10.4751 4.81812)" fill="#333333"/>
|
||||
<rect x="11.1821" y="10.4749" width="1" height="8" rx="0.5" transform="rotate(135 11.1821 10.4749)" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 343 B |
3
frontend/appflowy_web_app/src/assets/selected.svg
Normal file
3
frontend/appflowy_web_app/src/assets/selected.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 8.2L6.84615 10L13 4" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 210 B |
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dialog, DialogProps, IconButton } from '@mui/material';
|
||||
import { ReactComponent as CloseIcon } from '@/assets/close.svg';
|
||||
|
||||
export interface NormalModalProps extends DialogProps {
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
onOk?: () => void;
|
||||
onCancel?: () => void;
|
||||
danger?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function NormalModal({
|
||||
okText,
|
||||
title,
|
||||
cancelText,
|
||||
onOk,
|
||||
onCancel,
|
||||
danger,
|
||||
children,
|
||||
...dialogProps
|
||||
}: NormalModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const modalOkText = okText || t('button.ok');
|
||||
const modalCancelText = cancelText || t('button.cancel');
|
||||
const buttonColor = danger ? 'var(--function-error)' : undefined;
|
||||
|
||||
return (
|
||||
<Dialog {...dialogProps}>
|
||||
<div className={'relative flex flex-col gap-4 p-5'}>
|
||||
<div className={'flex w-full items-center justify-between text-base font-medium'}>
|
||||
<div className={'flex-1 text-center '}>{title}</div>
|
||||
<div className={'relative -right-1.5'}>
|
||||
<IconButton size={'small'} color={'inherit'} className={'h-8 w-8'} onClick={onCancel}>
|
||||
<CloseIcon className={'h-8 w-8'} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1'}>{children}</div>
|
||||
<div className={'flex w-full justify-end gap-4'}>
|
||||
<Button color={'inherit'} variant={'outlined'} onClick={onCancel}>
|
||||
{modalCancelText}
|
||||
</Button>
|
||||
<Button color={'primary'} variant={'contained'} style={{ backgroundColor: buttonColor }} onClick={onOk}>
|
||||
{modalOkText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default NormalModal;
|
@ -0,0 +1 @@
|
||||
export * from './NormalModal';
|
@ -46,15 +46,29 @@ function AppTheme({ children }: { children: React.ReactNode }) {
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
text: {
|
||||
borderRadius: '8px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--fill-list-hover)',
|
||||
},
|
||||
},
|
||||
contained: {
|
||||
color: 'var(--content-on-fill)',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--content-blue-600)',
|
||||
},
|
||||
borderRadius: '8px',
|
||||
},
|
||||
outlined: {
|
||||
'&.MuiButton-outlinedInherit': {
|
||||
borderColor: 'var(--line-divider)',
|
||||
},
|
||||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiButtonBase: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@ -78,10 +92,16 @@ function AppTheme({ children }: { children: React.ReactNode }) {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
boxShadow: 'var(--shadow)',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
borderRadius: '12px',
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
sx: {
|
||||
'& .MuiBackdrop-root': {
|
||||
|
@ -1,23 +1,13 @@
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import LoginProvider from '@/components/login/LoginProvider';
|
||||
import MagicLink from '@/components/login/MagicLink';
|
||||
import { Divider } from '@mui/material';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export function Login() {
|
||||
export function Login({ redirectTo }: { redirectTo: string }) {
|
||||
const { t } = useTranslation();
|
||||
const [search] = useSearchParams();
|
||||
const redirectTo = search.get('redirectTo') || '';
|
||||
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && redirectTo && encodeURIComponent(redirectTo) !== window.location.href) {
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
}, [isAuthenticated, redirectTo]);
|
||||
return (
|
||||
<div className={'my-10 flex flex-col items-center justify-center gap-[24px] px-4'}>
|
||||
<div className={'flex flex-col items-center justify-center gap-[14px]'}>
|
||||
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Dialog, IconButton } from '@mui/material';
|
||||
import { Login } from './Login';
|
||||
import { ReactComponent as CloseIcon } from '@/assets/close.svg';
|
||||
|
||||
export function LoginModal({ redirectTo, open, onClose }: { redirectTo: string; open: boolean; onClose: () => void }) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<div className={'relative px-6'}>
|
||||
<Login redirectTo={redirectTo} />
|
||||
<div className={'absolute top-2 right-2'}>
|
||||
<IconButton color={'inherit'} onClick={onClose}>
|
||||
<CloseIcon className={'h-8 w-8'} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@ -58,7 +58,7 @@ function LoginProvider({ redirectTo }: { redirectTo: string }) {
|
||||
variant={'outlined'}
|
||||
onClick={() => handleClick(option.value)}
|
||||
className={
|
||||
'flex h-[46px] w-[380px] items-center justify-center gap-[10px] rounded-[12px] border border-line-divider text-sm font-medium'
|
||||
'flex h-[46px] w-[380px] items-center justify-center gap-[10px] rounded-[12px] border border-line-divider text-sm font-medium text-text-title'
|
||||
}
|
||||
>
|
||||
<option.Icon className={'h-[20px] w-[20px]'} />
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './Login';
|
||||
export * from './LoginModal';
|
||||
|
@ -1,20 +1,20 @@
|
||||
// import { invalidToken } from '@/application/session/token';
|
||||
import { invalidToken } from '@/application/session/token';
|
||||
import { Popover } from '@/components/_shared/popover';
|
||||
// import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { ThemeModeContext } from '@/components/app/useAppThemeMode';
|
||||
import { openUrl } from '@/utils/url';
|
||||
import { IconButton } from '@mui/material';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { ReactComponent as MoreIcon } from '@/assets/more.svg';
|
||||
import { ReactComponent as MoonIcon } from '@/assets/moon.svg';
|
||||
import { ReactComponent as SunIcon } from '@/assets/sun.svg';
|
||||
// import { ReactComponent as LoginIcon } from '@/assets/login.svg';
|
||||
import { ReactComponent as LoginIcon } from '@/assets/login.svg';
|
||||
import { ReactComponent as ReportIcon } from '@/assets/report.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
||||
import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg';
|
||||
|
||||
// import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
function MoreActions() {
|
||||
const { isDark, setDark } = useContext(ThemeModeContext) || {};
|
||||
@ -31,21 +31,21 @@ function MoreActions() {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
|
||||
//
|
||||
// const handleLogin = useCallback(() => {
|
||||
// invalidToken();
|
||||
// navigate('/login?redirectTo=' + encodeURIComponent(window.location.href));
|
||||
// }, [navigate]);
|
||||
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
|
||||
|
||||
const handleLogin = useCallback(() => {
|
||||
invalidToken();
|
||||
navigate('/login?redirectTo=' + encodeURIComponent(window.location.href));
|
||||
}, [navigate]);
|
||||
const actions = useMemo(() => {
|
||||
return [
|
||||
// {
|
||||
// Icon: LoginIcon,
|
||||
// label: isAuthenticated ? t('button.logout') : t('web.login'),
|
||||
// onClick: handleLogin,
|
||||
// },
|
||||
{
|
||||
Icon: LoginIcon,
|
||||
label: isAuthenticated ? t('button.logout') : t('web.login'),
|
||||
onClick: handleLogin,
|
||||
},
|
||||
isDark
|
||||
? {
|
||||
Icon: SunIcon,
|
||||
@ -69,7 +69,7 @@ function MoreActions() {
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [t, isDark, setDark]);
|
||||
}, [isAuthenticated, t, handleLogin, isDark, setDark]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -9,6 +9,7 @@ import Breadcrumb from './Breadcrumb';
|
||||
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
||||
import MoreActions from './MoreActions';
|
||||
import { ReactComponent as SideOutlined } from '@/assets/side_outlined.svg';
|
||||
import Duplicate from './duplicate/Duplicate';
|
||||
|
||||
export const HEADER_HEIGHT = 48;
|
||||
|
||||
@ -92,6 +93,7 @@ export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer:
|
||||
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<MoreActions />
|
||||
<Duplicate />
|
||||
<Divider orientation={'vertical'} className={'mx-2'} flexItem />
|
||||
<Tooltip title={t('publish.downloadApp')}>
|
||||
<button onClick={openOrDownload}>
|
||||
|
@ -0,0 +1,31 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoginModal } from '@/components/login/LoginModal';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useDuplicate } from '@/components/publish/header/duplicate/useDuplicate';
|
||||
import DuplicateModal from '@/components/publish/header/duplicate/DuplicateModal';
|
||||
|
||||
function Duplicate() {
|
||||
const { t } = useTranslation();
|
||||
const { loginOpen, duplicateOpen, handleDuplicateClose, handleLoginClose, url } = useDuplicate();
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const handleClick = useCallback(() => {
|
||||
setSearch({
|
||||
...search,
|
||||
action: 'duplicate',
|
||||
});
|
||||
}, [search, setSearch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleClick} size={'small'} variant={'outlined'} color={'inherit'}>
|
||||
{t('button.duplicate')}
|
||||
</Button>
|
||||
<LoginModal redirectTo={url} open={loginOpen} onClose={handleLoginClose} />
|
||||
<DuplicateModal open={duplicateOpen} onClose={handleDuplicateClose} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Duplicate;
|
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NormalModal } from '@/components/_shared/modal';
|
||||
import SelectWorkspace from '@/components/publish/header/duplicate/SelectWorkspace';
|
||||
import { useLoadWorkspaces } from '@/components/publish/header/duplicate/useDuplicate';
|
||||
import SpaceList from '@/components/publish/header/duplicate/SpaceList';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
|
||||
function DuplicateModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { workspaceList, spaceList, setSelectedSpaceId, setSelectedWorkspaceId, selectedWorkspaceId, selectedSpaceId } =
|
||||
useLoadWorkspaces();
|
||||
|
||||
return (
|
||||
<NormalModal
|
||||
onCancel={onClose}
|
||||
okText={t('button.add')}
|
||||
title={t('publish.duplicateTitle')}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOk={async () => {
|
||||
// submit form
|
||||
notify.success(t('publish.duplicateSuccessfully'));
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<div className={'flex flex-col gap-4'}>
|
||||
<SelectWorkspace workspaceList={workspaceList} value={selectedWorkspaceId} onChange={setSelectedWorkspaceId} />
|
||||
<SpaceList spaceList={spaceList} value={selectedSpaceId} onChange={setSelectedSpaceId} />
|
||||
</div>
|
||||
</NormalModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DuplicateModal;
|
@ -0,0 +1,112 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, IconButton, Tooltip } from '@mui/material';
|
||||
import { Workspace } from '@/application/types';
|
||||
import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg';
|
||||
import { ReactComponent as CheckIcon } from '@/assets/selected.svg';
|
||||
import { Popover } from '@/components/_shared/popover';
|
||||
|
||||
export interface SelectWorkspaceProps {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
workspaceList: Workspace[];
|
||||
}
|
||||
|
||||
function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProps) {
|
||||
const { t } = useTranslation();
|
||||
const email = 'lu@appflowy.io';
|
||||
const selectedWorkspace = useMemo(() => {
|
||||
return workspaceList.find((workspace) => workspace.id === value);
|
||||
}, [value, workspaceList]);
|
||||
const ref = useRef<HTMLButtonElement | null>(null);
|
||||
const [selectOpen, setSelectOpen] = useState<boolean>(false);
|
||||
|
||||
const renderWorkspace = useCallback(
|
||||
(workspace: Workspace) => {
|
||||
return (
|
||||
<div className={'flex items-center gap-[10px] overflow-hidden'}>
|
||||
<div className={'h-8 w-8 text-2xl'}>{workspace.icon}</div>
|
||||
<div className={'flex flex-1 flex-col items-start gap-0.5 overflow-hidden'}>
|
||||
<div className={'w-full truncate text-left text-sm font-medium'}>{workspace.name}</div>
|
||||
<div className={'text-xs text-text-caption'}>
|
||||
{t('publish.membersCount', {
|
||||
count: workspace.memberCount || 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex w-[360px] flex-col gap-2'}>
|
||||
<div className={'text-sm text-text-caption'}>{t('publish.selectWorkspace')}</div>
|
||||
<Button
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setSelectOpen(true);
|
||||
}}
|
||||
className={'px-3 py-2'}
|
||||
variant={'outlined'}
|
||||
color={'inherit'}
|
||||
>
|
||||
<div className={'flex w-full items-center gap-[10px]'}>
|
||||
<div className={'flex-1 overflow-hidden'}>{selectedWorkspace ? renderWorkspace(selectedWorkspace) : null}</div>
|
||||
<IconButton size={'small'} className={`h-6 w-6 ${selectOpen ? '-rotate-90' : 'rotate-90'} transform`}>
|
||||
<RightIcon className={'h-6 w-6'} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Button>
|
||||
<Popover
|
||||
anchorEl={ref.current}
|
||||
open={selectOpen}
|
||||
transformOrigin={{
|
||||
vertical: -8,
|
||||
horizontal: 'left',
|
||||
}}
|
||||
onClose={() => {
|
||||
setSelectOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className={'flex max-h-[360px] w-[360px] flex-col gap-1 p-2'}>
|
||||
<div className={'w-full px-3 py-2 text-sm font-medium text-text-caption'}>{email}</div>
|
||||
<Divider />
|
||||
<div className={'flex flex-1 flex-col overflow-y-auto overflow-x-hidden'}>
|
||||
{workspaceList.map((workspace) => {
|
||||
const isSelected = workspace.id === selectedWorkspace?.id;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={workspace.name}
|
||||
key={workspace.id}
|
||||
placement={'bottom'}
|
||||
enterDelay={1000}
|
||||
enterNextDelay={1000}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange?.(workspace.id);
|
||||
setSelectOpen(false);
|
||||
}}
|
||||
className={'w-full px-3 py-2'}
|
||||
variant={'text'}
|
||||
color={'inherit'}
|
||||
>
|
||||
<div className={'flex-1 overflow-hidden'}>{renderWorkspace(workspace)}</div>
|
||||
<div className={'h-6 w-6'}>
|
||||
{isSelected && <CheckIcon className={'h-6 w-6 text-content-blue-400'} />}
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectWorkspace;
|
@ -0,0 +1,84 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { SpaceView } from '@/application/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as CheckIcon } from '@/assets/selected.svg';
|
||||
import { renderColor } from '@/utils/color';
|
||||
import SpaceIcon from '@/components/publish/header/SpaceIcon';
|
||||
import { Button, Tooltip } from '@mui/material';
|
||||
|
||||
export interface SpaceListProps {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
spaceList: SpaceView[];
|
||||
}
|
||||
|
||||
function SpaceList({ spaceList, value, onChange }: SpaceListProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getExtraObj = useCallback((extra: string) => {
|
||||
try {
|
||||
return extra
|
||||
? (JSON.parse(extra) as {
|
||||
is_space?: boolean;
|
||||
space_icon?: string;
|
||||
space_icon_color?: string;
|
||||
})
|
||||
: {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderSpace = useCallback(
|
||||
(space: SpaceView) => {
|
||||
const extraObj = getExtraObj(space.extra || '');
|
||||
|
||||
return (
|
||||
<div className={'flex items-center gap-[10px] overflow-hidden text-sm'}>
|
||||
<span
|
||||
className={'icon h-5 w-5'}
|
||||
style={{
|
||||
backgroundColor: extraObj.space_icon_color ? renderColor(extraObj.space_icon_color) : undefined,
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<SpaceIcon value={extraObj.space_icon || ''} />
|
||||
</span>
|
||||
<div className={'flex-1 truncate'}>{space.name}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[getExtraObj]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex max-h-[260px] w-[360px] flex-col gap-2 overflow-hidden'}>
|
||||
<div className={'text-sm text-text-caption'}>{t('publish.addTo')}</div>
|
||||
<div className={'flex w-full flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden'}>
|
||||
{spaceList.map((space) => {
|
||||
const isSelected = value === space.id;
|
||||
|
||||
return (
|
||||
<Tooltip title={space.name} key={space.id} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
|
||||
<Button
|
||||
variant={'text'}
|
||||
color={'inherit'}
|
||||
className={'flex items-center p-1 font-normal'}
|
||||
onClick={() => {
|
||||
onChange?.(space.id);
|
||||
}}
|
||||
>
|
||||
<div className={'flex-1 overflow-hidden text-left'}>{renderSpace(space)}</div>
|
||||
<div className={'h-6 w-6'}>
|
||||
{isSelected && <CheckIcon className={'h-6 w-6 text-content-blue-400'} />}
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpaceList;
|
@ -0,0 +1,183 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { SpaceView, Workspace } from '@/application/types';
|
||||
|
||||
export function useDuplicate() {
|
||||
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const [loginOpen, setLoginOpen] = React.useState(false);
|
||||
const [duplicateOpen, setDuplicateOpen] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const isDuplicate = search.get('action') === 'duplicate';
|
||||
|
||||
if (!isDuplicate) return;
|
||||
|
||||
setLoginOpen(!isAuthenticated);
|
||||
setDuplicateOpen(isAuthenticated);
|
||||
}, [isAuthenticated, search, setSearch]);
|
||||
|
||||
const url = window.location.href;
|
||||
|
||||
const handleLoginClose = useCallback(() => {
|
||||
setLoginOpen(false);
|
||||
setSearch((prev) => {
|
||||
prev.delete('action');
|
||||
return prev;
|
||||
});
|
||||
}, [setSearch]);
|
||||
|
||||
const handleDuplicateClose = useCallback(() => {
|
||||
setDuplicateOpen(false);
|
||||
setSearch((prev) => {
|
||||
prev.delete('action');
|
||||
return prev;
|
||||
});
|
||||
}, [setSearch]);
|
||||
|
||||
return {
|
||||
loginOpen,
|
||||
handleLoginClose,
|
||||
url,
|
||||
duplicateOpen,
|
||||
handleDuplicateClose,
|
||||
};
|
||||
}
|
||||
|
||||
export function useLoadWorkspaces() {
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>('1');
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>('1');
|
||||
|
||||
const [workspaceList] = useState<Workspace[]>([
|
||||
{
|
||||
icon: '😊',
|
||||
id: '1',
|
||||
name: 'AppFlowy',
|
||||
memberCount: 0,
|
||||
},
|
||||
{
|
||||
icon: '😍',
|
||||
id: '2',
|
||||
name: `Kilu's Workspace`,
|
||||
memberCount: 12,
|
||||
},
|
||||
{
|
||||
icon: '😎',
|
||||
id: '3',
|
||||
name: 'Workspace 3 djskh dhjsa dhsjkahdkja dshjkahd kashdjkashd askhdkjas',
|
||||
memberCount: 5,
|
||||
},
|
||||
{
|
||||
icon: '😇',
|
||||
id: '4',
|
||||
name: 'Workspace 4',
|
||||
memberCount: 3,
|
||||
},
|
||||
{
|
||||
icon: '😜',
|
||||
id: '5',
|
||||
name: 'Workspace 5',
|
||||
memberCount: 1,
|
||||
},
|
||||
{
|
||||
icon: '😉',
|
||||
id: '6',
|
||||
name: 'Workspace 6',
|
||||
memberCount: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const [spaceList] = useState<SpaceView[]>([
|
||||
{
|
||||
id: '1',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_1',
|
||||
space_icon_color: 'appflowy_them_color_tint2',
|
||||
}),
|
||||
name: 'Space 1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_2',
|
||||
space_icon_color: 'appflowy_them_color_tint1',
|
||||
}),
|
||||
name: 'Space 2 djisa djiso adiohjsa hdiosahdk ksahkjdhskjahdaskj',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_3',
|
||||
space_icon_color: 'appflowy_them_color_tint3',
|
||||
}),
|
||||
name: 'Space 3',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_4',
|
||||
space_icon_color: 'appflowy_them_color_tint4',
|
||||
}),
|
||||
name: 'Space 4',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_5',
|
||||
space_icon_color: 'appflowy_them_color_tint5',
|
||||
}),
|
||||
name: 'Space 5',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_6',
|
||||
space_icon_color: 'appflowy_them_color_tint6',
|
||||
}),
|
||||
name: 'Space 6',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_7',
|
||||
space_icon_color: 'appflowy_them_color_tint7',
|
||||
}),
|
||||
name: 'Space 7',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_8',
|
||||
space_icon_color: 'appflowy_them_color_tint8',
|
||||
}),
|
||||
name: 'Space 8',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
extra: JSON.stringify({
|
||||
is_space: true,
|
||||
space_icon: 'space_icon_9',
|
||||
space_icon_color: 'appflowy_them_color_tint9',
|
||||
}),
|
||||
name: 'Space',
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
workspaceList,
|
||||
spaceList,
|
||||
selectedWorkspaceId,
|
||||
setSelectedWorkspaceId,
|
||||
selectedSpaceId,
|
||||
setSelectedSpaceId,
|
||||
};
|
||||
}
|
@ -1,10 +1,21 @@
|
||||
import { Login } from '@/components/login';
|
||||
import React from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
|
||||
function LoginPage() {
|
||||
const [search] = useSearchParams();
|
||||
const redirectTo = search.get('redirectTo') || '';
|
||||
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && redirectTo && encodeURIComponent(redirectTo) !== window.location.href) {
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
}, [isAuthenticated, redirectTo]);
|
||||
return (
|
||||
<div className={'bg-body flex h-screen w-screen items-center justify-center'}>
|
||||
<Login />
|
||||
<Login redirectTo={redirectTo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2130,11 +2130,25 @@
|
||||
"zero": "Publish {} selected view",
|
||||
"one": "Publish {} selected views",
|
||||
"many": "Publish {} selected views",
|
||||
"other":"Publish {} selected views"
|
||||
"other": "Publish {} selected views"
|
||||
},
|
||||
"mustSelectPrimaryDatabase": "The primary view must be selected",
|
||||
"noDatabaseSelected": "No database selected, please select at least one database.",
|
||||
"unableToDeselectPrimaryDatabase": "Unable to deselect primary database"
|
||||
"unableToDeselectPrimaryDatabase": "Unable to deselect primary database",
|
||||
"duplicateTitle": "Where would you like to add",
|
||||
"selectWorkspace": "Select a workspace",
|
||||
"addTo": "Add to",
|
||||
"duplicateSuccessfully": "Duplicated success",
|
||||
"duplicateSuccessfullyDescription": "Open in AppFlowy's desktop app? If don't have the app, you can",
|
||||
"downloadIt": "download it here",
|
||||
"openApp": "Open in app",
|
||||
"duplicateFailed": "Duplicated failed",
|
||||
"membersCount": {
|
||||
"zero": "No members",
|
||||
"one": "1 member",
|
||||
"many": "{count} members",
|
||||
"other": "{count} members"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"continue": "Continue",
|
||||
|
Loading…
Reference in New Issue
Block a user