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
+10 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: "Custom Tag. Set nightly-latest will publish to development."
description: 'Custom Tag. Set nightly-latest will publish to development.'
required: true
type: string
@@ -20,6 +20,7 @@ jobs:
build:
name: Lint and Build
runs-on: self-hosted
environment: development
steps:
- uses: actions/checkout@v2
@@ -62,6 +63,14 @@ jobs:
- name: Build
run: pnpm build
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
- name: Export
run: pnpm export
@@ -1,4 +1,4 @@
name: Playwright Tests
name: E2E & Unit Tests
on:
push:
branches: [pathfinder]
@@ -33,5 +33,8 @@ jobs:
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
- name: Run E2E tests
run: pnpm run test:e2e
- name: Run Unit tests
run: pnpm run test:unit
+22 -17
View File
@@ -10,12 +10,17 @@ Use the table of contents icon on the top left corner of this document to get to
## New contributor guide
Currently we have two versions of AFFiNE:
- [AFFiNE Pre-Alpha](https://livedemo.affine.pro/). This version users the branch `master`, however is no longer actively developed and will be archived in the future.
- [AFFiNE Alpha](https://pathfinder.affine.pro/). This version uses the 'pathfinder' branch, this is the latest version under active development. We plan to update this to the master branch in the near future.
To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions:
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
## Getting started
@@ -47,15 +52,15 @@ For more information about using a codespace for working on GitHub documentation
2. Fork the repository.
- Using GitHub Desktop:
- Using GitHub Desktop:
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
- Using the command line:
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
- Using the command line:
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
3. Install or update to **Node.js v16**. For more information, see [the development guide](contributing/development.md).
3. Install or update to **Node.js v16**.
4. Create a working branch and start with your changes!
@@ -71,13 +76,13 @@ Once your changes are ready, don't forget to self-review to speed up the review
When you're finished with the changes, create a pull request, also known as a PR.
- Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request.
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
Once you submit your PR, a Docs team member will review your proposal. We may ask questions or request for additional information.
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
- If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues.
- Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request.
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
Once you submit your PR, a Docs team member will review your proposal. We may ask questions or request for additional information.
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
- If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues.
### Your PR is merged!
+19
View File
@@ -0,0 +1,19 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
projects: ['<rootDir>/packages/app'],
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
};
+7 -2
View File
@@ -8,15 +8,20 @@
"export": "pnpm --filter @pathfinder/app export",
"start": "pnpm --filter @pathfinder/app start",
"lint": "pnpm --filter @pathfinder/app lint",
"test:e2e": "playwright test"
"test:e2e": "playwright test",
"test:unit": "jest"
},
"devDependencies": {
"@jest/globals": "^29.3.1",
"@playwright/test": "^1.28.1",
"@types/node": "18.7.18",
"eslint": "8.22.0",
"eslint-config-next": "12.3.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.7.1"
"jest": "^29.3.1",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"typescript": "^4.9.3"
}
}
+10 -9
View File
@@ -5,14 +5,15 @@
/** @type { import('eslint').Linter.Config } */
module.exports = {
extends: [
'next/core-web-vitals',
'plugin:@next/next/recommended',
'plugin:prettier/recommended',
],
rules: {
'prettier/prettier': 'warn',
},
extends: [
'next/core-web-vitals',
'plugin:@next/next/recommended',
'plugin:prettier/recommended',
// 'next/babel',
],
rules: {
'prettier/prettier': 'warn',
},
reportUnusedDisableDirectives: true,
reportUnusedDisableDirectives: true,
};
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
+9 -1
View File
@@ -1,4 +1,3 @@
// @ts-check
const { getGitVersion, getCommitHash } = require('./scripts/gitInfo');
/** @type {import('next').NextConfig} */
@@ -14,6 +13,15 @@ const nextConfig = {
VERSION: getGitVersion(),
COMMIT_HASH: getCommitHash(),
},
// XXX not test yet
rewrites: async () => {
return [
{
source: '/api/:path*',
destination: 'http://100.77.180.48:11001/api/:path*',
},
];
},
};
module.exports = nextConfig;
+1
View File
@@ -23,6 +23,7 @@
"@mui/icons-material": "^5.10.9",
"@mui/material": "^5.8.6",
"@toeverything/pathfinder-logger": "workspace:@pathfinder/logger@*",
"@pathfinder/data-services": "workspace:@pathfinder/data-services@*",
"cmdk": "^0.1.20",
"css-spring": "^4.1.0",
"dayjs": "^1.11.7",
+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;
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@pathfinder/data-services",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"exports": {
"./src/*": "./dist/src/*.js",
".": "./dist/src/index.js"
},
"scripts": {
"build": "tsc --project ./tsconfig.json"
},
"keywords": [],
"author": "",
"repository": {
"type": "git",
"url": "git+https://github.com/toeverything/AFFINE.git"
},
"devDependencies": {
"typescript": "^4.8.4"
},
"dependencies": {
"axios": "^1.2.0",
"encoding": "^0.1.13",
"firebase": "^9.13.0",
"swr": "^2.0.0"
}
}
+41
View File
@@ -0,0 +1,41 @@
import { initializeApp } from 'firebase/app';
import {
getAuth,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
GoogleAuthProvider,
signInWithPopup,
} from 'firebase/auth';
import type { User } from 'firebase/auth';
/**
* firebaseConfig reference: https://firebase.google.com/docs/web/setup#add_firebase_to_your_app
*/
const app = initializeApp({
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
});
export const firebaseAuth = getAuth(app);
const signUp = (email: string, password: string) => {
return createUserWithEmailAndPassword(firebaseAuth, email, password);
};
const signIn = (email: string, password: string) => {
return signInWithEmailAndPassword(firebaseAuth, email, password);
};
const googleAuthProvider = new GoogleAuthProvider();
export const signInWithGoogle = () => {
return signInWithPopup(firebaseAuth, googleAuthProvider);
};
export const onAuthStateChanged = (callback: (user: User | null) => void) => {
firebaseAuth.onAuthStateChanged(callback);
};
+3
View File
@@ -0,0 +1,3 @@
export { signInWithGoogle, onAuthStateChanged } from './auth';
export * from './request';
export * from './sdks';
@@ -0,0 +1,9 @@
export class ServiceError extends Error {
public message: string;
public code: string;
constructor(code: string, message: string) {
super(message);
this.message = message;
this.code = code;
}
}
@@ -0,0 +1,19 @@
import axios from 'axios';
import { setAuthorization } from './request';
import { handleResponseError } from './response';
declare module 'axios' {
interface AxiosRequestConfig {
/**
* If true, request will send with Authorization header.
*/
withAuthorization?: boolean;
}
}
export const request = axios.create({
withAuthorization: true,
});
request.interceptors.request.use(setAuthorization);
request.interceptors.response.use(handleResponseError);
@@ -0,0 +1,4 @@
export { request } from './axios';
export { ServiceError } from './ServiceError';
export { setToken, authorizationEvent } from './request';
export type { AccessTokenMessage } from './request';
@@ -0,0 +1,99 @@
import type { AxiosRequestConfig } from 'axios';
import { login } from '../../../sdks';
import { AuthorizationEvent } from './events';
import type { AccessTokenMessage } from './types';
const TOKEN_KEY = 'affine_token';
export const authorizationEvent = new AuthorizationEvent();
authorizationEvent.triggerChange(
parseAccessToken(getToken()?.accessToken || '')
);
interface Token {
accessToken: string;
refreshToken: string;
}
/**
* set null to clear token
*/
export function setToken(token: Token | null): void {
if (token === null) {
window.localStorage.removeItem(TOKEN_KEY);
authorizationEvent.triggerChange(null);
return;
}
window.localStorage.setItem(TOKEN_KEY, JSON.stringify(token));
authorizationEvent.triggerChange(parseAccessToken(token.accessToken));
}
function getToken(): { accessToken: string; refreshToken: string } | null {
try {
return JSON.parse(window.localStorage.getItem(TOKEN_KEY) || '');
} catch (error) {
return null;
}
}
function parseAccessToken(token: string): AccessTokenMessage | null {
try {
const message: AccessTokenMessage = JSON.parse(
window.atob(token.split('.')[1])
);
return message;
} catch (error) {
return null;
}
}
function isAccessTokenExpired(token: string) {
const message = parseAccessToken(token);
if (!message) {
return true;
}
return Date.now() - message.create_at > message.exp;
}
async function isLoggedIn(): Promise<boolean> {
const token = getToken();
if (!token) {
return false;
}
return isAccessTokenExpired(token.accessToken);
}
let refreshingToken: ReturnType<typeof login> | undefined;
export async function setAuthorization(config: AxiosRequestConfig<unknown>) {
if (!config.headers) {
config.headers = {};
}
if (config.withAuthorization) {
let token = getToken();
if (!token) {
throw new Error('No authorization token.');
}
if (isAccessTokenExpired(token.accessToken)) {
if (!refreshingToken) {
refreshingToken = login({
type: 'Refresh',
token: token.refreshToken,
});
}
const newToken = await refreshingToken;
token = {
accessToken: newToken.token,
refreshToken: newToken.refresh,
};
setToken(token);
refreshingToken = undefined;
}
if (!config.headers) {
config.headers = {};
}
config.headers['Authorization'] = token.accessToken;
}
return config;
}
@@ -0,0 +1,28 @@
import type { AccessTokenMessage } from './types';
type Callback = (user: AccessTokenMessage | null) => void;
export class AuthorizationEvent {
private callbacks: Callback[] = [];
private lastState: AccessTokenMessage | null = null;
/**
* Callback will execute when call this function.
*/
onChange(callback: Callback) {
this.callbacks.push(callback);
callback(this.lastState);
}
triggerChange(user: AccessTokenMessage | null) {
this.lastState = user;
this.callbacks.forEach(callback => callback(user));
}
removeCallback(callback: Callback) {
const index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
}
}
@@ -0,0 +1,2 @@
export * from './authorization';
export type { AccessTokenMessage } from './types';
@@ -0,0 +1,8 @@
export interface AccessTokenMessage {
create_at: number;
exp: number;
email: string;
id: number;
name: string;
avatar_url: string;
}
@@ -0,0 +1,6 @@
export {
setAuthorization,
setToken,
authorizationEvent,
} from './authorization';
export type { AccessTokenMessage } from './authorization';
@@ -0,0 +1,15 @@
import type { AxiosResponse } from 'axios';
import { ServiceError } from '../ServiceError';
export function handleResponseError(response: AxiosResponse<any, any>) {
const { data, status } = response;
if (status === 200) {
if (data.error) {
// TODO - common error handling
const error = new ServiceError(data.error.message, data.error.code);
throw error;
}
return response;
}
return response;
}
@@ -0,0 +1 @@
export { handleResponseError } from './handle-error';
@@ -0,0 +1,90 @@
import useSWR, { SWRConfiguration } from 'swr';
import { request, ServiceError } from '../request';
import type {
RequestAcceptInviting,
RequestInviteCollaborator,
RequestRemoveCollaborator,
ResponseInviteCollaborator,
} from './types';
const COLLABORATOR_INVITE_URL = '/api/account/invite';
const REMOVE_COLLABORATOR_URL = '/api/account/remove_collaborator';
async function doInviteCollaborator(
url: string,
req: RequestInviteCollaborator
) {
const { data } = await request.post<ResponseInviteCollaborator>(url, req);
return data;
}
export async function inviteCollaborator(req: RequestInviteCollaborator) {
return await doInviteCollaborator(COLLABORATOR_INVITE_URL, req);
}
async function doRemoveCollaborator(
url: string,
req: RequestRemoveCollaborator
) {
return request.post(url, req);
}
export async function removeCollaborator(req: RequestRemoveCollaborator) {
return await doRemoveCollaborator(REMOVE_COLLABORATOR_URL, req);
}
function doAcceptInviting(url: string, req: RequestAcceptInviting) {
return request.post(url, req);
}
export async function acceptInviting(req: RequestAcceptInviting) {
return await doAcceptInviting(COLLABORATOR_INVITE_URL, req);
}
export function useInviteCollaborator(
req: RequestInviteCollaborator,
config?: SWRConfiguration
) {
const { data, error } = useSWR<ResponseInviteCollaborator, ServiceError>(
[COLLABORATOR_INVITE_URL, req],
doInviteCollaborator,
config
);
return {
data,
isLoading: !error && !data,
isError: error,
};
}
export function useRemoveCollaborator(
req: RequestRemoveCollaborator,
config?: SWRConfiguration
) {
const { data, error } = useSWR<unknown, ServiceError>(
[REMOVE_COLLABORATOR_URL, req],
doRemoveCollaborator,
config
);
return {
data,
isLoading: !error && !data,
isError: error,
};
}
export function useAcceptInviting(
req: RequestAcceptInviting,
config?: SWRConfiguration
) {
const { data, error } = useSWR<unknown, ServiceError>(
[COLLABORATOR_INVITE_URL, req],
doAcceptInviting,
config
);
return {
data,
isLoading: !error && !data,
isError: error,
};
}
+4
View File
@@ -0,0 +1,4 @@
export * from './workspace';
export * from './workspace.hook';
export * from './user';
export * from './user.hook';
@@ -0,0 +1,33 @@
enum WorkspacePermission {
read = 'read',
write = 'write',
}
export type RequestInviteCollaborator = {
email: string;
workspace_id: string;
};
export type ResponseInviteCollaborator = {
inviting_code: string;
};
export type RequestRemoveCollaborator = {
user_id: string;
workspace_id: string;
};
export type ResponseRemoveCollaborator = {
workspace_id: string;
};
export type RequestAcceptInviting = {
inviting_code: string;
};
export type ResponseAcceptInviting = {
workspace_id: string;
name: string;
avatar_url: string;
permissions: WorkspacePermission;
};
@@ -0,0 +1,2 @@
export type CommonError = { error: { code: string; message: string } };
export type MayError = Partial<CommonError>;
@@ -0,0 +1,3 @@
export * from './common';
export * from './workspace';
export * from './account';
@@ -0,0 +1,32 @@
enum WorkspaceType {
'personal' = 'personal',
'team' = 'team',
}
export type Workspace = {
id: string;
name: string;
workspace_type: WorkspaceType;
avatar_url?: string;
public: boolean;
};
export type ResponseGetWorkspaces = {
create: Array<Workspace>;
read: Array<Workspace>;
write: Array<Workspace>;
};
export type RequestCreateWorkspace = {
name: string;
avatar_url?: string;
workspace_type: WorkspaceType;
};
export type RequestUpdateWorkspace = {
workspace_type?: WorkspaceType;
avatar_url?: string;
public?: boolean;
};
export type ResponseCreateWorkspace = any;
@@ -0,0 +1,45 @@
import useSWR from 'swr';
import type { SWRConfiguration } from 'swr';
import { login, getUserByEmail } from './user';
import type {
LoginParams,
LoginResponse,
GetUserByEmailParams,
User,
} from './user';
export const LOGIN_SWR_KEY = 'user.token';
export function useLogin(params: LoginParams, config?: SWRConfiguration) {
const { data, error, isValidating, isLoading, mutate } =
useSWR<LoginResponse>(
[LOGIN_SWR_KEY, params],
([_, params]) => login(params),
config
);
return {
loading: isLoading,
data,
error,
mutate,
};
}
export const GET_USER_BY_EMAIL_SWR_TOKEN = 'user.getUserByEmail';
export function useGetUserByEmail(
params: GetUserByEmailParams,
config?: SWRConfiguration
) {
const { data, error, isLoading, mutate } = useSWR<User | null>(
[GET_USER_BY_EMAIL_SWR_TOKEN, params],
([_, params]) => getUserByEmail(params),
config
);
return {
loading: isLoading,
data,
error,
mutate,
};
}
+60
View File
@@ -0,0 +1,60 @@
import { request } from '../request';
export interface ExchangeToken {
type: 'Google';
/**
* Token from firebase.
*/
token: string;
}
export interface RefreshToken {
type: 'Refresh';
token: string;
}
export type LoginParams = ExchangeToken | RefreshToken;
export interface LoginResponse {
/**
* JWT, expires in a very short time
*/
token: string;
/**
* Refresh token
*/
refresh: string;
}
export async function login(params: LoginParams): Promise<LoginResponse> {
const data = await request<LoginResponse>({
url: '/api/user/token',
method: 'POST',
data: params,
withAuthorization: false,
});
return data.data;
}
export interface GetUserByEmailParams {
email: string;
}
export interface User {
id: number;
name: string;
email: string;
avatar_url: string;
create_at: string;
}
export async function getUserByEmail(
params: GetUserByEmailParams
): Promise<User | null> {
const data = await request<User | null>({
url: '/api/user',
method: 'GET',
params,
});
return data.data;
}
@@ -0,0 +1,106 @@
import useSWR, { SWRConfiguration } from 'swr';
import type {
MayError,
RequestCreateWorkspace,
RequestUpdateWorkspace,
ResponseCreateWorkspace,
ResponseGetWorkspaces,
} from './types';
import { request, ServiceError } from '../request';
const WORKSPACE_URL = '/api/workspace';
async function doGetWorkSpaces(url: string) {
const { data } = await request.get<ResponseGetWorkspaces>(url);
return data;
}
export async function getWorkSpaces() {
return await doGetWorkSpaces(WORKSPACE_URL);
}
async function doCreateWorkspace(url: string, req: RequestCreateWorkspace) {
const { data } = await request.put<ResponseCreateWorkspace>(url, req);
return data;
}
export async function createWorkspace(req: RequestCreateWorkspace) {
doCreateWorkspace(WORKSPACE_URL, req);
}
async function doUpdateWorkspace(url: string, req: RequestUpdateWorkspace) {
const { data } = await request.post<MayError>(url, req);
return data;
}
export function updateWorkspace(id: string, req: RequestUpdateWorkspace) {
return doUpdateWorkspace(`${WORKSPACE_URL}${id}`, req);
}
async function doDeleteWorkspace(url: string) {
const { data } = await request.delete<MayError>(url);
return data;
}
export function deleteWorkspace(id: string) {
return doDeleteWorkspace(`${WORKSPACE_URL}${id}`);
}
export function useGetWorkspaces(config?: SWRConfiguration) {
const { data, error } = useSWR<ResponseGetWorkspaces, ServiceError, string>(
WORKSPACE_URL,
doGetWorkSpaces,
config
);
return {
data,
isLoading: !data,
isError: data && !error,
};
}
export function useCreateWorkspace(
req: RequestCreateWorkspace,
config?: SWRConfiguration
) {
const { data, error } = useSWR<ResponseGetWorkspaces, ServiceError>(
[WORKSPACE_URL, req],
doCreateWorkspace,
config
);
return {
data,
isLoading: !error && !data,
isError: error,
};
}
export function useUpdateWorkspace(
id: string,
req: RequestCreateWorkspace,
config?: SWRConfiguration
) {
const { data, error } = useSWR<MayError, ServiceError>(
[`${WORKSPACE_URL}${id}`, req],
doUpdateWorkspace,
config
);
return {
data,
isLoading: !error && !data,
isError: error,
};
}
export function useDeleteWorkspace(id: string, config?: SWRConfiguration) {
const { data, error } = useSWR<MayError, ServiceError>(
`${WORKSPACE_URL}${id}`,
doDeleteWorkspace,
config
);
return {
data,
isLoading: !error && !data,
isError: error,
};
}
@@ -0,0 +1,72 @@
import useSWR from 'swr';
import type { SWRConfiguration } from 'swr';
import {
getWorkspaceDetail,
updateWorkspace,
deleteWorkspace,
inviteMember,
Workspace,
} from './workspace';
import {
GetWorkspaceDetailParams,
WorkspaceDetail,
UpdateWorkspaceParams,
DeleteWorkspaceParams,
InviteMemberParams,
getWorkspaces,
} from './workspace';
export const GET_WORKSPACE_DETAIL_SWR_TOKEN = 'workspace.getWorkspaceDetail';
export function useGetWorkspaceDetail(
params: GetWorkspaceDetailParams,
config?: SWRConfiguration
) {
const { data, error, isLoading, mutate } = useSWR<WorkspaceDetail | null>(
[GET_WORKSPACE_DETAIL_SWR_TOKEN, params],
([_, params]) => getWorkspaceDetail(params),
config
);
return {
data,
error,
loading: isLoading,
mutate,
};
}
export const GET_WORKSPACES_SWR_TOKEN = 'workspace.getWorkspaces';
export function useGetWorkspaces(config?: SWRConfiguration) {
const { data, error, isLoading } = useSWR<Workspace[]>(
[GET_WORKSPACES_SWR_TOKEN],
() => getWorkspaces(),
config
);
return {
data,
error,
loading: isLoading,
};
}
export const UPDATE_WORKSPACE_SWR_TOKEN = 'workspace.updateWorkspace';
/**
* I don't think a hook needed for update workspace.
* If you figure out the scene, please implement this function.
*/
export function useUpdateWorkspace() {}
export const DELETE_WORKSPACE_SWR_TOKEN = 'workspace.deleteWorkspace';
/**
* I don't think a hook needed for delete workspace.
* If you figure out the scene, please implement this function.
*/
export function useDeleteWorkspace() {}
export const INVITE_MEMBER_SWR_TOKEN = 'workspace.inviteMember';
/**
* I don't think a hook needed for invite member.
* If you figure out the scene, please implement this function.
*/
export function useInviteMember() {}
@@ -0,0 +1,186 @@
import { request } from '../request';
import { User } from './user';
export interface GetWorkspaceDetailParams {
id: string;
}
export enum WorkspaceType {
Private = 0,
Normal = 1,
}
export enum PermissionType {
Read = 0,
Write = 1,
Admin = 2,
Owner = 3,
}
export interface Workspace {
id: string;
type: WorkspaceType;
public: boolean;
permission_type: PermissionType;
create_at: number;
}
export async function getWorkspaces(): Promise<Workspace[]> {
const data = await request({
url: '/api/workspace',
method: 'GET',
});
return data.data;
}
export interface WorkspaceDetail extends Workspace {
owner: User;
member_count: number;
}
export async function getWorkspaceDetail(
params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> {
const data = await request<WorkspaceDetail | null>({
url: `/api/workspace/${params.id}`,
method: 'PUT',
});
return data.data;
}
export interface Permission {
id: number;
type: PermissionType;
workspace_id: string;
accepted: boolean;
create_at: number;
}
export interface RegisteredUser extends User {
type: 'Registered';
}
export interface UnregisteredUser {
type: 'Unregistered';
email: string;
}
export interface Member extends Permission {
user: RegisteredUser | UnregisteredUser;
}
export interface GetWorkspaceMembersParams {
id: string;
}
export async function getWorkspaceMembers(
params: GetWorkspaceDetailParams
): Promise<Member[]> {
const data = await request<Member[]>({
url: `/api/workspace/${params.id}/permission`,
method: 'GET',
});
return data.data;
}
export interface CreateWorkspaceParams {
name: string;
avatar: string;
}
export async function createWorkspace(
params: CreateWorkspaceParams
): Promise<void> {
const data = await request({
url: '/api/workspace',
method: 'POST',
data: params,
});
return data.data;
}
export interface UpdateWorkspaceParams {
id: string;
public: boolean;
}
export async function updateWorkspace(
params: UpdateWorkspaceParams
): Promise<void> {
const data = await request({
url: `/api/workspace/${params.id}`,
method: 'POST',
data: {
public: params.public,
},
});
return data.data;
}
export interface DeleteWorkspaceParams {
id: string;
}
export async function deleteWorkspace(
params: DeleteWorkspaceParams
): Promise<void> {
const data = await request({
url: `/api/workspace/${params.id}`,
method: 'DELETE',
});
return data.data;
}
export interface InviteMemberParams {
id: string;
email: string;
}
/**
* Notice: Only support normal(contrast to private) workspace.
*/
export async function inviteMember(params: InviteMemberParams): Promise<void> {
const data = await request({
url: `/api/workspace/${params.id}/permission`,
method: 'POST',
data: {
email: params.email,
},
});
return data.data;
}
export interface RemoveMemberParams {
permissionId: number;
}
export async function removeMember(params: RemoveMemberParams): Promise<void> {
const data = await request({
url: `/api/permission/${params.permissionId}`,
method: 'DELETE',
});
return data.data;
}
export interface AcceptInvitingParams {
invitingCode: string;
}
export async function acceptInviting(
params: AcceptInvitingParams
): Promise<void> {
const data = await request({
url: `/api/invite/${params.invitingCode}`,
method: 'POST',
});
return data.data;
}
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": false,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"experimentalDecorators": true,
"declaration": true,
"baseUrl": ".",
"rootDir": ".",
"outDir": "./dist"
},
"include": ["next-env.d.ts", "src/**/*.ts", "pages/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
+2724 -73
View File
File diff suppressed because it is too large Load Diff