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

This commit is contained in:
DiamondThree
2022-12-22 18:16:46 +08:00
34 changed files with 871 additions and 486 deletions
-19
View File
@@ -1,19 +0,0 @@
// https://eslint.org/docs/latest/user-guide/configuring
// "off" or 0 - turn the rule off
// "warn" or 1 - turn the rule on as a warning (doesnt affect exit code)
// "error" or 2 - turn the rule on as an error (exit code will be 1)
/** @type { import('eslint').Linter.Config } */
module.exports = {
extends: [
'next/core-web-vitals',
'plugin:@next/next/recommended',
'plugin:prettier/recommended',
// 'next/babel',
],
rules: {
'prettier/prettier': 'warn',
},
reportUnusedDisableDirectives: true,
};
+10
View File
@@ -48,5 +48,15 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"typescript": "4.8.3"
},
"eslintConfig": {
"extends": [
"next/core-web-vitals",
"plugin:@next/next/recommended"
],
"rules": {
"prettier/prettier": "warn"
},
"reportUnusedDisableDirectives": true
}
}
@@ -1,4 +1,4 @@
import { signInWithGoogle, login, setToken } from '@pathfinder/data-services';
import { signInWithGoogle } from '@pathfinder/data-services';
import { styled } from '@/styles';
import { Button } from '@/ui/button';
import { useModal } from '@/providers/global-modal-provider';
@@ -10,13 +10,7 @@ export const GoogleLoginButton = () => {
<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,
});
.then(async => {
triggerLoginModal();
})
.catch(error => {
@@ -0,0 +1,79 @@
import Modal, { ModalCloseButton } from '@/ui/modal';
import {
StyledDeleteButtonContainer,
StyledSettingAvatar,
StyledSettingAvatarContent,
StyledSettingInputContainer,
} from './style';
import { useState } from 'react';
import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { Workspace, WorkspaceType } from '@pathfinder/data-services';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
import { StyledSettingH2 } from '../style';
export const GeneralPage = ({
workspace,
owner,
}: {
workspace: Workspace;
owner: WorkspaceDetails[string]['owner'];
}) => {
const { workspaces, user, currentWorkspace } = useAppState();
const [workspaceName, setWorkspaceName] = useState<string>(
workspaces[workspace.id]?.meta.name ||
(workspace.type === WorkspaceType.Private && user
? user.name
: `workspace-${workspace?.id}`)
);
const isOwner = user && owner.id === user.id;
const handleChangeWorkSpaceName = (newName: string) => {
setWorkspaceName(newName);
currentWorkspace?.meta.setName(newName);
workspaces[workspace.id]?.meta.setName(newName);
};
return workspace ? (
<div>
<StyledSettingH2 marginTop={56}>Workspace Avatar</StyledSettingH2>
<StyledSettingAvatarContent>
<StyledSettingAvatar
alt="workspace avatar"
src={workspaces[workspace.id]?.meta.avatar || ''}
>
W
</StyledSettingAvatar>
{/* {isOwner ? (
<StyledAvatarUploadBtn shape="round">upload</StyledAvatarUploadBtn>
) : null} */}
{/* <Button shape="round">remove</Button> */}
</StyledSettingAvatarContent>
<StyledSettingH2 marginTop={36}>Workspace Name</StyledSettingH2>
<StyledSettingInputContainer>
<Input
width={327}
value={workspaceName}
placeholder="Workspace Name"
disabled={!isOwner}
maxLength={14}
minLength={1}
onChange={handleChangeWorkSpaceName}
></Input>
</StyledSettingInputContainer>
<StyledSettingH2 marginTop={36}>Workspace Owner</StyledSettingH2>
<StyledSettingInputContainer>
<Input
width={327}
disabled
value={owner.name}
placeholder="Workspace Owner"
></Input>
</StyledSettingInputContainer>
<StyledDeleteButtonContainer>
<Button type="danger" shape="circle">
Delete Workspace
</Button>
</StyledDeleteButtonContainer>
</div>
) : null;
};
@@ -0,0 +1 @@
export * from './general';
@@ -0,0 +1,28 @@
import { styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar';
export const StyledSettingInputContainer = styled('div')(({ theme }) => {
return {
marginTop: '12px',
};
});
export const StyledDeleteButtonContainer = styled('div')(({ theme }) => {
return {
marginTop: '154px',
};
});
export const StyledSettingAvatarContent = styled('div')(({ theme }) => {
return {
marginTop: '12px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: '72px',
};
});
export const StyledSettingAvatar = styled(MuiAvatar)(({ theme }) => {
return { height: '72px', width: '72px' };
});
@@ -29,6 +29,7 @@ export const StyledSettingSidebar = styled('div')(({ theme }) => {
export const StyledSettingContent = styled('div')(({ theme }) => {
return {
paddingLeft: '48px',
height: '620px',
};
});
@@ -103,20 +104,6 @@ export const StyledSettingH2 = styled('h2')<{ marginTop?: number }>(
}
);
export const StyledSettingAvatarContent = styled('div')(({ theme }) => {
return {
marginTop: '12px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: '72px',
};
});
export const StyledSettingAvatar = styled(MuiAvatar)(({ theme }) => {
return { height: '72px', width: '72px' };
});
export const StyledAvatarUploadBtn = styled(Button)(({ theme }) => {
return {
backgroundColor: theme.colors.hoverBackground,
@@ -125,18 +112,6 @@ export const StyledAvatarUploadBtn = styled(Button)(({ theme }) => {
};
});
export const StyledSettingInputContainer = styled('div')(({ theme }) => {
return {
marginTop: '12px',
};
});
export const StyledDeleteButtonContainer = styled('div')(({ theme }) => {
return {
marginTop: '154px',
};
});
export const StyledMemberTitleContainer = styled('div')(({ theme }) => {
return {
display: 'flex',
@@ -2,7 +2,6 @@ import Modal, { ModalCloseButton } from '@/ui/modal';
import {
StyledAvatarUploadBtn,
StyledCopyButtonContainer,
StyledDeleteButtonContainer,
StyledMemberAvatar,
StyledMemberButtonContainer,
StyledMemberEmail,
@@ -17,12 +16,9 @@ import {
StyledPublishContent,
StyledPublishCopyContainer,
StyledPublishExplanation,
StyledSettingAvatar,
StyledSettingAvatarContent,
StyledSettingContainer,
StyledSettingContent,
StyledSettingH2,
StyledSettingInputContainer,
StyledSettingSidebar,
StyledSettingSidebarHeader,
StyledSettingTabContainer,
@@ -52,6 +48,10 @@ import { Avatar } from '@mui/material';
import { Menu, MenuItem } from '@/ui/menu';
import { toast } from '@/ui/toast';
import { useConfirm } from '@/providers/confirm-provider';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDetails } from '../workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
import { GeneralPage } from './general';
enum ActiveTab {
'general' = 'general',
'members' = 'members',
@@ -66,7 +66,8 @@ type SettingTabProps = {
type WorkspaceSettingProps = {
isShow: boolean;
onClose?: () => void;
workspace?: Workspace;
workspace: Workspace;
owner: WorkspaceDetails[string]['owner'];
};
const WorkspaceSettingTab = ({ activeTab, onTabChange }: SettingTabProps) => {
@@ -113,7 +114,9 @@ export const WorkspaceSetting = ({
isShow,
onClose,
workspace,
owner,
}: WorkspaceSettingProps) => {
const { workspaces, user } = useAppState();
const [activeTab, setActiveTab] = useState<ActiveTab>(ActiveTab.general);
const handleTabChange = (tab: ActiveTab) => {
setActiveTab(tab);
@@ -121,7 +124,7 @@ export const WorkspaceSetting = ({
const handleClickClose = () => {
onClose && onClose();
};
const isPrivate = workspace?.type === WorkspaceType.Private;
const isOwner = user && owner.id === user.id;
useEffect(() => {
// reset tab when modal is closed
if (!isShow) {
@@ -132,18 +135,20 @@ export const WorkspaceSetting = ({
<Modal open={isShow}>
<StyledSettingContainer>
<ModalCloseButton onClick={handleClickClose} />
<StyledSettingSidebar>
<StyledSettingSidebarHeader>
Workspace Settings
</StyledSettingSidebarHeader>
<WorkspaceSettingTab
activeTab={activeTab}
onTabChange={handleTabChange}
/>
</StyledSettingSidebar>
{isOwner ? (
<StyledSettingSidebar>
<StyledSettingSidebarHeader>
Workspace Settings
</StyledSettingSidebarHeader>
<WorkspaceSettingTab
activeTab={activeTab}
onTabChange={handleTabChange}
/>
</StyledSettingSidebar>
) : null}
<StyledSettingContent>
{activeTab === ActiveTab.general && (
<GeneralPage workspace={workspace} />
<GeneralPage workspace={workspace} owner={owner} />
)}
{activeTab === ActiveTab.members && workspace && (
<MembersPage workspace={workspace} />
@@ -155,32 +160,6 @@ export const WorkspaceSetting = ({
);
};
const GeneralPage = ({ workspace }: { workspace?: Workspace }) => {
return (
<div>
<StyledSettingH2 marginTop={56}>Workspace Avatar</StyledSettingH2>
<StyledSettingAvatarContent>
<StyledSettingAvatar alt="workspace avatar">W</StyledSettingAvatar>
{/* <StyledAvatarUploadBtn shape="round">upload</StyledAvatarUploadBtn>
<Button shape="round">remove</Button> */}
</StyledSettingAvatarContent>
<StyledSettingH2 marginTop={36}>Workspace Name</StyledSettingH2>
<StyledSettingInputContainer>
<Input width={327} placeholder="Workspace Name"></Input>
</StyledSettingInputContainer>
<StyledSettingH2 marginTop={36}>Workspace Owner</StyledSettingH2>
<StyledSettingInputContainer>
<Input width={327} placeholder="Workspace Owner"></Input>
</StyledSettingInputContainer>
<StyledDeleteButtonContainer>
<Button type="danger" shape="circle">
Delete Workspace
</Button>
</StyledDeleteButtonContainer>
</div>
);
};
const MembersPage = ({ workspace }: { workspace: Workspace }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const [members, setMembers] = useState<Member[]>([]);
@@ -14,7 +14,7 @@ import { WorkspaceSetting } from '@/components/workspace-setting';
import { useCallback, useEffect, useState } from 'react';
import { getWorkspaceDetail, WorkspaceType } from '@pathfinder/data-services';
type WorkspaceDetails = Record<
export type WorkspaceDetails = Record<
string,
{ memberCount: number; owner: { id: string; name: string } }
>;
@@ -39,6 +39,9 @@ export const SelectorPopperContent = ({
const handleCloseWorkSpace = () => {
setSettingWorkspaceId(null);
};
const settingWorkspace = settingWorkspaceId
? workspacesMeta.find(workspace => workspace.id === settingWorkspaceId)
: undefined;
const refreshDetails = useCallback(async () => {
const workspaceDetailList = await Promise.all(
@@ -103,7 +106,9 @@ export const SelectorPopperContent = ({
onClick={handleClickSettingWorkspace}
name={
workspaces[workspace.id]?.meta.name ||
`workspace-${workspace.id}`
(workspace.type === WorkspaceType.Private
? user.name
: `workspace-${workspace.id}`)
}
memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
/>
@@ -111,17 +116,20 @@ export const SelectorPopperContent = ({
})}
</WorkspaceWrapper>
<CreateWorkspaceItem />
<WorkspaceSetting
isShow={Boolean(settingWorkspaceId)}
onClose={handleCloseWorkSpace}
workspace={
settingWorkspaceId
? workspacesMeta.find(
workspace => workspace.id === settingWorkspaceId
)
: undefined
}
/>
{settingWorkspace ? (
<WorkspaceSetting
isShow={Boolean(settingWorkspaceId)}
onClose={handleCloseWorkSpace}
workspace={settingWorkspace}
owner={
(settingWorkspaceId &&
workSpaceDetails[settingWorkspaceId]?.owner) || {
id: user.id,
name: user.name,
}
}
/>
) : null}
<StyledDivider />
<ListItem
icon={<InformationIcon />}
@@ -8,6 +8,7 @@ import {
import { FooterSetting } from './FooterSetting';
import { FooterUsers } from './FooterUsers';
import { WorkspaceType } from '@pathfinder/data-services';
import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceItemProps {
id: string;
@@ -28,6 +29,8 @@ export const WorkspaceItem = ({
}: WorkspaceItemProps) => {
const router = useRouter();
const { currentWorkspaceId } = useAppState();
const handleClickSetting = async () => {
onClick && onClick(id);
};
@@ -37,7 +40,9 @@ export const WorkspaceItem = ({
onClick={() => {
router.push(`/workspace/${id}`);
}}
isPrivate={type === WorkspaceType.Private}
canSet={
type !== WorkspaceType.Private && currentWorkspaceId === String(id)
}
>
<WorkspaceItemAvatar alt={name} src={icon}>
{name.charAt(0)}
@@ -64,17 +69,17 @@ const Name = styled('div')(({ theme }) => {
};
});
const StyledWrapper = styled(WorkspaceItemWrapper)<{ isPrivate: boolean }>(
({ isPrivate }) => {
const StyledWrapper = styled(WorkspaceItemWrapper)<{ canSet: boolean }>(
({ canSet }) => {
return {
'& .footer-setting': {
display: 'none',
},
':hover .footer-users': {
display: isPrivate ? 'block' : 'none',
display: canSet ? 'none' : '',
},
':hover .footer-setting': {
display: isPrivate ? 'none' : 'block',
display: canSet ? 'block' : 'none',
},
};
}
@@ -89,10 +89,10 @@ const DynamicBlocksuite = ({
p => p instanceof IndexedDBDocProvider
);
if (indexDBProvider) {
(indexDBProvider as IndexedDBDocProvider)?.on('synced', async () => {
(indexDBProvider as IndexedDBDocProvider).on('synced', async () => {
const updates = await downloadWorkspace({ workspaceId });
if (updates.byteLength) {
if (updates && updates.byteLength) {
Workspace.Y.applyUpdate(workspace.doc, new Uint8Array(updates));
// if after update, the space:meta is empty, then we need to get map with doc
workspace.doc.getMap('space:meta');
@@ -28,21 +28,23 @@ export const useLoadPage = () => {
return;
}
const page = pageId ? workspace?.getPage(pageId) : null;
if (!page) {
const savedPageId = workspace.meta.pageMetas[0]?.id;
if (savedPageId) {
router.push(`/workspace/${currentWorkspaceId}/${savedPageId}`);
if (page) {
loadPage?.current?.(pageId);
return;
}
const savedPageId = workspace.meta.pageMetas[0]?.id;
if (savedPageId) {
router.push(`/workspace/${currentWorkspaceId}/${savedPageId}`);
return;
}
createPage?.current?.()?.then(async pageId => {
if (!pageId) {
return;
}
createPage?.current?.()?.then(async pageId => {
if (!pageId) {
return;
}
router.push(`/workspace/${currentWorkspaceId}/${pageId}`);
});
}
loadPage?.current?.(pageId);
router.push(`/workspace/${currentWorkspaceId}/${pageId}`);
});
}, [workspace, pageId, loadPage, createPage, router, currentWorkspaceId]);
return currentPage?.pageId === pageId ? currentPage : null;
@@ -78,7 +78,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
if (pageId === currentPage?.pageId) {
return currentPage;
}
const page = currentWorkspace?.getPage(pageId) || null;
const page = (pageId ? currentWorkspace?.getPage(pageId) : null) || null;
setState(state => ({ ...state, currentPage: page }));
return page;
};
@@ -94,7 +94,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
const editor = createEditorHandler?.(currentPage) || null;
if (editor) {
const pageMeta = currentWorkspace?.meta.pageMetas.find(
const pageMeta = currentWorkspace.meta.pageMetas.find(
p => p.id === currentPage.pageId
);
if (pageMeta?.mode) {
@@ -133,13 +133,13 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
const callback = async (user: AccessTokenMessage | null) => {
const workspacesMeta = user ? await getWorkspaces() : [];
const workspacesList = await Promise.all(
workspacesMeta?.map(async ({ id }) => {
workspacesMeta.map(async ({ id }) => {
const workspace = (await loadWorkspaceHandler?.(id)) || null;
return { id, workspace };
})
);
let workspaces: Record<string, Workspace | null> = {};
const workspaces: Record<string, Workspace | null> = {};
workspacesList.forEach(({ id, workspace }) => {
workspaces[id] = workspace;
+8 -3
View File
@@ -87,6 +87,7 @@ export const StyledTextButton = styled('button', {
'hoverBackground',
'hoverColor',
'hoverStyle',
'bold',
].includes(prop);
},
})<
@@ -141,9 +142,13 @@ export const StyledTextButton = styled('button', {
export const StyledButton = styled('button', {
shouldForwardProp: prop => {
return !['hoverBackground', 'hoverColor', 'hoverStyle', 'type'].includes(
prop
);
return ![
'hoverBackground',
'hoverColor',
'hoverStyle',
'type',
'bold',
].includes(prop);
},
})<
Pick<
+3
View File
@@ -7,6 +7,7 @@ type inputProps = {
disabled?: boolean;
width?: number;
maxLength?: number;
minLength?: number;
onChange?: (value: string) => void;
onBlur?: (e: any) => void;
};
@@ -17,6 +18,7 @@ export const Input = (props: inputProps) => {
value: valueProp,
placeholder,
maxLength,
minLength,
width = 260,
onChange,
onBlur,
@@ -39,6 +41,7 @@ export const Input = (props: inputProps) => {
placeholder={placeholder}
width={width}
maxLength={maxLength}
minLength={minLength}
onChange={handleChange}
onBlur={handleBlur}
></StyledInput>
+1
View File
@@ -25,6 +25,7 @@
"axios": "^1.2.0",
"encoding": "^0.1.13",
"firebase": "^9.13.0",
"ky": "^0.33.0",
"swr": "^2.0.0"
}
}
+10 -2
View File
@@ -7,6 +7,8 @@ import {
signInWithPopup,
} from 'firebase/auth';
import type { User } from 'firebase/auth';
import { login } from './sdks';
import { setToken } from './request';
/**
* firebaseConfig reference: https://firebase.google.com/docs/web/setup#add_firebase_to_your_app
@@ -32,8 +34,14 @@ const signIn = (email: string, password: string) => {
};
const googleAuthProvider = new GoogleAuthProvider();
export const signInWithGoogle = () => {
return signInWithPopup(firebaseAuth, googleAuthProvider);
export const signInWithGoogle = async () => {
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
const idToken = await user.user.getIdToken();
const token = await login({ token: idToken, type: 'Google' });
setToken({
accessToken: token.token,
refreshToken: token.refresh,
});
};
export const onAuthStateChanged = (callback: (user: User | null) => void) => {
@@ -1,9 +0,0 @@
export class ServiceError extends Error {
public message: string;
public code: string;
constructor(code: string, message: string) {
super(message);
this.message = message;
this.code = code;
}
}
@@ -1,23 +0,0 @@
import axios from 'axios';
import { setAuthorization } from './request';
import { handleResponseError, handleResponse } from './response';
declare module 'axios' {
interface AxiosRequestConfig {
/**
* If true, request will send with Authorization header.
*/
withAuthorization?: boolean;
/**
* If true, indicate this request is refreshing token.
*/
refreshTokenRequest?: boolean;
}
}
export const request = axios.create({
withAuthorization: true,
});
request.interceptors.request.use(setAuthorization);
request.interceptors.response.use(handleResponse, handleResponseError);
@@ -1,4 +1,4 @@
import type { AccessTokenMessage } from './types';
import { AccessTokenMessage } from './token';
type Callback = (user: AccessTokenMessage | null) => void;
+50 -4
View File
@@ -1,4 +1,50 @@
export { request } from './axios';
export { ServiceError } from './ServiceError';
export { setToken, getToken, authorizationEvent } from './request';
export type { AccessTokenMessage } from './request';
import ky from 'ky';
import {
getToken,
setToken,
isAccessTokenExpired,
refreshToken,
authorizationEvent,
} from './token';
export const request = ky.extend({
retry: 1,
hooks: {
beforeRequest: [
async request => {
if (!request.headers.has('token') || request.headers.get('token')) {
const token = getToken();
if (token) {
if (isAccessTokenExpired(token.accessToken)) {
await refreshToken();
}
request.headers.set('Authorization', token.accessToken);
}
}
},
],
// afterResponse: [
// async (_request, _options, response) => {
// if (response.status === 200) {
// const data = await response.json();
// if (data.error) {
// return new Response(data.error.message, {
// status: data.error.code,
// });
// }
// }
// return response;
// },
// ],
beforeRetry: [
async ({ request }) => {
const token = await refreshToken();
request.headers.set('Authorization', token.accessToken);
},
],
},
});
export type { AccessTokenMessage } from './token';
export { authorizationEvent, getToken, setToken };
@@ -1,2 +0,0 @@
export * from './authorization';
export type { AccessTokenMessage } from './types';
@@ -1,8 +0,0 @@
export interface AccessTokenMessage {
create_at: number;
exp: number;
email: string;
id: string;
name: string;
avatar_url: string;
}
@@ -1,7 +0,0 @@
export {
setAuthorization,
setToken,
getToken,
authorizationEvent,
} from './authorization';
export type { AccessTokenMessage } from './authorization';
@@ -1,34 +0,0 @@
import type { AxiosResponse, AxiosError } from 'axios';
import { ServiceError } from '../ServiceError';
import { refreshToken } from '../request/authorization';
import { request } from '../axios';
export async function handleResponseError(error: AxiosError<any, any>) {
const { response, config, message } = error;
const status = response?.status;
if (status === 401) {
if (!config) {
throw new ServiceError(message, status.toString());
}
if (config.refreshTokenRequest) {
throw new ServiceError(message, status.toString());
}
await refreshToken();
return request({ ...config, refreshTokenRequest: true });
}
return error;
}
export async function handleResponse(response: AxiosResponse<any, any>) {
const { status, data } = 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;
}
@@ -1 +0,0 @@
export { handleResponseError, handleResponse } from './handle-error';
@@ -1,44 +1,17 @@
import type { AxiosRequestConfig } from 'axios';
import { login } from '../../../sdks';
import { AuthorizationEvent } from './events';
import type { AccessTokenMessage } from './types';
import { login } from '../sdks';
export interface AccessTokenMessage {
create_at: number;
exp: number;
email: string;
id: string;
name: string;
avatar_url: string;
}
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));
}
export function getToken(): {
accessToken: string;
refreshToken: string;
} | null {
try {
return JSON.parse(window.localStorage.getItem(TOKEN_KEY) || '');
} catch (error) {
return null;
}
}
function b64DecodeUnicode(str: string) {
// Going backwards: from byte stream, to percent-encoding, to original string.
return decodeURIComponent(
@@ -64,7 +37,7 @@ function parseAccessToken(token: string): AccessTokenMessage | null {
}
}
function isAccessTokenExpired(token: string) {
export function isAccessTokenExpired(token: string) {
const message = parseAccessToken(token);
if (!message) {
return true;
@@ -72,37 +45,39 @@ function isAccessTokenExpired(token: string) {
return Date.now() - message.create_at > message.exp;
}
async function isLoggedIn(): Promise<boolean> {
const token = getToken();
if (!token) {
return false;
export function getToken(): {
accessToken: string;
refreshToken: string;
} | null {
try {
return JSON.parse(window.localStorage.getItem(TOKEN_KEY) || '');
} catch (error) {
return null;
}
return isAccessTokenExpired(token.accessToken);
}
export const authorizationEvent = new AuthorizationEvent();
authorizationEvent.triggerChange(
parseAccessToken(getToken()?.accessToken || '')
);
interface Token {
accessToken: string;
refreshToken: string;
}
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));
}
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)) {
await refreshToken();
}
if (!config.headers) {
config.headers = {};
}
config.headers['Authorization'] = token.accessToken;
}
return config;
}
export const refreshToken = async () => {
let token = getToken();
if (!token) {
@@ -121,4 +96,5 @@ export const refreshToken = async () => {
};
setToken(token);
refreshingToken = undefined;
return token;
};
+4 -13
View File
@@ -27,13 +27,9 @@ export interface LoginResponse {
}
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;
return request
.post('/api/user/token', { json: params, headers: { token: undefined } })
.json<LoginResponse>();
}
export interface GetUserByEmailParams {
@@ -52,10 +48,5 @@ export interface User {
export async function getUserByEmail(
params: GetUserByEmailParams
): Promise<User | null> {
const data = await request<User | null>({
url: '/api/user',
method: 'GET',
params,
});
return data.data;
return request.get('/api/user', { json: params }).json<User | null>();
}
+34 -82
View File
@@ -26,12 +26,7 @@ export interface Workspace {
}
export async function getWorkspaces(): Promise<Workspace[]> {
const data = await request({
url: '/api/workspace',
method: 'GET',
});
return data.data;
return request.get('/api/workspace').json();
}
export interface WorkspaceDetail extends Workspace {
@@ -42,14 +37,7 @@ export interface WorkspaceDetail extends Workspace {
export async function getWorkspaceDetail(
params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> {
const data = await request<WorkspaceDetail | null>({
url: `/api/workspace/${params.id}`,
method: 'GET',
});
if (data.data?.owner.id) {
data.data.owner.id = String(data.data?.owner.id);
}
return data.data;
return request.get(`/api/workspace/${params.id}`).json();
}
export interface Permission {
@@ -80,12 +68,7 @@ export interface GetWorkspaceMembersParams {
export async function getWorkspaceMembers(
params: GetWorkspaceDetailParams
): Promise<Member[]> {
const data = await request<Member[]>({
url: `/api/workspace/${params.id}/permission`,
method: 'GET',
});
return data.data;
return request.get(`/api/workspace/${params.id}/permission`).json();
}
export interface CreateWorkspaceParams {
@@ -96,13 +79,7 @@ export interface CreateWorkspaceParams {
export async function createWorkspace(
params: CreateWorkspaceParams
): Promise<void> {
const data = await request({
url: '/api/workspace',
method: 'POST',
data: params,
});
return data.data;
return request.post('/api/workspace', { json: params }).json();
}
export interface UpdateWorkspaceParams {
@@ -113,15 +90,13 @@ export interface UpdateWorkspaceParams {
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;
return request
.post(`/api/workspace/${params.id}`, {
json: {
public: params.public,
},
})
.json();
}
export interface DeleteWorkspaceParams {
@@ -131,12 +106,7 @@ export interface DeleteWorkspaceParams {
export async function deleteWorkspace(
params: DeleteWorkspaceParams
): Promise<void> {
const data = await request({
url: `/api/workspace/${params.id}`,
method: 'DELETE',
});
return data.data;
await request.delete(`/api/workspace/${params.id}`);
}
export interface InviteMemberParams {
@@ -148,15 +118,13 @@ export interface InviteMemberParams {
* 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;
return request
.post(`/api/workspace/${params.id}/permission`, {
json: {
email: params.email,
},
})
.json();
}
export interface RemoveMemberParams {
@@ -164,12 +132,7 @@ export interface RemoveMemberParams {
}
export async function removeMember(params: RemoveMemberParams): Promise<void> {
const data = await request({
url: `/api/permission/${params.permissionId}`,
method: 'DELETE',
});
return data.data;
await request.delete(`/api/permission/${params.permissionId}`);
}
export interface AcceptInvitingParams {
@@ -179,12 +142,7 @@ export interface AcceptInvitingParams {
export async function acceptInviting(
params: AcceptInvitingParams
): Promise<void> {
const data = await request({
url: `/api/invite/${params.invitingCode}`,
method: 'POST',
});
return data.data;
await request.post(`/api/invite/${params.invitingCode}`);
}
export interface DownloadWOrkspaceParams {
@@ -193,28 +151,22 @@ export interface DownloadWOrkspaceParams {
export async function downloadWorkspace(
params: DownloadWOrkspaceParams
): Promise<ArrayBuffer> {
const data = await request({
url: `/api/workspace/${params.workspaceId}/doc`,
method: 'GET',
responseType: 'arraybuffer',
});
return data.data;
return request.get(`/api/workspace/${params.workspaceId}/doc`).arrayBuffer();
}
export async function uploadBlob(params: { blob: Blob }): Promise<string> {
const data = await request({
url: '/api/blob',
method: 'PUT',
data: params.blob,
});
return data.data;
return request.post('/api/blob', { body: params.blob }).text();
// const data = await request({
// url: '/api/blob',
// method: 'PUT',
// data: params.blob,
// });
// return data.data;
}
export async function getBlob(params: { blobId: string }): Promise<string> {
const data = await request({
url: `/api/blob/${params.blobId}`,
method: 'GET',
});
return data.data;
export async function getBlob(params: {
blobId: string;
}): Promise<ArrayBuffer> {
return request.get(`/api/blob/${params.blobId}`).arrayBuffer();
}