mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +08:00
Merge branch 'feat/filesystem_and_search' of github.com:toeverything/AFFiNE into feat/filesystem_and_search
This commit is contained in:
@@ -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 (doesn’t 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,
|
||||
};
|
||||
@@ -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[]>([]);
|
||||
|
||||
+21
-13
@@ -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 />}
|
||||
|
||||
+10
-5
@@ -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;
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"axios": "^1.2.0",
|
||||
"encoding": "^0.1.13",
|
||||
"firebase": "^9.13.0",
|
||||
"ky": "^0.33.0",
|
||||
"swr": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type { AccessTokenMessage } from './types';
|
||||
import { AccessTokenMessage } from './token';
|
||||
|
||||
type Callback = (user: AccessTokenMessage | null) => void;
|
||||
|
||||
@@ -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';
|
||||
+40
-64
@@ -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;
|
||||
};
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user