mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 18:09:58 +08:00
Merge branch 'feat/filesystem_and_search' of github.com:toeverything/AFFiNE into feat/filesystem_and_search
This commit is contained in:
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: "Custom Tag. Set nightly-latest will publish to development."
|
description: 'Custom Tag. Set nightly-latest will publish to development.'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Lint and Build
|
name: Lint and Build
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
|
environment: development
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -62,6 +63,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm 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
|
- name: Export
|
||||||
run: pnpm export
|
run: pnpm export
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Playwright Tests
|
name: E2E & Unit Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [pathfinder]
|
branches: [pathfinder]
|
||||||
@@ -33,5 +33,8 @@ jobs:
|
|||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run E2E tests
|
||||||
run: pnpm run test:e2e
|
run: pnpm run test:e2e
|
||||||
|
|
||||||
|
- name: Run Unit tests
|
||||||
|
run: pnpm run test:unit
|
||||||
+22
-17
@@ -10,12 +10,17 @@ Use the table of contents icon on the top left corner of this document to get to
|
|||||||
|
|
||||||
## New contributor guide
|
## 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:
|
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)
|
- [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)
|
- [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)
|
- [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)
|
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -47,15 +52,15 @@ For more information about using a codespace for working on GitHub documentation
|
|||||||
|
|
||||||
2. Fork the repository.
|
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.
|
- [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)!
|
- 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:
|
- 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.
|
- [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!
|
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.
|
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.
|
- 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.
|
- 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.
|
- 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.
|
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.
|
- 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).
|
- 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.
|
- 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!
|
### Your PR is merged!
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -8,15 +8,20 @@
|
|||||||
"export": "pnpm --filter @pathfinder/app export",
|
"export": "pnpm --filter @pathfinder/app export",
|
||||||
"start": "pnpm --filter @pathfinder/app start",
|
"start": "pnpm --filter @pathfinder/app start",
|
||||||
"lint": "pnpm --filter @pathfinder/app lint",
|
"lint": "pnpm --filter @pathfinder/app lint",
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test",
|
||||||
|
"test:unit": "jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^29.3.1",
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
"@types/node": "18.7.18",
|
"@types/node": "18.7.18",
|
||||||
"eslint": "8.22.0",
|
"eslint": "8.22.0",
|
||||||
"eslint-config-next": "12.3.1",
|
"eslint-config-next": "12.3.1",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
|
|
||||||
/** @type { import('eslint').Linter.Config } */
|
/** @type { import('eslint').Linter.Config } */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'next/core-web-vitals',
|
'next/core-web-vitals',
|
||||||
'plugin:@next/next/recommended',
|
'plugin:@next/next/recommended',
|
||||||
'plugin:prettier/recommended',
|
'plugin:prettier/recommended',
|
||||||
],
|
// 'next/babel',
|
||||||
rules: {
|
],
|
||||||
'prettier/prettier': 'warn',
|
rules: {
|
||||||
},
|
'prettier/prettier': 'warn',
|
||||||
|
},
|
||||||
|
|
||||||
reportUnusedDisableDirectives: true,
|
reportUnusedDisableDirectives: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
const { getGitVersion, getCommitHash } = require('./scripts/gitInfo');
|
const { getGitVersion, getCommitHash } = require('./scripts/gitInfo');
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
@@ -14,6 +13,15 @@ const nextConfig = {
|
|||||||
VERSION: getGitVersion(),
|
VERSION: getGitVersion(),
|
||||||
COMMIT_HASH: getCommitHash(),
|
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;
|
module.exports = nextConfig;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@mui/icons-material": "^5.10.9",
|
"@mui/icons-material": "^5.10.9",
|
||||||
"@mui/material": "^5.8.6",
|
"@mui/material": "^5.8.6",
|
||||||
"@toeverything/pathfinder-logger": "workspace:@pathfinder/logger@*",
|
"@toeverything/pathfinder-logger": "workspace:@pathfinder/logger@*",
|
||||||
|
"@pathfinder/data-services": "workspace:@pathfinder/data-services@*",
|
||||||
"cmdk": "^0.1.20",
|
"cmdk": "^0.1.20",
|
||||||
"css-spring": "^4.1.0",
|
"css-spring": "^4.1.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { NotFoundTitle, PageContainer } from './styles';
|
||||||
|
|
||||||
|
export const NotfoundPage = () => {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<NotFoundTitle>404 - Page Not Found</NotFoundTitle>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotfoundPage;
|
||||||
@@ -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 { Menu, MenuItem } from '@/ui/menu';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useConfirm } from '@/providers/confirm-provider';
|
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 PopoverContent = () => {
|
||||||
const { editor, mode, setMode } = useEditor();
|
const { editor, mode, setMode } = useEditor();
|
||||||
@@ -131,6 +134,7 @@ const HeaderRight = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SyncIcon />
|
||||||
<ThemeModeSwitch />
|
<ThemeModeSwitch />
|
||||||
<Menu content={<PopoverContent />} placement="bottom-end">
|
<Menu content={<PopoverContent />} placement="bottom-end">
|
||||||
<IconButton>
|
<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',
|
||||||
|
});
|
||||||
+85
@@ -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',
|
||||||
|
};
|
||||||
|
});
|
||||||
+39
@@ -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',
|
||||||
|
};
|
||||||
|
});
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from './CreateWorkspaceItem';
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from './workspace-create';
|
||||||
+55
@@ -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',
|
||||||
|
};
|
||||||
|
});
|
||||||
+58
@@ -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;
|
||||||
+37
@@ -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',
|
||||||
|
});
|
||||||
+48
@@ -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',
|
||||||
|
};
|
||||||
|
});
|
||||||
+44
@@ -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',
|
||||||
|
};
|
||||||
|
});
|
||||||
+17
@@ -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',
|
||||||
|
};
|
||||||
|
});
|
||||||
+25
@@ -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',
|
||||||
|
});
|
||||||
+64
@@ -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',
|
||||||
|
});
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from './WorkspaceItem';
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './PrivateWorkspaceItem';
|
||||||
|
export * from './WorkspaceItem';
|
||||||
|
export * from './CreateWorkspaceItem';
|
||||||
|
export * from './ListItem';
|
||||||
|
export * from './LoginItem';
|
||||||
+41
@@ -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,
|
||||||
|
});
|
||||||
+18
@@ -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,
|
StyledArrowButton,
|
||||||
StyledLink,
|
StyledLink,
|
||||||
StyledListItem,
|
StyledListItem,
|
||||||
|
StyledListItemForWorkspace,
|
||||||
StyledNewPageButton,
|
StyledNewPageButton,
|
||||||
StyledSliderBar,
|
StyledSliderBar,
|
||||||
StyledSubListItem,
|
StyledSubListItem,
|
||||||
@@ -25,6 +26,7 @@ import { useEditor } from '@/providers/editor-provider';
|
|||||||
import { useModal } from '@/providers/global-modal-provider';
|
import { useModal } from '@/providers/global-modal-provider';
|
||||||
|
|
||||||
import { IconButton } from '@/ui/button';
|
import { IconButton } from '@/ui/button';
|
||||||
|
import { WorkspaceSelector } from './WorkspaceSelector';
|
||||||
import useLocalStorage from '@/hooks/use-local-storage';
|
import useLocalStorage from '@/hooks/use-local-storage';
|
||||||
import { useTranslation } from '@/libs/i18n';
|
import { useTranslation } from '@/libs/i18n';
|
||||||
const FavoriteList = ({ showList }: { showList: boolean }) => {
|
const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||||
@@ -69,6 +71,9 @@ export const WorkSpaceSliderBar = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledSliderBar show={show}>
|
<StyledSliderBar show={show}>
|
||||||
|
<StyledListItemForWorkspace>
|
||||||
|
<WorkspaceSelector />
|
||||||
|
</StyledListItemForWorkspace>
|
||||||
<Tooltip content="Search and quickly jump to a page" placement="right">
|
<Tooltip content="Search and quickly jump to a page" placement="right">
|
||||||
<StyledListItem
|
<StyledListItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -80,7 +85,7 @@ export const WorkSpaceSliderBar = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Link href={{ pathname: '/page-list/all' }}>
|
<Link href={{ pathname: '/page-list/all' }}>
|
||||||
<StyledListItem active={router.pathname === '/page-list/all'}>
|
<StyledListItem active={router.pathname === '/page-list/all'}>
|
||||||
<AllPagesIcon /> {t('All pages')}
|
<AllPagesIcon /> <span>All pages</span>
|
||||||
</StyledListItem>
|
</StyledListItem>
|
||||||
</Link>
|
</Link>
|
||||||
<StyledListItem active={router.pathname === '/page-list/favorite'}>
|
<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 }) => {
|
export const StyledLink = styled(Link)(({ theme }) => {
|
||||||
return {
|
return {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
|||||||
@@ -9,64 +9,64 @@ import fr from './fr.json';
|
|||||||
import bn from './bn.json';
|
import bn from './bn.json';
|
||||||
|
|
||||||
export const LOCALES = [
|
export const LOCALES = [
|
||||||
{
|
{
|
||||||
id: 1000016008,
|
id: 1000016008,
|
||||||
name: 'English',
|
name: 'English',
|
||||||
tag: 'en',
|
tag: 'en',
|
||||||
originalName: 'English',
|
originalName: 'English',
|
||||||
flagEmoji: '🇬🇧',
|
flagEmoji: '🇬🇧',
|
||||||
base: true,
|
base: true,
|
||||||
completeRate: 1,
|
completeRate: 1,
|
||||||
res: en,
|
res: en,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1000016009,
|
id: 1000016009,
|
||||||
name: 'Simplified Chinese',
|
name: 'Simplified Chinese',
|
||||||
tag: 'zh-Hans',
|
tag: 'zh-Hans',
|
||||||
originalName: '简体中文',
|
originalName: '简体中文',
|
||||||
flagEmoji: '🇨🇳',
|
flagEmoji: '🇨🇳',
|
||||||
base: false,
|
base: false,
|
||||||
completeRate: 1,
|
completeRate: 1,
|
||||||
res: zh_Hans,
|
res: zh_Hans,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1000016012,
|
id: 1000016012,
|
||||||
name: 'Traditional Chinese',
|
name: 'Traditional Chinese',
|
||||||
tag: 'zh-Hant',
|
tag: 'zh-Hant',
|
||||||
originalName: '繁體中文',
|
originalName: '繁體中文',
|
||||||
flagEmoji: '🇭🇰',
|
flagEmoji: '🇭🇰',
|
||||||
base: false,
|
base: false,
|
||||||
completeRate: 1,
|
completeRate: 1,
|
||||||
res: zh_Hant,
|
res: zh_Hant,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1000034005,
|
id: 1000034005,
|
||||||
name: 'Serbian',
|
name: 'Serbian',
|
||||||
tag: 'sr',
|
tag: 'sr',
|
||||||
originalName: 'српски',
|
originalName: 'српски',
|
||||||
flagEmoji: '🇷🇸',
|
flagEmoji: '🇷🇸',
|
||||||
base: false,
|
base: false,
|
||||||
completeRate: 0.9166666666666666,
|
completeRate: 0.9166666666666666,
|
||||||
res: sr,
|
res: sr,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1000034008,
|
id: 1000034008,
|
||||||
name: 'French',
|
name: 'French',
|
||||||
tag: 'fr',
|
tag: 'fr',
|
||||||
originalName: 'français',
|
originalName: 'français',
|
||||||
flagEmoji: '🇫🇷',
|
flagEmoji: '🇫🇷',
|
||||||
base: false,
|
base: false,
|
||||||
completeRate: 1,
|
completeRate: 1,
|
||||||
res: fr,
|
res: fr,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1000034010,
|
id: 1000034010,
|
||||||
name: 'Bangla',
|
name: 'Bangla',
|
||||||
tag: 'bn',
|
tag: 'bn',
|
||||||
originalName: 'বাংলা',
|
originalName: 'বাংলা',
|
||||||
flagEmoji: '🇧🇩',
|
flagEmoji: '🇧🇩',
|
||||||
base: false,
|
base: false,
|
||||||
completeRate: 0.7083333333333334,
|
completeRate: 0.7083333333333334,
|
||||||
res: bn,
|
res: bn,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -44,26 +44,25 @@ import { fetchTolgee } from './request';
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const getAllProjectLanguages = async (size = 1000) => {
|
export const getAllProjectLanguages = async (size = 1000) => {
|
||||||
const url = `/languages?size=${size}`;
|
const url = `/languages?size=${size}`;
|
||||||
const resp = await fetchTolgee(url);
|
const resp = await fetchTolgee(url);
|
||||||
if (resp.status < 200 || resp.status >= 300) {
|
if (resp.status < 200 || resp.status >= 300) {
|
||||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||||
}
|
}
|
||||||
const json: {
|
const json: {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
_embedded: {
|
||||||
_embedded: {
|
languages: {
|
||||||
languages: {
|
id: number;
|
||||||
id: number;
|
name: string;
|
||||||
name: string;
|
tag: string;
|
||||||
tag: string;
|
originalName: string;
|
||||||
originalName: string;
|
flagEmoji: string;
|
||||||
flagEmoji: string;
|
base: boolean;
|
||||||
base: boolean;
|
}[];
|
||||||
}[];
|
};
|
||||||
};
|
page: unknown;
|
||||||
page: unknown;
|
} = await resp.json();
|
||||||
} = await resp.json();
|
return json._embedded.languages;
|
||||||
return json._embedded.languages;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,13 +71,13 @@ export const getAllProjectLanguages = async (size = 1000) => {
|
|||||||
* See https://tolgee.io/api#operation/getTranslations_
|
* See https://tolgee.io/api#operation/getTranslations_
|
||||||
*/
|
*/
|
||||||
export const getTranslations = async () => {
|
export const getTranslations = async () => {
|
||||||
const url = '/translations';
|
const url = '/translations';
|
||||||
const resp = await fetchTolgee(url);
|
const resp = await fetchTolgee(url);
|
||||||
if (resp.status < 200 || resp.status >= 300) {
|
if (resp.status < 200 || resp.status >= 300) {
|
||||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||||
}
|
}
|
||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
return json;
|
return json;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,25 +86,24 @@ export const getTranslations = async () => {
|
|||||||
* See https://tolgee.io/api#operation/getAllTranslations_1
|
* See https://tolgee.io/api#operation/getAllTranslations_1
|
||||||
*/
|
*/
|
||||||
export const getLanguagesTranslations = async <T extends string>(
|
export const getLanguagesTranslations = async <T extends string>(
|
||||||
languages: T
|
languages: T
|
||||||
) => {
|
) => {
|
||||||
const url = `/translations/${languages}`;
|
const url = `/translations/${languages}`;
|
||||||
const resp = await fetchTolgee(url);
|
const resp = await fetchTolgee(url);
|
||||||
if (resp.status < 200 || resp.status >= 300) {
|
if (resp.status < 200 || resp.status >= 300) {
|
||||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||||
}
|
}
|
||||||
const json: { [key in T]?: Record<string, string> } = await resp.json();
|
const json: { [key in T]?: Record<string, string> } = await resp.json();
|
||||||
return json;
|
return json;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRemoteTranslations = async (languages: string) => {
|
export const getRemoteTranslations = async (languages: string) => {
|
||||||
const translations = await getLanguagesTranslations(languages);
|
const translations = await getLanguagesTranslations(languages);
|
||||||
if (!(languages in translations)) {
|
if (!(languages in translations)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
// The assert is safe because we checked above
|
// The assert is safe because we checked above
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
return translations[languages]!;
|
||||||
return translations[languages]!;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,19 +112,19 @@ export const getRemoteTranslations = async (languages: string) => {
|
|||||||
* See https://tolgee.io/api#operation/create_2
|
* See https://tolgee.io/api#operation/create_2
|
||||||
*/
|
*/
|
||||||
export const createsNewKey = async (
|
export const createsNewKey = async (
|
||||||
key: string,
|
key: string,
|
||||||
translations: Record<string, string>
|
translations: Record<string, string>
|
||||||
) => {
|
) => {
|
||||||
const url = '/translations/keys/create';
|
const url = '/translations/keys/create';
|
||||||
const resp = await fetchTolgee(url, {
|
const resp = await fetchTolgee(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name: key, translations }),
|
body: JSON.stringify({ name: key, translations }),
|
||||||
});
|
});
|
||||||
if (resp.status < 200 || resp.status >= 300) {
|
if (resp.status < 200 || resp.status >= 300) {
|
||||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||||
}
|
}
|
||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
return json;
|
return json;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,16 +133,16 @@ export const createsNewKey = async (
|
|||||||
* See https://tolgee.io/api#operation/tagKey_1
|
* See https://tolgee.io/api#operation/tagKey_1
|
||||||
*/
|
*/
|
||||||
export const addTag = async (keyId: string, tagName: string) => {
|
export const addTag = async (keyId: string, tagName: string) => {
|
||||||
const url = `/keys/${keyId}/tags`;
|
const url = `/keys/${keyId}/tags`;
|
||||||
const resp = await fetchTolgee(url, {
|
const resp = await fetchTolgee(url, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ name: tagName }),
|
body: JSON.stringify({ name: tagName }),
|
||||||
});
|
});
|
||||||
if (resp.status < 200 || resp.status >= 300) {
|
if (resp.status < 200 || resp.status >= 300) {
|
||||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||||
}
|
}
|
||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
return json;
|
return json;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,21 +151,21 @@ export const addTag = async (keyId: string, tagName: string) => {
|
|||||||
* See https://tolgee.io/api#operation/tagKey_1
|
* See https://tolgee.io/api#operation/tagKey_1
|
||||||
*/
|
*/
|
||||||
export const removeTag = async (keyId: string, tagId: number) => {
|
export const removeTag = async (keyId: string, tagId: number) => {
|
||||||
const url = `/keys/${keyId}/tags/${tagId}`;
|
const url = `/keys/${keyId}/tags/${tagId}`;
|
||||||
const resp = await fetchTolgee(url, {
|
const resp = await fetchTolgee(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
if (resp.status < 200 || resp.status >= 300) {
|
if (resp.status < 200 || resp.status >= 300) {
|
||||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||||
}
|
}
|
||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
return json;
|
return json;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addTagByKey = async (key: string, tag: string) => {
|
export const addTagByKey = async (key: string, tag: string) => {
|
||||||
// TODO get key id by key name
|
// TODO get key id by key name
|
||||||
// const keyId =
|
// const keyId =
|
||||||
// addTag(keyId, tag);
|
// addTag(keyId, tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,11 +174,11 @@ export const addTagByKey = async (key: string, tag: string) => {
|
|||||||
* See https://tolgee.io/api#operation/export_1
|
* See https://tolgee.io/api#operation/export_1
|
||||||
*/
|
*/
|
||||||
export const exportResources = async () => {
|
export const exportResources = async () => {
|
||||||
const url = `/export`;
|
const url = `/export`;
|
||||||
const resp = await fetchTolgee(url);
|
const resp = await fetchTolgee(url);
|
||||||
|
|
||||||
if (resp.status < 200 || resp.status >= 300) {
|
if (resp.status < 200 || resp.status >= 300) {
|
||||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
// cSpell:ignore Tolgee
|
// cSpell:ignore Tolgee
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -9,126 +8,123 @@ import type { TranslationRes } from './utils';
|
|||||||
const RES_DIR = path.resolve(process.cwd(), 'src', 'resources');
|
const RES_DIR = path.resolve(process.cwd(), 'src', 'resources');
|
||||||
|
|
||||||
const countKeys = (obj: TranslationRes) => {
|
const countKeys = (obj: TranslationRes) => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
Object.entries(obj).forEach(([key, value]) => {
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
count++;
|
count++;
|
||||||
} else {
|
} else {
|
||||||
count += countKeys(value);
|
count += countKeys(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBaseTranslations = async (baseLanguage: { tag: string }) => {
|
const getBaseTranslations = async (baseLanguage: { tag: string }) => {
|
||||||
try {
|
try {
|
||||||
const baseTranslationsStr = await fs.readFile(
|
const baseTranslationsStr = await fs.readFile(
|
||||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||||
{ encoding: 'utf8' }
|
{ encoding: 'utf8' }
|
||||||
);
|
);
|
||||||
const baseTranslations = JSON.parse(baseTranslationsStr);
|
const baseTranslations = JSON.parse(baseTranslationsStr);
|
||||||
return baseTranslations;
|
return baseTranslations;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('base language:', JSON.stringify(baseLanguage));
|
console.error('base language:', JSON.stringify(baseLanguage));
|
||||||
console.error('Failed to read base language', e);
|
console.error('Failed to read base language', e);
|
||||||
const translations = await getRemoteTranslations(baseLanguage.tag);
|
const translations = await getRemoteTranslations(baseLanguage.tag);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||||
JSON.stringify(translations, null, 4)
|
JSON.stringify(translations, null, 4)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
console.log('Loading project languages...');
|
console.log('Loading project languages...');
|
||||||
const languages = await getAllProjectLanguages();
|
const languages = await getAllProjectLanguages();
|
||||||
const baseLanguage = languages.find(language => language.base);
|
const baseLanguage = languages.find(language => language.base);
|
||||||
if (!baseLanguage) {
|
if (!baseLanguage) {
|
||||||
console.error(JSON.stringify(languages));
|
console.error(JSON.stringify(languages));
|
||||||
throw new Error('Could not find base language');
|
throw new Error('Could not find base language');
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(`Loading ${baseLanguage.tag} languages translations as base...`);
|
||||||
`Loading ${baseLanguage.tag} languages translations as base...`
|
|
||||||
);
|
|
||||||
|
|
||||||
const baseTranslations = await getBaseTranslations(baseLanguage);
|
const baseTranslations = await getBaseTranslations(baseLanguage);
|
||||||
const baseKeyNum = countKeys(baseTranslations);
|
const baseKeyNum = countKeys(baseTranslations);
|
||||||
const languagesWithTranslations = await Promise.all(
|
const languagesWithTranslations = await Promise.all(
|
||||||
languages.map(async language => {
|
languages.map(async language => {
|
||||||
console.log(`Loading ${language.tag} translations...`);
|
console.log(`Loading ${language.tag} translations...`);
|
||||||
const translations = await getRemoteTranslations(language.tag);
|
const translations = await getRemoteTranslations(language.tag);
|
||||||
const keyNum = countKeys(translations);
|
const keyNum = countKeys(translations);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...language,
|
...language,
|
||||||
translations,
|
translations,
|
||||||
completeRate: keyNum / baseKeyNum,
|
completeRate: keyNum / baseKeyNum,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableLanguages = languagesWithTranslations.filter(
|
const availableLanguages = languagesWithTranslations.filter(
|
||||||
language => language.completeRate > 0
|
language => language.completeRate > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
availableLanguages
|
availableLanguages
|
||||||
// skip base language
|
// skip base language
|
||||||
.filter(i => !i.base)
|
.filter(i => !i.base)
|
||||||
.forEach(async language => {
|
.forEach(async language => {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.resolve(RES_DIR, `${language.tag}.json`),
|
path.resolve(RES_DIR, `${language.tag}.json`),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.':
|
||||||
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.':
|
'',
|
||||||
'',
|
...language.translations,
|
||||||
...language.translations,
|
},
|
||||||
},
|
null,
|
||||||
null,
|
4
|
||||||
4
|
) + '\n'
|
||||||
) + '\n'
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Generating meta data...');
|
console.log('Generating meta data...');
|
||||||
const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
// Run \`pnpm run download-resources\` to regenerate.
|
// Run \`pnpm run download-resources\` to regenerate.
|
||||||
// To overwrite this, please overwrite ${path.basename(__filename)}
|
// To overwrite this, please overwrite ${path.basename(__filename)}
|
||||||
${availableLanguages
|
${availableLanguages
|
||||||
.map(
|
.map(
|
||||||
language =>
|
language =>
|
||||||
`import ${language.tag.replaceAll('-', '_')} from './${
|
`import ${language.tag.replaceAll('-', '_')} from './${
|
||||||
language.tag
|
language.tag
|
||||||
}.json'`
|
}.json'`
|
||||||
)
|
)
|
||||||
.join('\n')}
|
.join('\n')}
|
||||||
|
|
||||||
export const LOCALES = [
|
export const LOCALES = [
|
||||||
${availableLanguages
|
${availableLanguages
|
||||||
.map(({ translations, ...language }) =>
|
.map(({ translations, ...language }) =>
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...language,
|
...language,
|
||||||
res: '__RES_PLACEHOLDER',
|
res: '__RES_PLACEHOLDER',
|
||||||
}).replace(
|
}).replace(
|
||||||
'"__RES_PLACEHOLDER"',
|
'"__RES_PLACEHOLDER"',
|
||||||
language.tag.replaceAll('-', '_')
|
language.tag.replaceAll('-', '_')
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.join(',\n')}
|
)
|
||||||
|
.join(',\n')}
|
||||||
] as const;
|
] as const;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.resolve(RES_DIR, 'index.ts'),
|
path.resolve(RES_DIR, 'index.ts'),
|
||||||
format(code, {
|
format(code, {
|
||||||
parser: 'typescript',
|
parser: 'typescript',
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
trailingComma: 'es5',
|
trailingComma: 'es5',
|
||||||
tabWidth: 4,
|
tabWidth: 4,
|
||||||
arrowParens: 'avoid',
|
arrowParens: 'avoid',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
console.log('Done');
|
console.log('Done');
|
||||||
};
|
};
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -3,53 +3,53 @@ const TOLGEE_API_KEY = process.env['TOLGEE_API_KEY'];
|
|||||||
const TOLGEE_API_URL = 'https://i18n.affine.pro';
|
const TOLGEE_API_URL = 'https://i18n.affine.pro';
|
||||||
|
|
||||||
if (!TOLGEE_API_KEY) {
|
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 = (
|
const withTolgee = (
|
||||||
fetch: typeof globalThis.fetch
|
fetch: typeof globalThis.fetch
|
||||||
): typeof globalThis.fetch => {
|
): typeof globalThis.fetch => {
|
||||||
const baseUrl = `${TOLGEE_API_URL}/v2/projects`;
|
const baseUrl = `${TOLGEE_API_URL}/v2/projects`;
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
'X-API-Key': TOLGEE_API_KEY,
|
'X-API-Key': TOLGEE_API_KEY,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
});
|
});
|
||||||
|
|
||||||
const isRequest = (input: RequestInfo | URL): input is Request => {
|
const isRequest = (input: RequestInfo | URL): input is Request => {
|
||||||
return typeof input === 'object' && !('href' in input);
|
return typeof input === 'object' && !('href' in input);
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Proxy(fetch, {
|
return new Proxy(fetch, {
|
||||||
apply(
|
apply(
|
||||||
target,
|
target,
|
||||||
thisArg: unknown,
|
thisArg: unknown,
|
||||||
argArray: Parameters<typeof globalThis.fetch>
|
argArray: Parameters<typeof globalThis.fetch>
|
||||||
) {
|
) {
|
||||||
if (isRequest(argArray[0])) {
|
if (isRequest(argArray[0])) {
|
||||||
// Request
|
// Request
|
||||||
if (!argArray[0].headers) {
|
if (!argArray[0].headers) {
|
||||||
argArray[0] = {
|
argArray[0] = {
|
||||||
...argArray[0],
|
...argArray[0],
|
||||||
url: `${baseUrl}${argArray[0].url}`,
|
url: `${baseUrl}${argArray[0].url}`,
|
||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// URL or URLLike + ?RequestInit
|
// URL or URLLike + ?RequestInit
|
||||||
if (typeof argArray[0] === 'string') {
|
if (typeof argArray[0] === 'string') {
|
||||||
argArray[0] = `${baseUrl}${argArray[0]}`;
|
argArray[0] = `${baseUrl}${argArray[0]}`;
|
||||||
}
|
}
|
||||||
if (!argArray[1]) {
|
if (!argArray[1]) {
|
||||||
argArray[1] = {};
|
argArray[1] = {};
|
||||||
}
|
}
|
||||||
if (!argArray[1].headers) {
|
if (!argArray[1].headers) {
|
||||||
argArray[1].headers = headers;
|
argArray[1].headers = headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log('fetch', argArray);
|
// console.log('fetch', argArray);
|
||||||
return target.apply(thisArg, argArray);
|
return target.apply(thisArg, argArray);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTolgee = withTolgee(globalThis.fetch);
|
export const fetchTolgee = withTolgee(globalThis.fetch);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
// cSpell:ignore Tolgee
|
// cSpell:ignore Tolgee
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -6,10 +5,10 @@ import { addTagByKey, createsNewKey, getRemoteTranslations } from './api';
|
|||||||
import type { TranslationRes } from './utils';
|
import type { TranslationRes } from './utils';
|
||||||
|
|
||||||
const BASE_JSON_PATH = path.resolve(
|
const BASE_JSON_PATH = path.resolve(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'src',
|
'src',
|
||||||
'resources',
|
'resources',
|
||||||
'en.json'
|
'en.json'
|
||||||
);
|
);
|
||||||
const BASE_LANGUAGES = 'en' as const;
|
const BASE_LANGUAGES = 'en' as const;
|
||||||
|
|
||||||
@@ -23,142 +22,135 @@ const DEPRECATED_TAG_NAME = 'unused' as const;
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
const flatRes = (obj: TranslationRes) => {
|
const flatRes = (obj: TranslationRes) => {
|
||||||
const getEntries = (o: TranslationRes, prefix = ''): [string, string][] =>
|
const getEntries = (o: TranslationRes, prefix = ''): [string, string][] =>
|
||||||
Object.entries(o).flatMap<[string, string]>(([k, v]) =>
|
Object.entries(o).flatMap<[string, string]>(([k, v]) =>
|
||||||
typeof v !== 'string'
|
typeof v !== 'string'
|
||||||
? getEntries(v, `${prefix}${k}.`)
|
? getEntries(v, `${prefix}${k}.`)
|
||||||
: [[`${prefix}${k}`, v]]
|
: [[`${prefix}${k}`, v]]
|
||||||
);
|
);
|
||||||
return Object.fromEntries(getEntries(obj));
|
return Object.fromEntries(getEntries(obj));
|
||||||
};
|
};
|
||||||
|
|
||||||
const differenceObject = (
|
const differenceObject = (
|
||||||
newObj: Record<string, string>,
|
newObj: Record<string, string>,
|
||||||
oldObj: Record<string, string>
|
oldObj: Record<string, string>
|
||||||
) => {
|
) => {
|
||||||
const add: string[] = [];
|
const add: string[] = [];
|
||||||
const remove: string[] = [];
|
const remove: string[] = [];
|
||||||
const modify: string[] = [];
|
const modify: string[] = [];
|
||||||
const both: string[] = [];
|
const both: string[] = [];
|
||||||
|
|
||||||
Object.keys(newObj).forEach(key => {
|
Object.keys(newObj).forEach(key => {
|
||||||
if (!(key in oldObj)) {
|
if (!(key in oldObj)) {
|
||||||
add.push(key);
|
add.push(key);
|
||||||
} else {
|
} else {
|
||||||
both.push(key);
|
both.push(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(oldObj).forEach(key => {
|
Object.keys(oldObj).forEach(key => {
|
||||||
if (!(key in newObj)) {
|
if (!(key in newObj)) {
|
||||||
remove.push(key);
|
remove.push(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
both.forEach(key => {
|
both.forEach(key => {
|
||||||
if (!(key in newObj) || !(key in oldObj)) {
|
if (!(key in newObj) || !(key in oldObj)) {
|
||||||
throw new Error('Unreachable');
|
throw new Error('Unreachable');
|
||||||
}
|
}
|
||||||
const newVal = newObj[key];
|
const newVal = newObj[key];
|
||||||
const oldVal = oldObj[key];
|
const oldVal = oldObj[key];
|
||||||
if (newVal !== oldVal) {
|
if (newVal !== oldVal) {
|
||||||
modify.push(key);
|
modify.push(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { add, remove, modify };
|
return { add, remove, modify };
|
||||||
};
|
};
|
||||||
|
|
||||||
function warnDiff(diff: { add: string[]; remove: string[]; modify: string[] }) {
|
function warnDiff(diff: { add: string[]; remove: string[]; modify: string[] }) {
|
||||||
if (diff.add.length) {
|
if (diff.add.length) {
|
||||||
console.log('New keys found:', diff.add.join(', '));
|
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
|
//See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
|
||||||
process.env['CI'] &&
|
process.env['CI'] &&
|
||||||
console.log(
|
console.log(
|
||||||
`::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join(
|
`::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join(
|
||||||
', '
|
', '
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (diff.remove.length) {
|
if (diff.remove.length) {
|
||||||
console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', '));
|
console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', '));
|
||||||
process.env['CI'] &&
|
process.env['CI'] &&
|
||||||
console.warn(
|
console.warn(
|
||||||
`::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join(
|
`::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join(
|
||||||
', '
|
', '
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (diff.modify.length) {
|
if (diff.modify.length) {
|
||||||
console.warn(
|
console.warn('[WARN]', 'Inconsistent keys found:', diff.modify.join(', '));
|
||||||
'[WARN]',
|
process.env['CI'] &&
|
||||||
'Inconsistent keys found:',
|
console.warn(
|
||||||
diff.modify.join(', ')
|
`::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join(
|
||||||
);
|
', '
|
||||||
process.env['CI'] &&
|
)}`
|
||||||
console.warn(
|
);
|
||||||
`::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join(
|
}
|
||||||
', '
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
console.log('Loading local base translations...');
|
console.log('Loading local base translations...');
|
||||||
const baseLocalTranslations = JSON.parse(
|
const baseLocalTranslations = JSON.parse(
|
||||||
await readFile(BASE_JSON_PATH, {
|
await readFile(BASE_JSON_PATH, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const flatLocalTranslations = flatRes(baseLocalTranslations);
|
const flatLocalTranslations = flatRes(baseLocalTranslations);
|
||||||
console.log(
|
console.log(
|
||||||
`Loading local base translations success! Total ${
|
`Loading local base translations success! Total ${
|
||||||
Object.keys(flatLocalTranslations).length
|
Object.keys(flatLocalTranslations).length
|
||||||
} keys`
|
} keys`
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Fetch remote base translations...');
|
console.log('Fetch remote base translations...');
|
||||||
const baseRemoteTranslations = await getRemoteTranslations(BASE_LANGUAGES);
|
const baseRemoteTranslations = await getRemoteTranslations(BASE_LANGUAGES);
|
||||||
const flatRemoteTranslations = flatRes(baseRemoteTranslations);
|
const flatRemoteTranslations = flatRes(baseRemoteTranslations);
|
||||||
console.log(
|
console.log(
|
||||||
`Fetch remote base translations success! Total ${
|
`Fetch remote base translations success! Total ${
|
||||||
Object.keys(flatRemoteTranslations).length
|
Object.keys(flatRemoteTranslations).length
|
||||||
} keys`
|
} keys`
|
||||||
);
|
);
|
||||||
|
|
||||||
const diff = differenceObject(
|
const diff = differenceObject(flatLocalTranslations, flatRemoteTranslations);
|
||||||
flatLocalTranslations,
|
|
||||||
flatRemoteTranslations
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(''); // new line
|
console.log(''); // new line
|
||||||
warnDiff(diff);
|
warnDiff(diff);
|
||||||
console.log(''); // new line
|
console.log(''); // new line
|
||||||
|
|
||||||
if (process.argv.slice(2).includes('--check')) {
|
if (process.argv.slice(2).includes('--check')) {
|
||||||
// check mode
|
// check mode
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
diff.add.forEach(async key => {
|
diff.add.forEach(async key => {
|
||||||
const val = flatLocalTranslations[key];
|
const val = flatLocalTranslations[key];
|
||||||
console.log(`Creating new key: ${key} -> ${val}`);
|
console.log(`Creating new key: ${key} -> ${val}`);
|
||||||
await createsNewKey(key, { [BASE_LANGUAGES]: 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 => {
|
diff.remove.forEach(key => {
|
||||||
// TODO set unused tag
|
// TODO set unused tag
|
||||||
// console.log(`Add ${DEPRECATED_TAG_NAME} to ${key}`);
|
// console.log(`Add ${DEPRECATED_TAG_NAME} to ${key}`);
|
||||||
addTagByKey(key, DEPRECATED_TAG_NAME);
|
addTagByKey(key, DEPRECATED_TAG_NAME);
|
||||||
});
|
});
|
||||||
|
|
||||||
diff.modify.forEach(key => {
|
diff.modify.forEach(key => {
|
||||||
// TODO warn different between local and remote base translations
|
// TODO warn different between local and remote base translations
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO send notification
|
// TODO send notification
|
||||||
};
|
};
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface TranslationRes {
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import NotfoundPage from '@/components/404';
|
||||||
|
export default function Custom404() {
|
||||||
|
return <NotfoundPage></NotfoundPage>;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import '../../public/variable.css';
|
|||||||
import './temporary.css';
|
import './temporary.css';
|
||||||
import { EditorProvider } from '@/providers/editor-provider';
|
import { EditorProvider } from '@/providers/editor-provider';
|
||||||
import { ModalProvider } from '@/providers/global-modal-provider';
|
import { ModalProvider } from '@/providers/global-modal-provider';
|
||||||
|
import { AppStateProvider } from '@/providers/app-state-provider';
|
||||||
import { Logger } from '@toeverything/pathfinder-logger';
|
import { Logger } from '@toeverything/pathfinder-logger';
|
||||||
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
|
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
|
||||||
import '@fontsource/space-mono';
|
import '@fontsource/space-mono';
|
||||||
@@ -49,6 +50,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||||||
<ThemeProvider key="ThemeProvider" />,
|
<ThemeProvider key="ThemeProvider" />,
|
||||||
<ModalProvider key="ModalProvider" />,
|
<ModalProvider key="ModalProvider" />,
|
||||||
<ConfirmProvider key="ConfirmProvider" />,
|
<ConfirmProvider key="ConfirmProvider" />,
|
||||||
|
<AppStateProvider key="appStateProvider" />,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<StyledPage>
|
<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 ContactModal from '@/components/contact-modal';
|
||||||
import QuickSearch from '@/components/quick-search';
|
import QuickSearch from '@/components/quick-search';
|
||||||
import { ImportModal } from '@/components/import';
|
import { ImportModal } from '@/components/import';
|
||||||
|
import { LoginModal } from '@/components/login-modal';
|
||||||
|
|
||||||
type ModalContextValue = {
|
type ModalContextValue = {
|
||||||
triggerShortcutsModal: () => void;
|
triggerShortcutsModal: () => void;
|
||||||
triggerContactModal: () => void;
|
triggerContactModal: () => void;
|
||||||
triggerQuickSearchModal: (visible?: boolean) => void;
|
triggerQuickSearchModal: (visible?: boolean) => void;
|
||||||
triggerImportModal: () => void;
|
triggerImportModal: () => void;
|
||||||
|
triggerLoginModal: () => void;
|
||||||
};
|
};
|
||||||
type ModalContextProps = PropsWithChildren<{}>;
|
type ModalContextProps = PropsWithChildren<{}>;
|
||||||
type ModalMap = {
|
type ModalMap = {
|
||||||
@@ -17,6 +19,7 @@ type ModalMap = {
|
|||||||
shortcuts: boolean;
|
shortcuts: boolean;
|
||||||
quickSearch: boolean;
|
quickSearch: boolean;
|
||||||
import: boolean;
|
import: boolean;
|
||||||
|
login: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModalContext = createContext<ModalContextValue>({
|
export const ModalContext = createContext<ModalContextValue>({
|
||||||
@@ -24,6 +27,7 @@ export const ModalContext = createContext<ModalContextValue>({
|
|||||||
triggerContactModal: () => {},
|
triggerContactModal: () => {},
|
||||||
triggerQuickSearchModal: (visible?) => {},
|
triggerQuickSearchModal: (visible?) => {},
|
||||||
triggerImportModal: () => {},
|
triggerImportModal: () => {},
|
||||||
|
triggerLoginModal: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useModal = () => useContext(ModalContext);
|
export const useModal = () => useContext(ModalContext);
|
||||||
@@ -36,6 +40,7 @@ export const ModalProvider = ({
|
|||||||
shortcuts: false,
|
shortcuts: false,
|
||||||
quickSearch: false,
|
quickSearch: false,
|
||||||
import: false,
|
import: false,
|
||||||
|
login: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const triggerHandler = (key: keyof ModalMap, visible?: boolean) => {
|
const triggerHandler = (key: keyof ModalMap, visible?: boolean) => {
|
||||||
@@ -60,6 +65,9 @@ export const ModalProvider = ({
|
|||||||
triggerImportModal: () => {
|
triggerImportModal: () => {
|
||||||
triggerHandler('import');
|
triggerHandler('import');
|
||||||
},
|
},
|
||||||
|
triggerLoginModal: () => {
|
||||||
|
triggerHandler('login');
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContactModal
|
<ContactModal
|
||||||
@@ -86,6 +94,12 @@ export const ModalProvider = ({
|
|||||||
triggerHandler('import', false);
|
triggerHandler('import', false);
|
||||||
}}
|
}}
|
||||||
></ImportModal>
|
></ImportModal>
|
||||||
|
<LoginModal
|
||||||
|
open={modalMap.login}
|
||||||
|
onClose={() => {
|
||||||
|
triggerHandler('login', false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</ModalContext.Provider>
|
</ModalContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const getLightTheme = (
|
|||||||
linkColor2: '#6880FF',
|
linkColor2: '#6880FF',
|
||||||
linkVisitedColor: '#ABB8FE',
|
linkVisitedColor: '#ABB8FE',
|
||||||
popoverColor: '#4C6275',
|
popoverColor: '#4C6275',
|
||||||
|
inputColor: '#4C6275',
|
||||||
tooltipColor: '#fff',
|
tooltipColor: '#fff',
|
||||||
codeColor: '#517ea6',
|
codeColor: '#517ea6',
|
||||||
quoteColor: '#4C6275',
|
quoteColor: '#4C6275',
|
||||||
@@ -105,6 +106,7 @@ export const getDarkTheme = (
|
|||||||
linkColor2: '#6880FF',
|
linkColor2: '#6880FF',
|
||||||
linkVisitedColor: '#505FAB',
|
linkVisitedColor: '#505FAB',
|
||||||
popoverColor: '#C6CBD9',
|
popoverColor: '#C6CBD9',
|
||||||
|
inputColor: '#C6CBD9',
|
||||||
tooltipColor: '#fff',
|
tooltipColor: '#fff',
|
||||||
codeColor:
|
codeColor:
|
||||||
editorMode === 'edgeless' ? lightTheme.colors.codeColor : '#BDDBFD',
|
editorMode === 'edgeless' ? lightTheme.colors.codeColor : '#BDDBFD',
|
||||||
@@ -150,6 +152,7 @@ export const globalThemeVariables: (
|
|||||||
'--affine-link-visited-color': theme.colors.linkVisitedColor,
|
'--affine-link-visited-color': theme.colors.linkVisitedColor,
|
||||||
'--affine-icon-color': theme.colors.iconColor,
|
'--affine-icon-color': theme.colors.iconColor,
|
||||||
'--affine-popover-color': theme.colors.popoverColor,
|
'--affine-popover-color': theme.colors.popoverColor,
|
||||||
|
'--affine-input-color': theme.colors.inputColor,
|
||||||
'--affine-code-color': theme.colors.codeColor,
|
'--affine-code-color': theme.colors.codeColor,
|
||||||
'--affine-quote-color': theme.colors.quoteColor,
|
'--affine-quote-color': theme.colors.quoteColor,
|
||||||
'--affine-selected-color': theme.colors.selectedColor,
|
'--affine-selected-color': theme.colors.selectedColor,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface AffineTheme {
|
|||||||
linkVisitedColor: string;
|
linkVisitedColor: string;
|
||||||
iconColor: string;
|
iconColor: string;
|
||||||
popoverColor: string;
|
popoverColor: string;
|
||||||
|
inputColor: string;
|
||||||
tooltipColor: string;
|
tooltipColor: string;
|
||||||
codeColor: string;
|
codeColor: string;
|
||||||
quoteColor: string;
|
quoteColor: string;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export type IconButtonProps = {
|
|||||||
| typeof SIZE_NORMAL
|
| typeof SIZE_NORMAL
|
||||||
| [number, number];
|
| [number, number];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hoverBackground?: string;
|
hoverBackground?: CSSProperties['background'];
|
||||||
hoverColor?: string;
|
hoverColor?: string;
|
||||||
hoverStyle?: CSSProperties;
|
hoverStyle?: CSSProperties;
|
||||||
children: ReactElement<HTMLAttributes<SVGElement>, 'svg'>;
|
children: ReactElement<HTMLAttributes<SVGElement>, 'svg'>;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './icon-button';
|
export * from './icon-button';
|
||||||
export * from './button';
|
export * from './button';
|
||||||
|
export * from './text-button';
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const StyledIconButton = styled('button', {
|
|||||||
height: number;
|
height: number;
|
||||||
borderRadius: number;
|
borderRadius: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hoverBackground?: string;
|
hoverBackground?: CSSProperties['background'];
|
||||||
hoverColor?: string;
|
hoverColor?: string;
|
||||||
hoverStyle?: CSSProperties;
|
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', {
|
export const StyledButton = styled('button', {
|
||||||
shouldForwardProp: prop => {
|
shouldForwardProp: prop => {
|
||||||
return !['hoverBackground', 'hoverColor', 'hoverStyle', 'type'].includes(
|
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;
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './input';
|
||||||
|
import { Input } from './input';
|
||||||
|
export default Input;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
// Inspire by https://stackoverflow.com/a/11381730/8415727
|
// Inspire by https://stackoverflow.com/a/11381730/8415727
|
||||||
export const getIsMobile = function () {
|
export function isMobile(userAgent: string) {
|
||||||
let check = false;
|
return !!(
|
||||||
(function (a) {
|
/(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(
|
||||||
if (
|
userAgent
|
||||||
/(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(
|
||||||
) ||
|
userAgent.substr(0, 4)
|
||||||
/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)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
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;
|
export default getIsMobile;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Generated
+2724
-73
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user