Merge branch 'feat/filesystem_and_search' of github.com:toeverything/AFFiNE into feat/filesystem_and_search

This commit is contained in:
JimmFly
2022-12-19 11:18:39 +08:00
91 changed files with 5391 additions and 524 deletions
+11
View File
@@ -0,0 +1,11 @@
import { NotFoundTitle, PageContainer } from './styles';
export const NotfoundPage = () => {
return (
<PageContainer>
<NotFoundTitle>404 - Page Not Found</NotFoundTitle>
</PageContainer>
);
};
export default NotfoundPage;
+21
View File
@@ -0,0 +1,21 @@
import { absoluteCenter, displayFlex, styled } from '@/styles';
export const PageContainer = styled('div')(({ theme }) => {
return {
width: '100%',
height: 'calc(100vh)',
backgroundColor: theme.colors.pageBackground,
};
});
export const NotFoundTitle = styled('h1')(({ theme }) => {
return {
position: 'relative',
top: 'calc(50% - 100px)',
height: '100px',
fontSize: '60px',
lineHeight: '100px',
color: theme.colors.textColor,
textAlign: 'center',
};
});
@@ -22,6 +22,9 @@ import { getWarningMessage, shouldShowWarning } from './utils';
import { Menu, MenuItem } from '@/ui/menu';
import { useRouter } from 'next/router';
import { useConfirm } from '@/providers/confirm-provider';
import { useModal } from '@/providers/global-modal-provider';
import { useAppState } from '@/providers/app-state-provider';
import { SyncIcon } from './sync-icon';
const PopoverContent = () => {
const { editor, mode, setMode } = useEditor();
@@ -131,6 +134,7 @@ const HeaderRight = () => {
}
return (
<>
<SyncIcon />
<ThemeModeSwitch />
<Menu content={<PopoverContent />} placement="bottom-end">
<IconButton>
@@ -0,0 +1,19 @@
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/global-modal-provider';
import { useAppState } from '@/providers/app-state-provider';
import { IconButton } from '@/ui/button';
export const SyncIcon = () => {
const { triggerLoginModal } = useModal();
const appState = useAppState();
return appState.user ? (
<IconButton iconSize="middle" disabled>
<CloudInsyncIcon />
</IconButton>
) : (
<IconButton iconSize="middle" onClick={triggerLoginModal}>
<CloudUnsyncedIcon />
</IconButton>
);
};
@@ -0,0 +1,124 @@
import { signInWithGoogle, login, setToken } from '@pathfinder/data-services';
import { styled } from '@/styles';
import { Button } from '@/ui/button';
import { useModal } from '@/providers/global-modal-provider';
import { GoogleIcon, StayLogOutIcon } from './icons';
export const GoogleLoginButton = () => {
const { triggerLoginModal } = useModal();
return (
<StyledGoogleButton
onClick={() => {
signInWithGoogle()
.then(async user => {
const idToken = await user.user.getIdToken();
const token = await login({ token: idToken, type: 'Google' });
setToken({
accessToken: token.token,
refreshToken: token.refresh,
});
triggerLoginModal();
})
.catch(error => {
console.log('sign google error', error);
});
}}
>
<ButtonWrapper>
<IconWrapper>
<GoogleIcon />
</IconWrapper>
<TextWrapper>
<Title>Continue with Google</Title>
<Description>Set up an AFFINE account to sync data</Description>
</TextWrapper>
</ButtonWrapper>
</StyledGoogleButton>
);
};
export const StayLogOutButton = () => {
return (
<StyledStayLogOutButton>
<ButtonWrapper>
<IconWrapper>
<StayLogOutIcon />
</IconWrapper>
<TextWrapper>
<Title>Stay logged out</Title>
<Description>All changes are saved locally</Description>
</TextWrapper>
</ButtonWrapper>
</StyledStayLogOutButton>
);
};
const StyledGoogleButton = styled(Button)(({ theme }) => {
return {
width: '361px',
height: '56px',
padding: '4px',
background: '#6880FF',
color: '#fff',
'& > span': {
marginLeft: 0,
},
':hover': {
background: '#516BF4',
color: '#fff',
},
};
});
const StyledStayLogOutButton = styled(Button)(({ theme }) => {
return {
width: '361px',
height: '56px',
padding: '4px',
'& > span': {
marginLeft: 0,
},
':hover': {
borderColor: '#6880FF',
},
};
});
const ButtonWrapper = styled('div')({
display: 'flex',
flexDirection: 'row',
});
const IconWrapper = styled('div')({
width: '48px',
height: '48px',
flex: '0 48px',
borderRadius: '5px',
overflow: 'hidden',
marginRight: '12px',
});
const TextWrapper = styled('div')({
flex: 1,
textAlign: 'left',
});
const Title = styled('h1')(({ theme }) => {
return {
fontSize: '18px',
lineHeight: '26px',
fontWeight: 500,
};
});
const Description = styled('p')(({ theme }) => {
return {
fontSize: '16px',
lineHeight: '22px',
fontWeight: 400,
};
});
@@ -0,0 +1,6 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#FFC107"/>
<path d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z" fill="#FF3D00"/>
<path d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z" fill="#4CAF50"/>
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#1976D2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,42 @@
import { CloudUnsyncedIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import GoogleSvg from './google.svg';
export const GoogleIcon = () => {
return (
<GoogleIconWrapper>
<picture>
<img src={GoogleSvg.src} alt="Google" />
</picture>
</GoogleIconWrapper>
);
};
const GoogleIconWrapper = styled('div')(({ theme }) => ({
width: '48px',
height: '48px',
background: theme.colors.pageBackground,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
export const StayLogOutIcon = () => {
return (
<StayLogOutWrapper>
<CloudUnsyncedIcon />
</StayLogOutWrapper>
);
};
const StayLogOutWrapper = styled('div')(({ theme }) => {
return {
width: '48px',
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
background: theme.colors.hoverBackground,
};
});
@@ -0,0 +1,68 @@
import { ResetIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { TextButton, Button } from '@/ui/button';
import { GoogleLoginButton, StayLogOutButton } from './LoginOptionButton';
interface LoginModalProps {
open: boolean;
onClose: () => void;
}
export const LoginModal = ({ open, onClose }: LoginModalProps) => {
return (
<Modal open={open} onClose={onClose}>
<ModalWrapper width={620} height={334}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
/>
</Header>
<Content>
<ContentTitle>Currently not logged in</ContentTitle>
<GoogleLoginButton />
<StayLogOutButton />
</Content>
<Footer>
<TextButton icon={<StyledResetIcon />}>Clear local data</TextButton>
</Footer>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const Footer = styled('div')({
height: '70px',
paddingLeft: '24px',
marginTop: '32px',
});
const StyledResetIcon = styled(ResetIcon)({
marginRight: '12px',
width: '20px',
height: '20px',
});
@@ -0,0 +1,85 @@
import { InformationIcon, LogOutIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { Divider } from '@/ui/divider';
import { useAppState } from '@/providers/app-state-provider';
import { SelectorPopperContainer } from './styles';
import {
PrivateWorkspaceItem,
WorkspaceItem,
CreateWorkspaceItem,
ListItem,
LoginItem,
} from './WorkspaceItem';
const workspaces = [
{
name: 'Design',
icon: '',
},
{
name: 'Operation',
icon: '',
},
{
name: 'Something is too long to show in this box',
icon: '',
},
];
export const SelectorPopperContent = () => {
const { user, workspaces } = useAppState();
return !user ? (
<SelectorPopperContainer placement="bottom-start">
<LoginItem />
<StyledDivider />
<ListItem
icon={<InformationIcon />}
name="About AFFiNE"
onClick={() => console.log('About AFFiNE')}
/>
</SelectorPopperContainer>
) : (
<SelectorPopperContainer placement="bottom-start">
<PrivateWorkspaceItem />
<StyledDivider />
<WorkspaceGroupTitle>Workspace</WorkspaceGroupTitle>
{workspaces.map(workspace => {
return (
<WorkspaceItem
key={workspace.id}
id={workspace.id}
name={`workspace-${workspace.id}`}
icon={''}
/>
);
})}
<CreateWorkspaceItem />
<StyledDivider />
<ListItem
icon={<InformationIcon />}
name="About AFFiNE"
onClick={() => console.log('About AFFiNE')}
/>
<ListItem
icon={<LogOutIcon />}
name="Sign out"
onClick={() => console.log('Sign out')}
/>
</SelectorPopperContainer>
);
};
const StyledDivider = styled(Divider)({
margin: '8px 12px',
width: 'calc(100% - 24px)',
});
const WorkspaceGroupTitle = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
lineHeight: '30px',
height: '30px',
padding: '0 12px',
};
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { AddIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import {
WorkspaceItemAvatar,
WorkspaceItemWrapper,
WorkspaceItemContent,
} from '../styles';
import { WorkspaceCreate } from './workspace-create';
const name = 'Create new Workspace';
export const CreateWorkspaceItem = () => {
const [open, setOpen] = useState(false);
return (
<>
<WorkspaceItemWrapper onClick={() => setOpen(true)}>
<WorkspaceItemAvatar>
<AddIcon />
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
</WorkspaceItemWrapper>
<WorkspaceCreate open={open} onClose={() => setOpen(false)} />
</>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.base,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
@@ -0,0 +1 @@
export * from './CreateWorkspaceItem';
@@ -0,0 +1,55 @@
import { styled } from '@/styles';
export const StyledModalWrapper = styled('div')(({ theme }) => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: theme.colors.popoverBackground,
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
color: theme.colors.popoverColor,
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(({ theme }) => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
};
});
export const StyledInputContent = styled('div')(({ theme }) => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '40px 0 24px 0',
};
});
export const StyledButtonContent = styled('div')(({ theme }) => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0px 0 32px 0',
};
});
@@ -0,0 +1,58 @@
import { createWorkspace } from '@pathfinder/data-services';
import Modal from '@/ui/modal';
import Input from '@/ui/input';
import { Button } from '@/ui/button';
import {
StyledModalHeader,
StyledTextContent,
StyledModalWrapper,
StyledInputContent,
StyledButtonContent,
} from './style';
import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal';
interface WorkspaceCreateProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
const [workspaceName, setWorkspaceId] = useState<string>('');
const handlerInputChange = (workspaceName: string) => {
setWorkspaceId(workspaceName);
};
console.log('workspaceName', workspaceName);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton />
<StyledModalHeader>Create new Workspace</StyledModalHeader>
<StyledTextContent>
Workspaces are shared environments where teams can collaborate. After
creating a Workspace, you can invite others to join.
</StyledTextContent>
<StyledInputContent>
<Input
onChange={handlerInputChange}
placeholder="Set a Workspace name"
value={workspaceName}
></Input>
</StyledInputContent>
<StyledButtonContent>
<Button
disabled={!workspaceName.length}
style={{ width: '260px' }}
onClick={() => {
createWorkspace({ name: workspaceName, avatar: '' });
}}
>
Create
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
export default WorkspaceCreate;
@@ -0,0 +1,37 @@
import type { ReactNode } from 'react';
import { styled } from '@/styles';
import { WorkspaceItemWrapper, WorkspaceItemContent } from './styles';
interface ListItemProps {
name: string;
icon: ReactNode;
onClick: () => void;
}
export const ListItem = ({ name, icon, onClick }: ListItemProps) => {
return (
<WorkspaceItemWrapper onClick={onClick}>
<StyledIconWrapper>{icon}</StyledIconWrapper>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
</WorkspaceItemWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.sm,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const StyledIconWrapper = styled('div')({
width: '20px',
height: '20px',
fontSize: '20px',
});
@@ -0,0 +1,48 @@
import { useModal } from '@/providers/global-modal-provider';
import { styled } from '@/styles';
import {
WorkspaceItemAvatar,
LoginItemWrapper,
WorkspaceItemContent,
} from './styles';
export const LoginItem = () => {
const { triggerLoginModal } = useModal();
return (
<LoginItemWrapper onClick={() => triggerLoginModal()}>
<WorkspaceItemAvatar alt="AFFiNE" src={''}>
A
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title="AFFiNE">AFFiNE</Name>
<Description
title="Log in to sync with affine"
className="login-description"
>
Log in to sync with affine
</Description>
</WorkspaceItemContent>
</LoginItemWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.base,
fontWeight: 500,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const Description = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
@@ -0,0 +1,44 @@
import { styled } from '@/styles';
import { useAppState } from '@/providers/app-state-provider';
import {
WorkspaceItemAvatar,
PrivateWorkspaceWrapper,
WorkspaceItemContent,
} from './styles';
export const PrivateWorkspaceItem = () => {
const { user } = useAppState();
return !user ? null : (
<PrivateWorkspaceWrapper>
<WorkspaceItemAvatar alt={user.name} src={user.avatar_url}>
{user.name.charAt(0)}
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title={user.name}>{user.name}</Name>
<Email title={user.email}>{user.email}</Email>
</WorkspaceItemContent>
</PrivateWorkspaceWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.base,
fontWeight: 500,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const Email = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
@@ -0,0 +1,17 @@
import { SettingsIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { IconButton } from '@/ui/button';
export const FooterSetting = () => {
return (
<Wrapper className="footer-setting">
<SettingsIcon />
</Wrapper>
);
};
const Wrapper = styled(IconButton)(({ theme }) => {
return {
fontSize: '20px',
};
});
@@ -0,0 +1,25 @@
import { UsersIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import { IconButton } from '@/ui/button';
export const FooterUsers = () => {
return (
<Wrapper className="footer-users">
<>
<UsersIcon />
<Tip>99+</Tip>
</>
</Wrapper>
);
};
const Wrapper = styled(IconButton)({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '16px',
});
const Tip = styled('span')({
fontSize: '12px',
});
@@ -0,0 +1,64 @@
import { styled } from '@/styles';
import {
WorkspaceItemAvatar,
WorkspaceItemWrapper,
WorkspaceItemContent,
} from '../styles';
import { FooterSetting } from './FooterSetting';
import { FooterUsers } from './FooterUsers';
interface WorkspaceItemProps {
id: string;
name: string;
icon: string;
}
export const WorkspaceItem = ({ name, icon }: WorkspaceItemProps) => {
return (
<StyledWrapper>
<WorkspaceItemAvatar alt={name} src={icon}>
{name.charAt(0)}
</WorkspaceItemAvatar>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
<Footer>
<FooterUsers />
<FooterSetting />
</Footer>
</StyledWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.sm,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const StyledWrapper = styled(WorkspaceItemWrapper)({
'& .footer-setting': {
display: 'none',
},
':hover .footer-users': {
display: 'none',
},
':hover .footer-setting': {
display: 'block',
},
});
const Footer = styled('div')({
width: '42px',
flex: '0 42px',
fontSize: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: '12px',
});
@@ -0,0 +1 @@
export * from './WorkspaceItem';
@@ -0,0 +1,5 @@
export * from './PrivateWorkspaceItem';
export * from './WorkspaceItem';
export * from './CreateWorkspaceItem';
export * from './ListItem';
export * from './LoginItem';
@@ -0,0 +1,41 @@
import MuiAvatar from '@mui/material/Avatar';
import { styled } from '@/styles';
export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
cursor: 'pointer',
borderRadius: '5px',
padding: '6px 12px',
':hover': {
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
}));
export const PrivateWorkspaceWrapper = styled(WorkspaceItemWrapper)({
padding: '10px 12px',
});
export const LoginItemWrapper = styled(WorkspaceItemWrapper)(({ theme }) => {
return {
padding: '10px 12px',
':hover .login-description': {
color: theme.colors.primaryColor,
},
};
});
export const WorkspaceItemAvatar = styled(MuiAvatar)({
height: '40px',
width: '40px',
});
export const WorkspaceItemContent = styled('div')({
minWidth: 0,
marginLeft: '12px',
flexGrow: 1,
});
@@ -0,0 +1,18 @@
import { Popper } from '@/ui/popper';
import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
import { SelectorPopperContent } from './SelectorPopperContent';
export const WorkspaceSelector = () => {
return (
<Popper
content={<SelectorPopperContent />}
zIndex={1000}
placement="bottom-start"
>
<SelectorWrapper>
<Avatar alt="Affine" />
<WorkspaceName>AFFiNE</WorkspaceName>
</SelectorWrapper>
</Popper>
);
};
@@ -0,0 +1 @@
export * from './WorkspaceSelector';
@@ -0,0 +1,38 @@
import MuiAvatar from '@mui/material/Avatar';
import { styled } from '@/styles';
import { StyledPopperContainer } from '@/ui/shared/Container';
export const SelectorWrapper = styled('div')({
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
});
export const Avatar = styled(MuiAvatar)({
height: '28px',
width: '28px',
});
export const WorkspaceName = styled('span')(({ theme }) => {
return {
marginLeft: '12px',
lineHeight: 1,
fontSize: '18px',
fontWeight: 500,
color: theme.colors.iconColor,
};
});
export const SelectorPopperContainer = styled(StyledPopperContainer)(
({ theme }) => {
return {
width: '334px',
boxShadow: theme.shadow.tooltip,
padding: '24px 12px',
backgroundColor: theme.colors.pageBackground,
fontSize: theme.font.xs,
};
}
);
@@ -4,6 +4,7 @@ import {
StyledArrowButton,
StyledLink,
StyledListItem,
StyledListItemForWorkspace,
StyledNewPageButton,
StyledSliderBar,
StyledSubListItem,
@@ -25,6 +26,7 @@ import { useEditor } from '@/providers/editor-provider';
import { useModal } from '@/providers/global-modal-provider';
import { IconButton } from '@/ui/button';
import { WorkspaceSelector } from './WorkspaceSelector';
import useLocalStorage from '@/hooks/use-local-storage';
import { useTranslation } from '@/libs/i18n';
const FavoriteList = ({ showList }: { showList: boolean }) => {
@@ -69,6 +71,9 @@ export const WorkSpaceSliderBar = () => {
return (
<>
<StyledSliderBar show={show}>
<StyledListItemForWorkspace>
<WorkspaceSelector />
</StyledListItemForWorkspace>
<Tooltip content="Search and quickly jump to a page" placement="right">
<StyledListItem
onClick={() => {
@@ -80,7 +85,7 @@ export const WorkSpaceSliderBar = () => {
</Tooltip>
<Link href={{ pathname: '/page-list/all' }}>
<StyledListItem active={router.pathname === '/page-list/all'}>
<AllPagesIcon /> {t('All pages')}
<AllPagesIcon /> <span>All pages</span>
</StyledListItem>
</Link>
<StyledListItem active={router.pathname === '/page-list/favorite'}>
@@ -67,6 +67,10 @@ export const StyledListItem = styled.button<{ active?: boolean }>(
}
);
export const StyledListItemForWorkspace = styled(StyledListItem)({
height: '52px',
});
export const StyledLink = styled(Link)(({ theme }) => {
return {
flexGrow: 1,
+60 -60
View File
@@ -9,64 +9,64 @@ import fr from './fr.json';
import bn from './bn.json';
export const LOCALES = [
{
id: 1000016008,
name: 'English',
tag: 'en',
originalName: 'English',
flagEmoji: '🇬🇧',
base: true,
completeRate: 1,
res: en,
},
{
id: 1000016009,
name: 'Simplified Chinese',
tag: 'zh-Hans',
originalName: '简体中文',
flagEmoji: '🇨🇳',
base: false,
completeRate: 1,
res: zh_Hans,
},
{
id: 1000016012,
name: 'Traditional Chinese',
tag: 'zh-Hant',
originalName: '繁體中文',
flagEmoji: '🇭🇰',
base: false,
completeRate: 1,
res: zh_Hant,
},
{
id: 1000034005,
name: 'Serbian',
tag: 'sr',
originalName: 'српски',
flagEmoji: '🇷🇸',
base: false,
completeRate: 0.9166666666666666,
res: sr,
},
{
id: 1000034008,
name: 'French',
tag: 'fr',
originalName: 'français',
flagEmoji: '🇫🇷',
base: false,
completeRate: 1,
res: fr,
},
{
id: 1000034010,
name: 'Bangla',
tag: 'bn',
originalName: 'বাংলা',
flagEmoji: '🇧🇩',
base: false,
completeRate: 0.7083333333333334,
res: bn,
},
{
id: 1000016008,
name: 'English',
tag: 'en',
originalName: 'English',
flagEmoji: '🇬🇧',
base: true,
completeRate: 1,
res: en,
},
{
id: 1000016009,
name: 'Simplified Chinese',
tag: 'zh-Hans',
originalName: '简体中文',
flagEmoji: '🇨🇳',
base: false,
completeRate: 1,
res: zh_Hans,
},
{
id: 1000016012,
name: 'Traditional Chinese',
tag: 'zh-Hant',
originalName: '繁體中文',
flagEmoji: '🇭🇰',
base: false,
completeRate: 1,
res: zh_Hant,
},
{
id: 1000034005,
name: 'Serbian',
tag: 'sr',
originalName: 'српски',
flagEmoji: '🇷🇸',
base: false,
completeRate: 0.9166666666666666,
res: sr,
},
{
id: 1000034008,
name: 'French',
tag: 'fr',
originalName: 'français',
flagEmoji: '🇫🇷',
base: false,
completeRate: 1,
res: fr,
},
{
id: 1000034010,
name: 'Bangla',
tag: 'bn',
originalName: 'বাংলা',
flagEmoji: '🇧🇩',
base: false,
completeRate: 0.7083333333333334,
res: bn,
},
] as const;
+80 -82
View File
@@ -44,26 +44,25 @@ import { fetchTolgee } from './request';
* ```
*/
export const getAllProjectLanguages = async (size = 1000) => {
const url = `/languages?size=${size}`;
const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json: {
// eslint-disable-next-line @typescript-eslint/naming-convention
_embedded: {
languages: {
id: number;
name: string;
tag: string;
originalName: string;
flagEmoji: string;
base: boolean;
}[];
};
page: unknown;
} = await resp.json();
return json._embedded.languages;
const url = `/languages?size=${size}`;
const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json: {
_embedded: {
languages: {
id: number;
name: string;
tag: string;
originalName: string;
flagEmoji: string;
base: boolean;
}[];
};
page: unknown;
} = await resp.json();
return json._embedded.languages;
};
/**
@@ -72,13 +71,13 @@ export const getAllProjectLanguages = async (size = 1000) => {
* See https://tolgee.io/api#operation/getTranslations_
*/
export const getTranslations = async () => {
const url = '/translations';
const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
const url = '/translations';
const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
};
/**
@@ -87,25 +86,24 @@ export const getTranslations = async () => {
* See https://tolgee.io/api#operation/getAllTranslations_1
*/
export const getLanguagesTranslations = async <T extends string>(
languages: T
languages: T
) => {
const url = `/translations/${languages}`;
const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json: { [key in T]?: Record<string, string> } = await resp.json();
return json;
const url = `/translations/${languages}`;
const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json: { [key in T]?: Record<string, string> } = await resp.json();
return json;
};
export const getRemoteTranslations = async (languages: string) => {
const translations = await getLanguagesTranslations(languages);
if (!(languages in translations)) {
return {};
}
// The assert is safe because we checked above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return translations[languages]!;
const translations = await getLanguagesTranslations(languages);
if (!(languages in translations)) {
return {};
}
// The assert is safe because we checked above
return translations[languages]!;
};
/**
@@ -114,19 +112,19 @@ export const getRemoteTranslations = async (languages: string) => {
* See https://tolgee.io/api#operation/create_2
*/
export const createsNewKey = async (
key: string,
translations: Record<string, string>
key: string,
translations: Record<string, string>
) => {
const url = '/translations/keys/create';
const resp = await fetchTolgee(url, {
method: 'POST',
body: JSON.stringify({ name: key, translations }),
});
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
const url = '/translations/keys/create';
const resp = await fetchTolgee(url, {
method: 'POST',
body: JSON.stringify({ name: key, translations }),
});
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
};
/**
@@ -135,16 +133,16 @@ export const createsNewKey = async (
* See https://tolgee.io/api#operation/tagKey_1
*/
export const addTag = async (keyId: string, tagName: string) => {
const url = `/keys/${keyId}/tags`;
const resp = await fetchTolgee(url, {
method: 'PUT',
body: JSON.stringify({ name: tagName }),
});
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
const url = `/keys/${keyId}/tags`;
const resp = await fetchTolgee(url, {
method: 'PUT',
body: JSON.stringify({ name: tagName }),
});
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
};
/**
@@ -153,21 +151,21 @@ export const addTag = async (keyId: string, tagName: string) => {
* See https://tolgee.io/api#operation/tagKey_1
*/
export const removeTag = async (keyId: string, tagId: number) => {
const url = `/keys/${keyId}/tags/${tagId}`;
const resp = await fetchTolgee(url, {
method: 'DELETE',
});
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
const url = `/keys/${keyId}/tags/${tagId}`;
const resp = await fetchTolgee(url, {
method: 'DELETE',
});
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
const json = await resp.json();
return json;
};
export const addTagByKey = async (key: string, tag: string) => {
// TODO get key id by key name
// const keyId =
// addTag(keyId, tag);
// TODO get key id by key name
// const keyId =
// addTag(keyId, tag);
};
/**
@@ -176,11 +174,11 @@ export const addTagByKey = async (key: string, tag: string) => {
* See https://tolgee.io/api#operation/export_1
*/
export const exportResources = async () => {
const url = `/export`;
const resp = await fetchTolgee(url);
const url = `/export`;
const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
return resp;
if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
}
return resp;
};
+96 -100
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
// cSpell:ignore Tolgee
import fs from 'node:fs/promises';
import path from 'node:path';
@@ -9,126 +8,123 @@ import type { TranslationRes } from './utils';
const RES_DIR = path.resolve(process.cwd(), 'src', 'resources');
const countKeys = (obj: TranslationRes) => {
let count = 0;
Object.entries(obj).forEach(([key, value]) => {
if (typeof value === 'string') {
count++;
} else {
count += countKeys(value);
}
});
return count;
let count = 0;
Object.entries(obj).forEach(([key, value]) => {
if (typeof value === 'string') {
count++;
} else {
count += countKeys(value);
}
});
return count;
};
const getBaseTranslations = async (baseLanguage: { tag: string }) => {
try {
const baseTranslationsStr = await fs.readFile(
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
{ encoding: 'utf8' }
);
const baseTranslations = JSON.parse(baseTranslationsStr);
return baseTranslations;
} catch (e) {
console.error('base language:', JSON.stringify(baseLanguage));
console.error('Failed to read base language', e);
const translations = await getRemoteTranslations(baseLanguage.tag);
await fs.writeFile(
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
JSON.stringify(translations, null, 4)
);
}
try {
const baseTranslationsStr = await fs.readFile(
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
{ encoding: 'utf8' }
);
const baseTranslations = JSON.parse(baseTranslationsStr);
return baseTranslations;
} catch (e) {
console.error('base language:', JSON.stringify(baseLanguage));
console.error('Failed to read base language', e);
const translations = await getRemoteTranslations(baseLanguage.tag);
await fs.writeFile(
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
JSON.stringify(translations, null, 4)
);
}
};
const main = async () => {
console.log('Loading project languages...');
const languages = await getAllProjectLanguages();
const baseLanguage = languages.find(language => language.base);
if (!baseLanguage) {
console.error(JSON.stringify(languages));
throw new Error('Could not find base language');
}
console.log(
`Loading ${baseLanguage.tag} languages translations as base...`
);
console.log('Loading project languages...');
const languages = await getAllProjectLanguages();
const baseLanguage = languages.find(language => language.base);
if (!baseLanguage) {
console.error(JSON.stringify(languages));
throw new Error('Could not find base language');
}
console.log(`Loading ${baseLanguage.tag} languages translations as base...`);
const baseTranslations = await getBaseTranslations(baseLanguage);
const baseKeyNum = countKeys(baseTranslations);
const languagesWithTranslations = await Promise.all(
languages.map(async language => {
console.log(`Loading ${language.tag} translations...`);
const translations = await getRemoteTranslations(language.tag);
const keyNum = countKeys(translations);
const baseTranslations = await getBaseTranslations(baseLanguage);
const baseKeyNum = countKeys(baseTranslations);
const languagesWithTranslations = await Promise.all(
languages.map(async language => {
console.log(`Loading ${language.tag} translations...`);
const translations = await getRemoteTranslations(language.tag);
const keyNum = countKeys(translations);
return {
...language,
translations,
completeRate: keyNum / baseKeyNum,
};
})
);
return {
...language,
translations,
completeRate: keyNum / baseKeyNum,
};
})
);
const availableLanguages = languagesWithTranslations.filter(
language => language.completeRate > 0
);
const availableLanguages = languagesWithTranslations.filter(
language => language.completeRate > 0
);
availableLanguages
// skip base language
.filter(i => !i.base)
.forEach(async language => {
await fs.writeFile(
path.resolve(RES_DIR, `${language.tag}.json`),
JSON.stringify(
{
// eslint-disable-next-line @typescript-eslint/naming-convention
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.':
'',
...language.translations,
},
null,
4
) + '\n'
);
});
availableLanguages
// skip base language
.filter(i => !i.base)
.forEach(async language => {
await fs.writeFile(
path.resolve(RES_DIR, `${language.tag}.json`),
JSON.stringify(
{
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.':
'',
...language.translations,
},
null,
4
) + '\n'
);
});
console.log('Generating meta data...');
const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
console.log('Generating meta data...');
const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
// Run \`pnpm run download-resources\` to regenerate.
// To overwrite this, please overwrite ${path.basename(__filename)}
${availableLanguages
.map(
language =>
`import ${language.tag.replaceAll('-', '_')} from './${
language.tag
}.json'`
)
.join('\n')}
.map(
language =>
`import ${language.tag.replaceAll('-', '_')} from './${
language.tag
}.json'`
)
.join('\n')}
export const LOCALES = [
${availableLanguages
.map(({ translations, ...language }) =>
JSON.stringify({
...language,
res: '__RES_PLACEHOLDER',
}).replace(
'"__RES_PLACEHOLDER"',
language.tag.replaceAll('-', '_')
)
.map(({ translations, ...language }) =>
JSON.stringify({
...language,
res: '__RES_PLACEHOLDER',
}).replace(
'"__RES_PLACEHOLDER"',
language.tag.replaceAll('-', '_')
)
.join(',\n')}
)
.join(',\n')}
] as const;
`;
await fs.writeFile(
path.resolve(RES_DIR, 'index.ts'),
format(code, {
parser: 'typescript',
singleQuote: true,
trailingComma: 'es5',
tabWidth: 4,
arrowParens: 'avoid',
})
);
console.log('Done');
await fs.writeFile(
path.resolve(RES_DIR, 'index.ts'),
format(code, {
parser: 'typescript',
singleQuote: true,
trailingComma: 'es5',
tabWidth: 4,
arrowParens: 'avoid',
})
);
console.log('Done');
};
main();
+41 -41
View File
@@ -3,53 +3,53 @@ const TOLGEE_API_KEY = process.env['TOLGEE_API_KEY'];
const TOLGEE_API_URL = 'https://i18n.affine.pro';
if (!TOLGEE_API_KEY) {
throw new Error(`Please set "TOLGEE_API_KEY" as environment variable!`);
throw new Error(`Please set "TOLGEE_API_KEY" as environment variable!`);
}
const withTolgee = (
fetch: typeof globalThis.fetch
fetch: typeof globalThis.fetch
): typeof globalThis.fetch => {
const baseUrl = `${TOLGEE_API_URL}/v2/projects`;
const headers = new Headers({
'X-API-Key': TOLGEE_API_KEY,
'Content-Type': 'application/json',
});
const baseUrl = `${TOLGEE_API_URL}/v2/projects`;
const headers = new Headers({
'X-API-Key': TOLGEE_API_KEY,
'Content-Type': 'application/json',
});
const isRequest = (input: RequestInfo | URL): input is Request => {
return typeof input === 'object' && !('href' in input);
};
const isRequest = (input: RequestInfo | URL): input is Request => {
return typeof input === 'object' && !('href' in input);
};
return new Proxy(fetch, {
apply(
target,
thisArg: unknown,
argArray: Parameters<typeof globalThis.fetch>
) {
if (isRequest(argArray[0])) {
// Request
if (!argArray[0].headers) {
argArray[0] = {
...argArray[0],
url: `${baseUrl}${argArray[0].url}`,
headers,
};
}
} else {
// URL or URLLike + ?RequestInit
if (typeof argArray[0] === 'string') {
argArray[0] = `${baseUrl}${argArray[0]}`;
}
if (!argArray[1]) {
argArray[1] = {};
}
if (!argArray[1].headers) {
argArray[1].headers = headers;
}
}
// console.log('fetch', argArray);
return target.apply(thisArg, argArray);
},
});
return new Proxy(fetch, {
apply(
target,
thisArg: unknown,
argArray: Parameters<typeof globalThis.fetch>
) {
if (isRequest(argArray[0])) {
// Request
if (!argArray[0].headers) {
argArray[0] = {
...argArray[0],
url: `${baseUrl}${argArray[0].url}`,
headers,
};
}
} else {
// URL or URLLike + ?RequestInit
if (typeof argArray[0] === 'string') {
argArray[0] = `${baseUrl}${argArray[0]}`;
}
if (!argArray[1]) {
argArray[1] = {};
}
if (!argArray[1].headers) {
argArray[1].headers = headers;
}
}
// console.log('fetch', argArray);
return target.apply(thisArg, argArray);
},
});
};
export const fetchTolgee = withTolgee(globalThis.fetch);
+111 -119
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
// cSpell:ignore Tolgee
import { readFile } from 'fs/promises';
import path from 'path';
@@ -6,10 +5,10 @@ import { addTagByKey, createsNewKey, getRemoteTranslations } from './api';
import type { TranslationRes } from './utils';
const BASE_JSON_PATH = path.resolve(
process.cwd(),
'src',
'resources',
'en.json'
process.cwd(),
'src',
'resources',
'en.json'
);
const BASE_LANGUAGES = 'en' as const;
@@ -23,142 +22,135 @@ const DEPRECATED_TAG_NAME = 'unused' as const;
* ```
*/
const flatRes = (obj: TranslationRes) => {
const getEntries = (o: TranslationRes, prefix = ''): [string, string][] =>
Object.entries(o).flatMap<[string, string]>(([k, v]) =>
typeof v !== 'string'
? getEntries(v, `${prefix}${k}.`)
: [[`${prefix}${k}`, v]]
);
return Object.fromEntries(getEntries(obj));
const getEntries = (o: TranslationRes, prefix = ''): [string, string][] =>
Object.entries(o).flatMap<[string, string]>(([k, v]) =>
typeof v !== 'string'
? getEntries(v, `${prefix}${k}.`)
: [[`${prefix}${k}`, v]]
);
return Object.fromEntries(getEntries(obj));
};
const differenceObject = (
newObj: Record<string, string>,
oldObj: Record<string, string>
newObj: Record<string, string>,
oldObj: Record<string, string>
) => {
const add: string[] = [];
const remove: string[] = [];
const modify: string[] = [];
const both: string[] = [];
const add: string[] = [];
const remove: string[] = [];
const modify: string[] = [];
const both: string[] = [];
Object.keys(newObj).forEach(key => {
if (!(key in oldObj)) {
add.push(key);
} else {
both.push(key);
}
});
Object.keys(newObj).forEach(key => {
if (!(key in oldObj)) {
add.push(key);
} else {
both.push(key);
}
});
Object.keys(oldObj).forEach(key => {
if (!(key in newObj)) {
remove.push(key);
}
});
Object.keys(oldObj).forEach(key => {
if (!(key in newObj)) {
remove.push(key);
}
});
both.forEach(key => {
if (!(key in newObj) || !(key in oldObj)) {
throw new Error('Unreachable');
}
const newVal = newObj[key];
const oldVal = oldObj[key];
if (newVal !== oldVal) {
modify.push(key);
}
});
return { add, remove, modify };
both.forEach(key => {
if (!(key in newObj) || !(key in oldObj)) {
throw new Error('Unreachable');
}
const newVal = newObj[key];
const oldVal = oldObj[key];
if (newVal !== oldVal) {
modify.push(key);
}
});
return { add, remove, modify };
};
function warnDiff(diff: { add: string[]; remove: string[]; modify: string[] }) {
if (diff.add.length) {
console.log('New keys found:', diff.add.join(', '));
//See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
process.env['CI'] &&
console.log(
`::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join(
', '
)}`
);
}
if (diff.remove.length) {
console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', '));
process.env['CI'] &&
console.warn(
`::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join(
', '
)}`
);
}
if (diff.modify.length) {
console.warn(
'[WARN]',
'Inconsistent keys found:',
diff.modify.join(', ')
);
process.env['CI'] &&
console.warn(
`::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join(
', '
)}`
);
}
if (diff.add.length) {
console.log('New keys found:', diff.add.join(', '));
//See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
process.env['CI'] &&
console.log(
`::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join(
', '
)}`
);
}
if (diff.remove.length) {
console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', '));
process.env['CI'] &&
console.warn(
`::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join(
', '
)}`
);
}
if (diff.modify.length) {
console.warn('[WARN]', 'Inconsistent keys found:', diff.modify.join(', '));
process.env['CI'] &&
console.warn(
`::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join(
', '
)}`
);
}
}
const main = async () => {
console.log('Loading local base translations...');
const baseLocalTranslations = JSON.parse(
await readFile(BASE_JSON_PATH, {
encoding: 'utf8',
})
);
const flatLocalTranslations = flatRes(baseLocalTranslations);
console.log(
`Loading local base translations success! Total ${
Object.keys(flatLocalTranslations).length
} keys`
);
console.log('Loading local base translations...');
const baseLocalTranslations = JSON.parse(
await readFile(BASE_JSON_PATH, {
encoding: 'utf8',
})
);
const flatLocalTranslations = flatRes(baseLocalTranslations);
console.log(
`Loading local base translations success! Total ${
Object.keys(flatLocalTranslations).length
} keys`
);
console.log('Fetch remote base translations...');
const baseRemoteTranslations = await getRemoteTranslations(BASE_LANGUAGES);
const flatRemoteTranslations = flatRes(baseRemoteTranslations);
console.log(
`Fetch remote base translations success! Total ${
Object.keys(flatRemoteTranslations).length
} keys`
);
console.log('Fetch remote base translations...');
const baseRemoteTranslations = await getRemoteTranslations(BASE_LANGUAGES);
const flatRemoteTranslations = flatRes(baseRemoteTranslations);
console.log(
`Fetch remote base translations success! Total ${
Object.keys(flatRemoteTranslations).length
} keys`
);
const diff = differenceObject(
flatLocalTranslations,
flatRemoteTranslations
);
const diff = differenceObject(flatLocalTranslations, flatRemoteTranslations);
console.log(''); // new line
warnDiff(diff);
console.log(''); // new line
console.log(''); // new line
warnDiff(diff);
console.log(''); // new line
if (process.argv.slice(2).includes('--check')) {
// check mode
return;
}
if (process.argv.slice(2).includes('--check')) {
// check mode
return;
}
diff.add.forEach(async key => {
const val = flatLocalTranslations[key];
console.log(`Creating new key: ${key} -> ${val}`);
await createsNewKey(key, { [BASE_LANGUAGES]: val });
});
diff.add.forEach(async key => {
const val = flatLocalTranslations[key];
console.log(`Creating new key: ${key} -> ${val}`);
await createsNewKey(key, { [BASE_LANGUAGES]: val });
});
// TODO remove unused tags from used keys
// TODO remove unused tags from used keys
diff.remove.forEach(key => {
// TODO set unused tag
// console.log(`Add ${DEPRECATED_TAG_NAME} to ${key}`);
addTagByKey(key, DEPRECATED_TAG_NAME);
});
diff.remove.forEach(key => {
// TODO set unused tag
// console.log(`Add ${DEPRECATED_TAG_NAME} to ${key}`);
addTagByKey(key, DEPRECATED_TAG_NAME);
});
diff.modify.forEach(key => {
// TODO warn different between local and remote base translations
});
diff.modify.forEach(key => {
// TODO warn different between local and remote base translations
});
// TODO send notification
// TODO send notification
};
main();
+1 -1
View File
@@ -1,3 +1,3 @@
export interface TranslationRes {
[x: string]: string | TranslationRes;
[x: string]: string | TranslationRes;
}
@@ -0,0 +1,10 @@
export const AlreadyJoined = () => {
return (
<div>
<p>
You are already a member of <code>Workspace name</code>
</p>
<button>Open Workspace</button>
</div>
);
};
@@ -0,0 +1,11 @@
export const Confirm = () => {
return (
<div>
<p>Inviter name</p>
<p>
invite you to join in <code>Workspace name</code>
</p>
<button>Join</button>
</div>
);
};
@@ -0,0 +1,8 @@
export const LinkExpired = () => {
return (
<div>
<h1>The current invitation link has expired.</h1>
<a href={location.origin}>Back to home</a>
</div>
);
};
@@ -0,0 +1,21 @@
import { useRouter } from 'next/router';
import { AlreadyJoined } from './AlreadyJoined';
import { Confirm } from './Confirm';
import { LinkExpired } from './LinkExpired';
export const ConfirmInvitation = () => {
const router = useRouter();
// Temporary code. The code should be returned by request.
const { code } = router.query;
const Component = {
'-1': LinkExpired,
0: Confirm,
1: AlreadyJoined,
}[code as string];
return (
<div>
<h1>Confirm Invitation</h1>
{Component ? <Component /> : null}
</div>
);
};
+4
View File
@@ -0,0 +1,4 @@
import NotfoundPage from '@/components/404';
export default function Custom404() {
return <NotfoundPage></NotfoundPage>;
}
+2
View File
@@ -5,6 +5,7 @@ import '../../public/variable.css';
import './temporary.css';
import { EditorProvider } from '@/providers/editor-provider';
import { ModalProvider } from '@/providers/global-modal-provider';
import { AppStateProvider } from '@/providers/app-state-provider';
import { Logger } from '@toeverything/pathfinder-logger';
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
import '@fontsource/space-mono';
@@ -49,6 +50,7 @@ function MyApp({ Component, pageProps }: AppProps) {
<ThemeProvider key="ThemeProvider" />,
<ModalProvider key="ModalProvider" />,
<ConfirmProvider key="ConfirmProvider" />,
<AppStateProvider key="appStateProvider" />,
]}
>
<StyledPage>
@@ -0,0 +1,20 @@
// for dynamic route get workspace id maybe path will change
import { useRouter } from 'next/router';
const Post = () => {
const router = useRouter();
const { workspace_id } = router.query;
return (
<p
style={{
height: 'calc(100vh)',
color: 'gray',
}}
>
workspace_id: {workspace_id},
</p>
);
};
export default Post;
@@ -0,0 +1,8 @@
import type { NextPage } from 'next';
import { ConfirmInvitation } from '../pages-content/ConfirmInvitationPage';
const ConfirmInvitationPage: NextPage = () => {
return <ConfirmInvitation />;
};
export default ConfirmInvitationPage;
@@ -0,0 +1,57 @@
import { createContext, useMemo, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react';
import {
authorizationEvent,
AccessTokenMessage,
getWorkspaces,
} from '@pathfinder/data-services';
import type { Workspace } from '@pathfinder/data-services';
interface AppStateValue {
user: AccessTokenMessage | null;
workspaces: Workspace[];
}
interface AppStateContext extends AppStateValue {
setState: (state: AppStateValue) => void;
}
const AppState = createContext<AppStateContext>({
user: null,
workspaces: [],
setState: () => {},
});
export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
const [state, setState] = useState<AppStateValue>({
user: null,
workspaces: [],
});
useEffect(() => {
const callback = async (user: AccessTokenMessage | null) => {
const workspaces = user ? await getWorkspaces() : [];
setState(state => ({ ...state, user: user, workspaces }));
};
authorizationEvent.onChange(callback);
return () => {
authorizationEvent.removeCallback(callback);
};
}, []);
const context = useMemo(
() => ({
...state,
setState,
}),
[state, setState]
);
return <AppState.Provider value={context}>{children}</AppState.Provider>;
};
export const useAppState = () => {
const state = useContext(AppState);
return state;
};
@@ -4,12 +4,14 @@ import ShortcutsModal from '@/components/shortcuts-modal';
import ContactModal from '@/components/contact-modal';
import QuickSearch from '@/components/quick-search';
import { ImportModal } from '@/components/import';
import { LoginModal } from '@/components/login-modal';
type ModalContextValue = {
triggerShortcutsModal: () => void;
triggerContactModal: () => void;
triggerQuickSearchModal: (visible?: boolean) => void;
triggerImportModal: () => void;
triggerLoginModal: () => void;
};
type ModalContextProps = PropsWithChildren<{}>;
type ModalMap = {
@@ -17,6 +19,7 @@ type ModalMap = {
shortcuts: boolean;
quickSearch: boolean;
import: boolean;
login: boolean;
};
export const ModalContext = createContext<ModalContextValue>({
@@ -24,6 +27,7 @@ export const ModalContext = createContext<ModalContextValue>({
triggerContactModal: () => {},
triggerQuickSearchModal: (visible?) => {},
triggerImportModal: () => {},
triggerLoginModal: () => {},
});
export const useModal = () => useContext(ModalContext);
@@ -36,6 +40,7 @@ export const ModalProvider = ({
shortcuts: false,
quickSearch: false,
import: false,
login: false,
});
const triggerHandler = (key: keyof ModalMap, visible?: boolean) => {
@@ -60,6 +65,9 @@ export const ModalProvider = ({
triggerImportModal: () => {
triggerHandler('import');
},
triggerLoginModal: () => {
triggerHandler('login');
},
}}
>
<ContactModal
@@ -86,6 +94,12 @@ export const ModalProvider = ({
triggerHandler('import', false);
}}
></ImportModal>
<LoginModal
open={modalMap.login}
onClose={() => {
triggerHandler('login', false);
}}
/>
{children}
</ModalContext.Provider>
);
+3
View File
@@ -30,6 +30,7 @@ export const getLightTheme = (
linkColor2: '#6880FF',
linkVisitedColor: '#ABB8FE',
popoverColor: '#4C6275',
inputColor: '#4C6275',
tooltipColor: '#fff',
codeColor: '#517ea6',
quoteColor: '#4C6275',
@@ -105,6 +106,7 @@ export const getDarkTheme = (
linkColor2: '#6880FF',
linkVisitedColor: '#505FAB',
popoverColor: '#C6CBD9',
inputColor: '#C6CBD9',
tooltipColor: '#fff',
codeColor:
editorMode === 'edgeless' ? lightTheme.colors.codeColor : '#BDDBFD',
@@ -150,6 +152,7 @@ export const globalThemeVariables: (
'--affine-link-visited-color': theme.colors.linkVisitedColor,
'--affine-icon-color': theme.colors.iconColor,
'--affine-popover-color': theme.colors.popoverColor,
'--affine-input-color': theme.colors.inputColor,
'--affine-code-color': theme.colors.codeColor,
'--affine-quote-color': theme.colors.quoteColor,
'--affine-selected-color': theme.colors.selectedColor,
+1
View File
@@ -37,6 +37,7 @@ export interface AffineTheme {
linkVisitedColor: string;
iconColor: string;
popoverColor: string;
inputColor: string;
tooltipColor: string;
codeColor: string;
quoteColor: string;
+1 -1
View File
@@ -39,7 +39,7 @@ export type IconButtonProps = {
| typeof SIZE_NORMAL
| [number, number];
disabled?: boolean;
hoverBackground?: string;
hoverBackground?: CSSProperties['background'];
hoverColor?: string;
hoverStyle?: CSSProperties;
children: ReactElement<HTMLAttributes<SVGElement>, 'svg'>;
+1
View File
@@ -1,2 +1,3 @@
export * from './icon-button';
export * from './button';
export * from './text-button';
+65 -1
View File
@@ -21,7 +21,7 @@ export const StyledIconButton = styled('button', {
height: number;
borderRadius: number;
disabled?: boolean;
hoverBackground?: string;
hoverBackground?: CSSProperties['background'];
hoverColor?: string;
hoverStyle?: CSSProperties;
}>(
@@ -68,6 +68,70 @@ export const StyledIconButton = styled('button', {
};
}
);
export const StyledTextButton = styled('button', {
shouldForwardProp: prop => {
return ![
'borderRadius',
'top',
'right',
'width',
'height',
'hoverBackground',
'hoverColor',
'hoverStyle',
].includes(prop);
},
})<
Pick<
ButtonProps,
| 'size'
| 'disabled'
| 'hoverBackground'
| 'hoverColor'
| 'hoverStyle'
| 'shape'
| 'type'
| 'bold'
>
>(
({
theme,
size = 'default',
disabled,
hoverBackground,
hoverColor,
hoverStyle,
bold = false,
shape = 'default',
// TODO: Implement type
type = 'default',
}) => {
const { fontSize, borderRadius, padding, height } = getSize(size);
console.log('size', size, height);
return {
height,
paddingLeft: padding,
paddingRight: padding,
...displayInlineFlex('flex-start', 'center'),
position: 'relative',
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
transition: 'background .15s',
// TODO: Implement circle shape
borderRadius: shape === 'default' ? borderRadius : height / 2,
fontSize,
fontWeight: bold ? '500' : '400',
':hover': {
color: hoverColor ?? theme.colors.primaryColor,
background: hoverBackground ?? theme.colors.hoverBackground,
...(hoverStyle ?? {}),
},
};
}
);
export const StyledButton = styled('button', {
shouldForwardProp: prop => {
return !['hoverBackground', 'hoverColor', 'hoverStyle', 'type'].includes(
@@ -0,0 +1,53 @@
import { cloneElement, Children, forwardRef } from 'react';
import { StyledTextButton } from './styles';
import { ButtonProps } from './interface';
import { getSize } from './utils';
export const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
size = 'default',
disabled = false,
hoverBackground,
hoverColor,
hoverStyle,
shape = 'default',
icon,
type = 'default',
children,
bold = false,
...props
},
ref
) => {
const { iconSize } = getSize(size);
return (
<StyledTextButton
ref={ref}
disabled={disabled}
size={size}
shape={shape}
hoverBackground={hoverBackground}
hoverColor={hoverColor}
hoverStyle={hoverStyle}
// @ts-ignore
type={type}
bold={bold}
{...props}
>
{icon &&
cloneElement(Children.only(icon), {
width: iconSize,
height: iconSize,
className: `affine-button-icon ${icon.props.className ?? ''}`,
})}
{children && <span>{children}</span>}
</StyledTextButton>
);
}
);
TextButton.displayName = 'TextButton';
export default TextButton;
+8
View File
@@ -0,0 +1,8 @@
import MuiDivider from '@mui/material/Divider';
import { styled } from '@/styles';
export const Divider = styled(MuiDivider)(({ theme }) => {
return {
borderColor: theme.colors.borderColor,
};
});
+3
View File
@@ -0,0 +1,3 @@
export * from './input';
import { Input } from './input';
export default Input;
+37
View File
@@ -0,0 +1,37 @@
import { InputHTMLAttributes, useState } from 'react';
import { StyledInput } from './style';
type inputProps = {
value?: string;
placeholder?: string;
disabled?: boolean;
width?: number;
maxLength?: number;
onChange?: (value: string) => void;
};
export const Input = (props: inputProps) => {
const {
disabled,
value: valueProp,
placeholder,
maxLength,
width = 260,
onChange,
} = props;
const [value, setValue] = useState<string>(valueProp || '');
const handleChange: InputHTMLAttributes<HTMLInputElement>['onChange'] = e => {
setValue(e.target.value);
onChange && onChange(e.target.value);
};
return (
<StyledInput
value={value}
disabled={disabled}
placeholder={placeholder}
width={width}
maxLength={maxLength}
onChange={handleChange}
></StyledInput>
);
};
+30
View File
@@ -0,0 +1,30 @@
import { styled } from '@/styles';
export const StyledInput = styled('input')<{
disabled?: boolean;
value?: string;
width: number;
}>(({ theme, width, disabled }) => {
const fontWeight = 400;
const fontSize = '16px';
return {
width: `${width}px`,
lineHeight: '22px',
padding: '8px 12px',
fontWeight,
fontSize,
color: disabled ? theme.colors.disableColor : theme.colors.inputColor,
border: `1px solid`,
borderColor: theme.colors.borderColor, // TODO: check out disableColor,
backgroundColor: theme.colors.popoverBackground,
borderRadius: '10px',
'&::placeholder': {
fontWeight,
fontSize,
color: theme.colors.placeHolderColor,
},
'&:focus': {
borderColor: theme.colors.primaryColor,
},
};
});
@@ -0,0 +1,12 @@
import { describe, test, expect } from '@jest/globals';
import { isMobile } from '../get-is-mobile';
describe('get-is-mobile', () => {
test('get-is-mobile', () => {
expect(
isMobile(
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
)
).toBe(true);
});
});
+15 -13
View File
@@ -1,17 +1,19 @@
// Inspire by https://stackoverflow.com/a/11381730/8415727
export const getIsMobile = function () {
let check = false;
(function (a) {
if (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
a
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
a.substr(0, 4)
)
export function isMobile(userAgent: string) {
return !!(
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
userAgent
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
userAgent.substr(0, 4)
)
check = true;
})(navigator?.userAgent || navigator?.vendor || (window as any)?.opera);
return check;
);
}
export const getIsMobile = function () {
return isMobile(
navigator?.userAgent || navigator?.vendor || (window as any)?.opera
);
};
export default getIsMobile;