Compare commits
30 Commits
v0.15.2
...
v0.9.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcb12a8329 | ||
|
|
1fa48a8ee5 | ||
|
|
9b376cbe19 | ||
|
|
5acd6bf64c | ||
|
|
46f8e3af14 | ||
|
|
cf8b62d804 | ||
|
|
a5df724f4e | ||
|
|
ea0487316c | ||
|
|
ae3087a203 | ||
|
|
148db56eaa | ||
|
|
bef184c0e1 | ||
|
|
bd64ee3af0 | ||
|
|
a7e79c6c53 | ||
|
|
b7df15b6ac | ||
|
|
184ede66bb | ||
|
|
8e987231d4 | ||
|
|
37c6e81cbc | ||
|
|
eab5ad3f12 | ||
|
|
08c65b1227 | ||
|
|
3075a29777 | ||
|
|
d67facbc9c | ||
|
|
2418e2c0a8 | ||
|
|
15e18036dc | ||
|
|
86ca624fa6 | ||
|
|
3c5a36e042 | ||
|
|
07c20830f7 | ||
|
|
32ffa36604 | ||
|
|
818476d3ab | ||
|
|
bade53987f | ||
|
|
a04d7616e1 |
19
.github/workflows/build-desktop.yml
vendored
@@ -55,6 +55,25 @@ jobs:
|
||||
path: ./apps/core/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-native:
|
||||
name: Build Native
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
needs: build-core
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
package: '@affine/native'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Run tests
|
||||
run: yarn test
|
||||
working-directory: ./packages/native
|
||||
|
||||
desktop-test:
|
||||
name: Desktop Test
|
||||
runs-on: ${{ matrix.spec.os }}
|
||||
|
||||
13
README.md
@@ -147,18 +147,7 @@ We would also like to give thanks to open-source projects that make AFFiNE possi
|
||||
|
||||
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
|
||||
|
||||
# Contributors
|
||||
|
||||
## Current Core members
|
||||
|
||||
Team members who are currently maintaining the project:
|
||||
|
||||
- [JimmFly](https://github.com/JimmFly) - Jinfei Yang <yangjinfei001@gmail.com> (he/him)
|
||||
- [pengx17](https://github.com/pengx17) - Peng Xiao <pengxiao@outlook.com> (he/him)
|
||||
- [QiShaoXuan](https://github.com/QiSHaoXuan) - Shaoxuan Qi <qishaoxuan777@gmail.com> (he/him)
|
||||
- [himself65](https://github.com/himself65) - Zeyu "Alex" Yang <himself65@outlook.com> (he/him)
|
||||
|
||||
## All Contributors
|
||||
## Contributors
|
||||
|
||||
We would like to express our gratitude to all the individuals who have already contributed to AFFiNE! If you have any AFFiNE-related project, documentation, tool or template, please feel free to contribute it by submitting a pull request to our curated list on GitHub: [awesome-affine](https://github.com/toeverything/awesome-affine).
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ export const createConfiguration: (
|
||||
exclude: [/node_modules/],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg|webp)$/,
|
||||
test: /\.(png|jpg|gif|svg|webp|mp4)$/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@affine/core",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"scripts": {
|
||||
"build": "yarn -T run build-core",
|
||||
"dev": "yarn -T run dev-core",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@mui/material": "^5.14.7",
|
||||
"@radix-ui/react-select": "^1.2.2",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@toeverything/components": "^0.0.42",
|
||||
"@toeverything/components": "^0.0.43",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
"css-spring": "^4.1.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -16,7 +16,7 @@ import {
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
import {
|
||||
contentLayoutAtom,
|
||||
currentPageAtom,
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { atom } from 'jotai';
|
||||
@@ -129,7 +129,7 @@ export function createSetup(rootStore: ReturnType<typeof createStore>) {
|
||||
|
||||
function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
|
||||
// clean up plugin windows when switching to other pages
|
||||
rootStore.sub(currentPageAtom, () => {
|
||||
rootStore.sub(currentPageIdAtom, () => {
|
||||
rootStore.set(contentLayoutAtom, 'editor');
|
||||
});
|
||||
|
||||
@@ -149,7 +149,7 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
|
||||
'@affine/sdk/entry': {
|
||||
rootStore,
|
||||
currentWorkspaceAtom: currentWorkspaceAtom,
|
||||
currentPageAtom: currentPageAtom,
|
||||
currentPageIdAtom: currentPageIdAtom,
|
||||
pushLayoutAtom: pushLayoutAtom,
|
||||
deleteLayoutAtom: deleteLayoutAtom,
|
||||
},
|
||||
|
||||
@@ -3,12 +3,13 @@ import {
|
||||
CountDownRender,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { getUserQuery } from '@affine/graphql';
|
||||
import { type GetUserQuery, getUserQuery } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -56,7 +57,25 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
}
|
||||
|
||||
setIsValidEmail(true);
|
||||
const { user } = await verifyUser({ email });
|
||||
// 0 for no access for internal beta
|
||||
let user: GetUserQuery['user'] | null | 0 = null;
|
||||
await verifyUser({ email })
|
||||
.then(({ user: u }) => {
|
||||
user = u;
|
||||
})
|
||||
.catch(err => {
|
||||
const e = err?.[0];
|
||||
if (e instanceof GraphQLError && e.extensions?.code === 402) {
|
||||
setAuthState('noAccess');
|
||||
user = 0;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
if (user === 0) {
|
||||
return;
|
||||
}
|
||||
setAuthEmail(email);
|
||||
|
||||
if (user) {
|
||||
|
||||
@@ -21,7 +21,14 @@ import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
|
||||
@@ -96,6 +103,20 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
[invite, pushNotification, t]
|
||||
);
|
||||
|
||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [memberListHeight, setMemberListHeight] = useState<number | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
memberCount > COUNT_PER_PAGE &&
|
||||
listContainerRef.current &&
|
||||
memberListHeight === null
|
||||
) {
|
||||
const rect = listContainerRef.current.getBoundingClientRect();
|
||||
setMemberListHeight(rect.height);
|
||||
}
|
||||
}, [listContainerRef, memberCount, memberListHeight]);
|
||||
|
||||
const onRevoke = useCallback<OnRevoke>(
|
||||
async memberId => {
|
||||
const res = await revokeMemberPermission(memberId);
|
||||
@@ -129,7 +150,11 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
) : null}
|
||||
</SettingRow>
|
||||
|
||||
<div className={style.membersPanel}>
|
||||
<div
|
||||
className={style.membersPanel}
|
||||
ref={listContainerRef}
|
||||
style={memberListHeight ? { height: memberListHeight } : {}}
|
||||
>
|
||||
<Suspense fallback={<MemberListFallback memberCount={memberCount} />}>
|
||||
<MemberList
|
||||
workspaceId={workspaceId}
|
||||
@@ -139,11 +164,13 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Pagination
|
||||
totalCount={memberCount}
|
||||
countPerPage={COUNT_PER_PAGE}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
{memberCount > COUNT_PER_PAGE && (
|
||||
<Pagination
|
||||
totalCount={memberCount}
|
||||
countPerPage={COUNT_PER_PAGE}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -186,7 +213,7 @@ const MemberList = ({
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.memberList}>
|
||||
{members.map(member => (
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
@@ -196,7 +223,7 @@ const MemberList = ({
|
||||
onRevoke={onRevoke}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -225,54 +252,56 @@ const MemberItem = ({
|
||||
}, [currentUser.id, isOwner, member.id, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div key={member.id} className={style.listItem} data-testid="member-item">
|
||||
<Avatar
|
||||
size={36}
|
||||
url={member.avatarUrl}
|
||||
name={(member.emailVerified ? member.name : member.email) as string}
|
||||
/>
|
||||
<div className={style.memberContainer}>
|
||||
{member.emailVerified ? (
|
||||
<>
|
||||
<div className={style.memberName}>{member.name}</div>
|
||||
<div className={style.memberEmail}>{member.email}</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.memberName}>{member.email}</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(style.roleOrStatus, {
|
||||
pending: !member.accepted,
|
||||
})}
|
||||
>
|
||||
{member.accepted
|
||||
? member.permission === Permission.Owner
|
||||
? 'Workspace Owner'
|
||||
: 'Member'
|
||||
: 'Pending'}
|
||||
</div>
|
||||
<Menu
|
||||
items={
|
||||
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
|
||||
{operationButtonInfo.leaveOrRevokeText}
|
||||
</MenuItem>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
disabled={!operationButtonInfo.show}
|
||||
type="plain"
|
||||
style={{
|
||||
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
<div
|
||||
key={member.id}
|
||||
className={style.memberListItem}
|
||||
data-testid="member-item"
|
||||
>
|
||||
<Avatar
|
||||
size={36}
|
||||
url={member.avatarUrl}
|
||||
name={(member.emailVerified ? member.name : member.email) as string}
|
||||
/>
|
||||
<div className={style.memberContainer}>
|
||||
{member.emailVerified ? (
|
||||
<>
|
||||
<div className={style.memberName}>{member.name}</div>
|
||||
<div className={style.memberEmail}>{member.email}</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.memberName}>{member.email}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={clsx(style.roleOrStatus, {
|
||||
pending: !member.accepted,
|
||||
})}
|
||||
>
|
||||
{member.accepted
|
||||
? member.permission === Permission.Owner
|
||||
? 'Workspace Owner'
|
||||
: 'Member'
|
||||
: 'Pending'}
|
||||
</div>
|
||||
<Menu
|
||||
items={
|
||||
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
|
||||
{operationButtonInfo.leaveOrRevokeText}
|
||||
</MenuItem>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
disabled={!operationButtonInfo.show}
|
||||
type="plain"
|
||||
style={{
|
||||
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -93,14 +93,17 @@ export const membersFallback = style({
|
||||
color: 'var(--affine-primary-color)',
|
||||
});
|
||||
export const membersPanel = style({
|
||||
marginTop: '24px',
|
||||
padding: '4px',
|
||||
borderRadius: '12px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
export const listItem = style({
|
||||
export const memberList = style({});
|
||||
export const memberListItem = style({
|
||||
padding: '0 4px 0 16px',
|
||||
height: '58px',
|
||||
display: 'flex',
|
||||
@@ -155,7 +158,7 @@ export const memberEmail = style({
|
||||
});
|
||||
export const iconButton = style({});
|
||||
|
||||
globalStyle(`${listItem}:hover ${iconButton}`, {
|
||||
globalStyle(`${memberListItem}:hover ${iconButton}`, {
|
||||
opacity: 1,
|
||||
pointerEvents: 'all',
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useGeneralSettingList,
|
||||
} from './general-setting';
|
||||
import { SettingSidebar } from './setting-sidebar';
|
||||
import { footerIconWrapper, settingContent } from './style.css';
|
||||
import * as style from './style.css';
|
||||
import { WorkspaceSetting } from './workspace-setting';
|
||||
|
||||
type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
|
||||
@@ -84,33 +84,32 @@ export const SettingModal = ({
|
||||
onAccountSettingClick={onAccountSettingClick}
|
||||
/>
|
||||
|
||||
<div data-testid="setting-modal-content" className={settingContent}>
|
||||
<div className="wrapper">
|
||||
<div className="content">
|
||||
{activeTab === 'workspace' && workspaceId ? (
|
||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
|
||||
</Suspense>
|
||||
) : null}
|
||||
{generalSettingList.find(v => v.key === activeTab) ? (
|
||||
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
|
||||
) : null}
|
||||
{activeTab === 'account' && loginStatus === 'authenticated' ? (
|
||||
<AccountSetting />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<div className={footerIconWrapper}>
|
||||
<div data-testid="setting-modal-content" className={style.wrapper}>
|
||||
<div className={style.content}>
|
||||
{activeTab === 'workspace' && workspaceId ? (
|
||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
|
||||
</Suspense>
|
||||
) : null}
|
||||
{generalSettingList.find(v => v.key === activeTab) ? (
|
||||
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
|
||||
) : null}
|
||||
{activeTab === 'account' && loginStatus === 'authenticated' ? (
|
||||
<AccountSetting />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<a
|
||||
href="https://community.affine.pro/home"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={style.suggestionLink}
|
||||
>
|
||||
<span className={style.suggestionLinkIcon}>
|
||||
<ContactWithUsIcon />
|
||||
</div>
|
||||
<a
|
||||
href="https://community.affine.pro/home"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.settings.suggestion']()}
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
{t['com.affine.settings.suggestion']()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,45 +1,38 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingContent = style({
|
||||
export const wrapper = style({
|
||||
flexGrow: '1',
|
||||
height: '100%',
|
||||
padding: '40px 15px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper`, {
|
||||
padding: '0 15px',
|
||||
height: '100%',
|
||||
maxWidth: '560px',
|
||||
margin: '0 auto',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
padding: '40px 15px 20px 15px',
|
||||
overflow: 'hidden auto',
|
||||
|
||||
globalStyle(`${settingContent} .wrapper::-webkit-scrollbar`, {
|
||||
display: 'none',
|
||||
});
|
||||
globalStyle(`${settingContent} .content`, {
|
||||
minHeight: '100%',
|
||||
paddingBottom: '80px',
|
||||
});
|
||||
globalStyle(`${settingContent} .footer`, {
|
||||
cursor: 'pointer',
|
||||
paddingTop: '40px',
|
||||
marginTop: '-80px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
// children
|
||||
display: 'flex',
|
||||
minHeight: '100px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
|
||||
'::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .footer a`, {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
lineHeight: 'normal',
|
||||
export const content = style({
|
||||
width: '100%',
|
||||
marginBottom: '24px',
|
||||
});
|
||||
export const footerIconWrapper = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginRight: '12px',
|
||||
height: '19px',
|
||||
|
||||
export const suggestionLink = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const suggestionLinkIcon = style({
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginRight: '12px',
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export const pluginContainer = style({
|
||||
});
|
||||
|
||||
export const editor = style({
|
||||
height: 'calc(100% - 52px)',
|
||||
height: '100%',
|
||||
selectors: {
|
||||
'&.full-screen': {
|
||||
vars: {
|
||||
@@ -25,6 +25,11 @@ export const editor = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${editor} .affine-doc-viewport`, {
|
||||
paddingBottom: '150px',
|
||||
});
|
||||
|
||||
globalStyle('.is-public-page affine-page-meta-data', {
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type { LayoutNode } from '@affine/sdk//entry';
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
@@ -98,13 +98,17 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
setBlockHub={setBlockHub}
|
||||
onLoad={useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
page.workspace.setPageMeta(page.id, {
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
const disposableGroup = new DisposableGroup();
|
||||
disposableGroup.add(
|
||||
page.slots.blockUpdated.once(() => {
|
||||
page.workspace.setPageMeta(page.id, {
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
})
|
||||
);
|
||||
localStorage.setItem('last_page_id', page.id);
|
||||
let dispose = () => {};
|
||||
if (onLoad) {
|
||||
dispose = onLoad(page, editor);
|
||||
disposableGroup.add(onLoad(page, editor));
|
||||
}
|
||||
const rootStore = getCurrentStore();
|
||||
const editorItems = rootStore.get(pluginEditorAtom);
|
||||
@@ -124,7 +128,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
});
|
||||
|
||||
return () => {
|
||||
dispose();
|
||||
disposableGroup.dispose();
|
||||
clearTimeout(renderTimeout);
|
||||
window.setTimeout(() => {
|
||||
disposes.forEach(dispose => dispose());
|
||||
@@ -207,6 +211,7 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
return (
|
||||
<PanelGroup
|
||||
direction={node.direction}
|
||||
style={depth === 0 ? { height: 'calc(100% - 52px)' } : undefined}
|
||||
className={depth === 0 ? editorContainer : undefined}
|
||||
>
|
||||
<Panel
|
||||
@@ -256,7 +261,11 @@ export const PageDetailEditor = (props: PageDetailEditorProps) => {
|
||||
if (layout === 'editor') {
|
||||
return (
|
||||
<Suspense>
|
||||
<PanelGroup direction="horizontal" className={editorContainer}>
|
||||
<PanelGroup
|
||||
style={{ height: 'calc(100% - 52px)' }}
|
||||
direction="horizontal"
|
||||
className={editorContainer}
|
||||
>
|
||||
<Panel>
|
||||
<EditorWrapper {...props} />
|
||||
</Panel>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const ItemContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '8px 14px',
|
||||
gap: '14px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '8px',
|
||||
transition: 'background-color 0.2s',
|
||||
fontSize: '24px',
|
||||
color: 'var(--affine-icon-secondary)',
|
||||
});
|
||||
|
||||
export const ItemText = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '22px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontWeight: 400,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ImportIcon, PlusIcon } from '@blocksuite/icons';
|
||||
import { MenuItem } from '@toeverything/components/menu';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
export const AddWorkspace = ({
|
||||
onAddWorkspace,
|
||||
onNewWorkspace,
|
||||
}: {
|
||||
onAddWorkspace?: () => void;
|
||||
onNewWorkspace?: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runtimeConfig.enableSQLiteProvider && environment.isDesktop ? (
|
||||
<MenuItem
|
||||
block={true}
|
||||
preFix={<ImportIcon />}
|
||||
onClick={onAddWorkspace}
|
||||
data-testid="add-workspace"
|
||||
className={styles.ItemContainer}
|
||||
>
|
||||
<div className={styles.ItemText}>
|
||||
{t['com.affine.workspace.local.import']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem
|
||||
block={true}
|
||||
preFix={<PlusIcon />}
|
||||
onClick={onNewWorkspace}
|
||||
data-testid="new-workspace"
|
||||
className={styles.ItemContainer}
|
||||
>
|
||||
<div className={styles.ItemText}>
|
||||
{t['com.affine.workspaceList.addWorkspace.create']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const workspaceListWrapper = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const signInWrapper = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
borderRadius: '8px',
|
||||
});
|
||||
|
||||
export const iconContainer = style({
|
||||
width: '28px',
|
||||
padding: '2px 4px 4px',
|
||||
borderRadius: '14px',
|
||||
background: 'var(--affine-white)',
|
||||
display: 'flex',
|
||||
border: '1px solid var(--affine-icon-secondary)',
|
||||
color: 'var(--affine-icon-secondary)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
});
|
||||
|
||||
export const signInTextContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const signInTextPrimary = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
|
||||
export const signInTextSecondary = style({
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
|
||||
export const menuItem = style({
|
||||
borderRadius: '8px',
|
||||
});
|
||||
@@ -1,155 +1,62 @@
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
AccountIcon,
|
||||
ImportIcon,
|
||||
Logo1Icon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
SignOutIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { MenuItem } from '@toeverything/components/menu';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { startTransition, useCallback, useMemo, useTransition } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
authAtom,
|
||||
openCreateWorkspaceModalAtom,
|
||||
openDisableCloudAlertModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '../../../../atoms';
|
||||
import type { AllWorkspace } from '../../../../shared';
|
||||
import { signOutCloud } from '../../../../utils/cloud-utils';
|
||||
import { useNavigateHelper } from '../.././../../hooks/use-navigate-helper';
|
||||
import {
|
||||
StyledCreateWorkspaceCardPill,
|
||||
StyledCreateWorkspaceCardPillContent,
|
||||
StyledCreateWorkspaceCardPillIcon,
|
||||
StyledImportWorkspaceCardPill,
|
||||
StyledItem,
|
||||
StyledModalBody,
|
||||
StyledModalContent,
|
||||
StyledModalFooterContent,
|
||||
StyledModalHeader,
|
||||
StyledModalHeaderContent,
|
||||
StyledModalHeaderLeft,
|
||||
StyledModalTitle,
|
||||
StyledOperationWrapper,
|
||||
StyledSignInCardPill,
|
||||
StyledSignInCardPillTextCotainer,
|
||||
StyledSignInCardPillTextPrimary,
|
||||
StyledSignInCardPillTextSecondary,
|
||||
StyledWorkspaceFlavourTitle,
|
||||
} from './styles';
|
||||
import { AddWorkspace } from './add-workspace';
|
||||
import * as styles from './index.css';
|
||||
import { UserAccountItem } from './user-account';
|
||||
import { AFFiNEWorkspaceList } from './workspace-list';
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
disabled?: boolean;
|
||||
workspaces: RootWorkspaceMetadata[];
|
||||
currentWorkspaceId: AllWorkspace['id'] | null;
|
||||
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
onMoveWorkspace: (activeId: string, overId: string) => void;
|
||||
}
|
||||
const SignInItem = () => {
|
||||
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
|
||||
const AccountMenu = ({
|
||||
onOpenAccountSetting,
|
||||
onSignOut,
|
||||
}: {
|
||||
onOpenAccountSetting: () => void;
|
||||
onSignOut: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<AccountIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={onOpenAccountSetting}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.settings']()}
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<SignOutIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={onSignOut}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.logout']()}
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CloudWorkSpaceList = ({
|
||||
disabled,
|
||||
workspaces,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
currentWorkspaceId,
|
||||
onMoveWorkspace,
|
||||
}: WorkspaceModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const onClickSignIn = useCallback(async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setDisableCloudOpen(true);
|
||||
} else {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}
|
||||
}, [setOpen, setDisableCloudOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledModalHeader>
|
||||
<StyledModalHeaderLeft>
|
||||
<StyledWorkspaceFlavourTitle>
|
||||
{t['com.affine.workspace.cloud']()}
|
||||
</StyledWorkspaceFlavourTitle>
|
||||
</StyledModalHeaderLeft>
|
||||
</StyledModalHeader>
|
||||
<StyledModalContent>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||
}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onDragEnd={useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
onMoveWorkspace(active.id as string, over?.id as string);
|
||||
}
|
||||
},
|
||||
[onMoveWorkspace]
|
||||
)}
|
||||
/>
|
||||
</StyledModalContent>
|
||||
</>
|
||||
<MenuItem
|
||||
className={styles.menuItem}
|
||||
onClick={onClickSignIn}
|
||||
data-testid="cloud-signin-button"
|
||||
>
|
||||
<div className={styles.signInWrapper}>
|
||||
<div className={styles.iconContainer}>
|
||||
<Logo1Icon />
|
||||
</div>
|
||||
|
||||
<div className={styles.signInTextContainer}>
|
||||
<div className={styles.signInTextPrimary}>
|
||||
{t['com.affine.workspace.cloud.auth']()}
|
||||
</div>
|
||||
<div className={styles.signInTextSecondary}>
|
||||
{t['com.affine.workspace.cloud.description']()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -158,240 +65,43 @@ export const UserWithWorkspaceList = ({
|
||||
}: {
|
||||
onEventEnd?: () => void;
|
||||
}) => {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
|
||||
|
||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||
|
||||
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
|
||||
delay: 0,
|
||||
});
|
||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
|
||||
currentWorkspaceIdAtom
|
||||
);
|
||||
const setCurrentPageId = useSetAtom(currentPageIdAtom);
|
||||
const [, startCloseTransition] = useTransition();
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
// TODO: AFFiNE Cloud support
|
||||
const { data: session, status } = useSession();
|
||||
const isLoggedIn = useMemo(() => status === 'authenticated', [status]);
|
||||
const cloudWorkspaces = useMemo(
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
||||
[workspaces]
|
||||
);
|
||||
const localWorkspaces = useMemo(
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
const onClickWorkspaceSetting = useCallback(
|
||||
(workspaceId: string) => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
});
|
||||
onEventEnd?.();
|
||||
},
|
||||
[onEventEnd, setOpenSettingModalAtom]
|
||||
);
|
||||
|
||||
const onMoveWorkspace = useCallback(
|
||||
(activeId: string, overId: string) => {
|
||||
const oldIndex = workspaces.findIndex(w => w.id === activeId);
|
||||
const newIndex = workspaces.findIndex(w => w.id === overId);
|
||||
startTransition(() => {
|
||||
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
|
||||
});
|
||||
},
|
||||
[setWorkspaces, workspaces]
|
||||
);
|
||||
const onClickWorkspace = useCallback(
|
||||
(workspaceId: string) => {
|
||||
startCloseTransition(() => {
|
||||
setCurrentWorkspaceId(workspaceId);
|
||||
setCurrentPageId(null);
|
||||
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
|
||||
});
|
||||
onEventEnd?.();
|
||||
},
|
||||
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId]
|
||||
);
|
||||
const onNewWorkspace = useCallback(() => {
|
||||
setOpenCreateWorkspaceModal('new');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
const onAddWorkspace = useCallback(async () => {
|
||||
setOpenCreateWorkspaceModal('add');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
const onOpenAccountSetting = useCallback(() => {
|
||||
setSettingModalAtom(prev => ({
|
||||
...prev,
|
||||
open: true,
|
||||
activeTab: 'account',
|
||||
}));
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setSettingModalAtom]);
|
||||
const onSignOut = useCallback(async () => {
|
||||
signOutCloud()
|
||||
.then(() => {
|
||||
jumpToIndex();
|
||||
})
|
||||
.catch(console.error);
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, jumpToIndex]);
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoggedIn ? (
|
||||
<StyledModalHeaderContent>
|
||||
<StyledSignInCardPill>
|
||||
<StyledItem
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setDisableCloudOpen(true);
|
||||
} else {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
data-testid="cloud-signin-button"
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<Logo1Icon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
<StyledSignInCardPillTextCotainer>
|
||||
<StyledSignInCardPillTextPrimary>
|
||||
{t['com.affine.workspace.cloud.auth']()}
|
||||
</StyledSignInCardPillTextPrimary>
|
||||
<StyledSignInCardPillTextSecondary>
|
||||
{t['com.affine.workspace.cloud.description']()}
|
||||
</StyledSignInCardPillTextSecondary>
|
||||
</StyledSignInCardPillTextCotainer>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</StyledItem>
|
||||
</StyledSignInCardPill>
|
||||
<Divider
|
||||
style={{
|
||||
margin: '12px 0px',
|
||||
}}
|
||||
/>
|
||||
</StyledModalHeaderContent>
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
{isAuthenticated ? (
|
||||
<UserAccountItem
|
||||
email={session?.user.email ?? 'Unknown User'}
|
||||
onEventEnd={onEventEnd}
|
||||
/>
|
||||
) : (
|
||||
<StyledModalHeaderContent>
|
||||
<StyledModalHeader>
|
||||
<StyledModalTitle>{session?.user.email}</StyledModalTitle>
|
||||
<StyledOperationWrapper>
|
||||
<Menu
|
||||
items={
|
||||
<AccountMenu
|
||||
onOpenAccountSetting={onOpenAccountSetting}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
side: 'right',
|
||||
sideOffset: 30,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="more-button"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
type="plain"
|
||||
/>
|
||||
</Menu>
|
||||
</StyledOperationWrapper>
|
||||
</StyledModalHeader>
|
||||
<Divider style={{ margin: '12px 0px' }} />
|
||||
</StyledModalHeaderContent>
|
||||
<SignInItem />
|
||||
)}
|
||||
<StyledModalBody>
|
||||
{isLoggedIn && cloudWorkspaces.length !== 0 ? (
|
||||
<>
|
||||
<CloudWorkSpaceList
|
||||
workspaces={workspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onMoveWorkspace={onMoveWorkspace}
|
||||
/>
|
||||
<Divider
|
||||
style={{
|
||||
margin: '12px 0px',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<StyledModalHeader>
|
||||
<StyledWorkspaceFlavourTitle>
|
||||
{t['com.affine.workspace.local']()}
|
||||
</StyledWorkspaceFlavourTitle>
|
||||
</StyledModalHeader>
|
||||
<StyledModalContent>
|
||||
<WorkspaceList
|
||||
items={localWorkspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onDragEnd={useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
onMoveWorkspace(active.id as string, over?.id as string);
|
||||
}
|
||||
},
|
||||
[onMoveWorkspace]
|
||||
)}
|
||||
/>
|
||||
</StyledModalContent>
|
||||
{runtimeConfig.enableSQLiteProvider && environment.isDesktop ? (
|
||||
<StyledImportWorkspaceCardPill>
|
||||
<StyledItem onClick={onAddWorkspace} data-testid="add-workspace">
|
||||
<StyledCreateWorkspaceCardPillContent
|
||||
style={{ gap: '14px', paddingLeft: '2px' }}
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillIcon style={{ fontSize: '24px' }}>
|
||||
<ImportIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
<div>
|
||||
<p>{t['com.affine.workspace.local.import']()}</p>
|
||||
</div>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</StyledItem>
|
||||
</StyledImportWorkspaceCardPill>
|
||||
) : null}
|
||||
</StyledModalBody>
|
||||
<StyledModalFooterContent>
|
||||
<StyledCreateWorkspaceCardPill>
|
||||
<StyledItem onClick={onNewWorkspace} data-testid="new-workspace">
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<PlusIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
<div>
|
||||
<p>{t['New Workspace']()}</p>
|
||||
</div>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</StyledItem>
|
||||
</StyledCreateWorkspaceCardPill>
|
||||
</StyledModalFooterContent>
|
||||
</>
|
||||
<Divider size="thinner" />
|
||||
<AFFiNEWorkspaceList workspaces={workspaces} onEventEnd={onEventEnd} />
|
||||
{workspaces.length > 0 ? <Divider size="thinner" /> : null}
|
||||
<AddWorkspace
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
|
||||
export const StyledSplitLine = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: 'var(--affine-border-color)',
|
||||
marginRight: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
width: '202px',
|
||||
p: {
|
||||
height: '20px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
},
|
||||
svg: {
|
||||
marginRight: '10px',
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
span: {
|
||||
flexGrow: 1,
|
||||
...textEllipsis(1),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
marginBottom: '10px',
|
||||
maxWidth: '200px',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCard = styled('div')(() => {
|
||||
return {
|
||||
width: '310px',
|
||||
height: '124px',
|
||||
marginBottom: '24px',
|
||||
cursor: 'pointer',
|
||||
padding: '16px',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
borderRadius: '12px',
|
||||
transition: 'all .1s',
|
||||
background: 'var(--affine-white-80)',
|
||||
...displayFlex('flex-start', 'flex-start'),
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
'.add-icon': {
|
||||
borderColor: 'var(--affine-white)',
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
},
|
||||
'@media (max-width: 720px)': {
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
||||
return {
|
||||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
margin: '-8px -4px',
|
||||
flexFlow: 'column',
|
||||
gap: '12px',
|
||||
background: 'var(--affine-background-overlay-panel-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCardPill = styled('div')(() => {
|
||||
return {
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '58px',
|
||||
border: `1px solid var(--affine-border-color)`,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSignInCardPill = styled('div')(() => {
|
||||
return {
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '58px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledImportWorkspaceCardPill = styled('div')(() => {
|
||||
return {
|
||||
borderRadius: '5px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCardPillContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCardPillIcon = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '28px',
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSignInCardPillTextCotainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSignInCardPillTextSecondary = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '12px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSignInCardPillTextPrimary = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
maxWidth: '200px',
|
||||
textAlign: 'left',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeaderLeft = styled('div')(() => {
|
||||
return { ...displayFlex('flex-start', 'center') };
|
||||
});
|
||||
export const StyledModalTitle = styled('div')(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledHelperContainer = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginLeft: '15px',
|
||||
fontWeight: 400,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalContent = styled('div')({
|
||||
...displayFlex('space-between', 'flex-start', 'flex-start'),
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
export const StyledModalFooterContent = styled('div')({
|
||||
...displayFlex('space-between', 'flex-start', 'flex-start'),
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
marginTop: '12px',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
});
|
||||
|
||||
export const StyledModalHeaderContent = styled('div')({
|
||||
...displayFlex('space-between', 'flex-start', 'flex-start'),
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
});
|
||||
|
||||
export const StyledOperationWrapper = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('flex-end', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceAdd = styled('div')(() => {
|
||||
return {
|
||||
width: '58px',
|
||||
height: '58px',
|
||||
borderRadius: '100%',
|
||||
background: 'var(--affine-background-overlay-panel-color)',
|
||||
border: '1.5px dashed #f4f5fa',
|
||||
transition: 'background .2s',
|
||||
fontSize: '24px',
|
||||
...displayFlex('center', 'center'),
|
||||
borderColor: 'var(--affine-white)',
|
||||
color: 'var(--affine-background-overlay-panel-color)',
|
||||
};
|
||||
});
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '0px 14px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalBody = styled('div')(() => {
|
||||
return {
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: '4px',
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceFlavourTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
marginBottom: '4px',
|
||||
};
|
||||
});
|
||||
export const StyledItem = styled('button')<{
|
||||
active?: boolean;
|
||||
}>(({ active = false }) => {
|
||||
return {
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
borderRadius: '5px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
svg: {
|
||||
color: 'var(--affine-icon-color)',
|
||||
},
|
||||
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
|
||||
...(active
|
||||
? {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const userAccountContainer = style({
|
||||
display: 'flex',
|
||||
padding: '4px 0px 4px 12px',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const userEmail = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 400,
|
||||
lineHeight: '22px',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 'calc(100% - 36px)',
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
AccountIcon,
|
||||
MoreHorizontalIcon,
|
||||
SignOutIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../../../atoms';
|
||||
import { signOutCloud } from '../../../../../utils/cloud-utils';
|
||||
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
||||
import * as styles from './index.css';
|
||||
|
||||
const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => {
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
|
||||
const onOpenAccountSetting = useCallback(() => {
|
||||
setSettingModalAtom(prev => ({
|
||||
...prev,
|
||||
open: true,
|
||||
activeTab: 'account',
|
||||
}));
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
const onSignOut = useCallback(async () => {
|
||||
signOutCloud()
|
||||
.then(() => {
|
||||
jumpToIndex();
|
||||
})
|
||||
.catch(console.error);
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, jumpToIndex]);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<AccountIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={onOpenAccountSetting}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.settings']()}
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<SignOutIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={onSignOut}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.logout']()}
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserAccountItem = ({
|
||||
email,
|
||||
onEventEnd,
|
||||
}: {
|
||||
email: string;
|
||||
onEventEnd?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.userAccountContainer}>
|
||||
<div className={styles.userEmail}>{email}</div>
|
||||
<Menu
|
||||
items={<AccountMenu onEventEnd={onEventEnd} />}
|
||||
contentOptions={{
|
||||
side: 'right',
|
||||
sideOffset: 12,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="more-button"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
type="plain"
|
||||
/>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const workspaceListsWrapper = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
maxHeight: 'calc(100vh - 300px)',
|
||||
});
|
||||
export const workspaceListWrapper = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
export const workspaceType = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0px 12px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
|
||||
export const scrollbar = style({
|
||||
transform: 'translateX(10px)',
|
||||
width: '4px',
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { startTransition, useCallback, useMemo, useTransition } from 'react';
|
||||
|
||||
import {
|
||||
openCreateWorkspaceModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '../../../../../atoms';
|
||||
import type { AllWorkspace } from '../../../../../shared';
|
||||
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
|
||||
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
||||
import * as styles from './index.css';
|
||||
interface WorkspaceModalProps {
|
||||
disabled?: boolean;
|
||||
workspaces: (AffineCloudWorkspace | LocalWorkspace)[];
|
||||
currentWorkspaceId: AllWorkspace['id'] | null;
|
||||
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
onDragEnd: (event: DragEndEvent) => void;
|
||||
}
|
||||
|
||||
const CloudWorkSpaceList = ({
|
||||
disabled,
|
||||
workspaces,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
currentWorkspaceId,
|
||||
onDragEnd,
|
||||
}: WorkspaceModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<div className={styles.workspaceType}>
|
||||
{t['com.affine.workspaceList.workspaceListType.cloud']()}
|
||||
</div>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={workspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onDragEnd={onDragEnd}
|
||||
useIsWorkspaceOwner={useIsWorkspaceOwner}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalWorkspaces = ({
|
||||
disabled,
|
||||
workspaces,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
currentWorkspaceId,
|
||||
onDragEnd,
|
||||
}: WorkspaceModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<div className={styles.workspaceType}>
|
||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||
</div>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={workspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AFFiNEWorkspaceList = ({
|
||||
workspaces,
|
||||
onEventEnd,
|
||||
}: {
|
||||
workspaces: RootWorkspaceMetadata[];
|
||||
onEventEnd?: () => void;
|
||||
}) => {
|
||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
|
||||
currentWorkspaceIdAtom
|
||||
);
|
||||
|
||||
const setCurrentPageId = useSetAtom(currentPageIdAtom);
|
||||
|
||||
const [, startCloseTransition] = useTransition();
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
// TODO: AFFiNE Cloud support
|
||||
const { status } = useSession();
|
||||
|
||||
const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
|
||||
|
||||
const cloudWorkspaces = useMemo(
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
const localWorkspaces = useMemo(
|
||||
() =>
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
||||
[workspaces]
|
||||
);
|
||||
|
||||
const onClickWorkspaceSetting = useCallback(
|
||||
(workspaceId: string) => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
});
|
||||
onEventEnd?.();
|
||||
},
|
||||
[onEventEnd, setOpenSettingModalAtom]
|
||||
);
|
||||
|
||||
const onMoveWorkspace = useCallback(
|
||||
(activeId: string, overId: string) => {
|
||||
const oldIndex = workspaces.findIndex(w => w.id === activeId);
|
||||
|
||||
const newIndex = workspaces.findIndex(w => w.id === overId);
|
||||
startTransition(() => {
|
||||
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
|
||||
});
|
||||
},
|
||||
[setWorkspaces, workspaces]
|
||||
);
|
||||
|
||||
const onClickWorkspace = useCallback(
|
||||
(workspaceId: string) => {
|
||||
startCloseTransition(() => {
|
||||
setCurrentWorkspaceId(workspaceId);
|
||||
setCurrentPageId(null);
|
||||
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
|
||||
});
|
||||
onEventEnd?.();
|
||||
},
|
||||
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId]
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
onMoveWorkspace(active.id as string, over?.id as string);
|
||||
}
|
||||
},
|
||||
[onMoveWorkspace]
|
||||
);
|
||||
|
||||
const onNewWorkspace = useCallback(() => {
|
||||
setOpenCreateWorkspaceModal('new');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
const onAddWorkspace = useCallback(async () => {
|
||||
setOpenCreateWorkspaceModal('add');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
return (
|
||||
<ScrollableContainer
|
||||
className={styles.workspaceListsWrapper}
|
||||
scrollBarClassName={styles.scrollbar}
|
||||
>
|
||||
{isAuthenticated ? (
|
||||
<div>
|
||||
<CloudWorkSpaceList
|
||||
workspaces={cloudWorkspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
||||
<Divider size="thinner" />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<LocalWorkspaces
|
||||
workspaces={localWorkspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
};
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from './styles';
|
||||
|
||||
const hoverAtom = atom(false);
|
||||
|
||||
// FIXME:
|
||||
// 1. Remove mui style
|
||||
// 2. Refactor the code to improve readability
|
||||
@@ -41,6 +42,7 @@ const CloudWorkspaceStatus = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SyncingWorkspaceStatus = () => {
|
||||
return (
|
||||
<>
|
||||
@@ -49,6 +51,7 @@ const SyncingWorkspaceStatus = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UnSyncWorkspaceStatus = () => {
|
||||
return (
|
||||
<>
|
||||
@@ -82,11 +85,14 @@ const WorkspaceStatus = ({
|
||||
currentWorkspace: AllWorkspace;
|
||||
}) => {
|
||||
const isOnline = useSystemOnline();
|
||||
|
||||
// todo: finish display sync status
|
||||
const [forceSyncStatus, startForceSync] = useDatasourceSync(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
const setIsHovered = useSetAtom(hoverAtom);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return 'Saved locally';
|
||||
@@ -103,6 +109,7 @@ const WorkspaceStatus = ({
|
||||
return 'Sync with AFFiNE Cloud';
|
||||
}
|
||||
}, [currentWorkspace.flavour, forceSyncStatus.type, isOnline]);
|
||||
|
||||
const CloudWorkspaceSyncStatus = useCallback(() => {
|
||||
if (forceSyncStatus.type === 'syncing') {
|
||||
return SyncingWorkspaceStatus();
|
||||
@@ -160,6 +167,7 @@ export const WorkspaceCard = forwardRef<
|
||||
const [name] = useBlockSuiteWorkspaceName(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ export const StyledSelectorContainer = styled('div')({
|
||||
alignItems: 'center',
|
||||
padding: '0 6px',
|
||||
borderRadius: '8px',
|
||||
outline: 'none',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -19,17 +19,10 @@ import {
|
||||
} from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { Popover } from '@toeverything/components/popover';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useHistoryAtom } from '../../atoms/history';
|
||||
import { useAppSetting } from '../../atoms/settings';
|
||||
@@ -175,18 +168,21 @@ export const RootAppSidebar = ({
|
||||
}
|
||||
>
|
||||
<SidebarContainer>
|
||||
<Popover
|
||||
open={openUserWorkspaceList}
|
||||
content={
|
||||
<Suspense>
|
||||
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
|
||||
</Suspense>
|
||||
<Menu
|
||||
rootOptions={{
|
||||
open: openUserWorkspaceList,
|
||||
}}
|
||||
items={
|
||||
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
|
||||
}
|
||||
contentOptions={{
|
||||
// hide trigger
|
||||
sideOffset: -58,
|
||||
onInteractOutside: closeUserWorkspaceList,
|
||||
onEscapeKeyDown: closeUserWorkspaceList,
|
||||
style: {
|
||||
width: '300px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<WorkspaceCard
|
||||
@@ -195,7 +191,7 @@ export const RootAppSidebar = ({
|
||||
setOpenUserWorkspaceList(true);
|
||||
}, [])}
|
||||
/>
|
||||
</Popover>
|
||||
</Menu>
|
||||
<QuickSearchInput
|
||||
data-testid="slider-bar-quick-search-button"
|
||||
onClick={onOpenQuickSearchModal}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
type NavigateOptions,
|
||||
useLocation,
|
||||
@@ -103,14 +103,26 @@ export function useNavigateHelper() {
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return {
|
||||
jumpToPage,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSubPath,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
jumpToExpired,
|
||||
jumpToSignIn,
|
||||
};
|
||||
return useMemo(
|
||||
() => ({
|
||||
jumpToPage,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSubPath,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
jumpToExpired,
|
||||
jumpToSignIn,
|
||||
}),
|
||||
[
|
||||
jumpTo404,
|
||||
jumpToExpired,
|
||||
jumpToIndex,
|
||||
jumpToPage,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSignIn,
|
||||
jumpToSubPath,
|
||||
openPage,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { lazy } from 'react';
|
||||
@@ -61,15 +62,29 @@ export const Component = () => {
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: 300,
|
||||
margin: '80px auto',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'var(--affine-shadow-2)',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
padding: '16px 12px',
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
}}
|
||||
>
|
||||
<UserWithWorkspaceList />
|
||||
<Menu
|
||||
rootOptions={{
|
||||
open: true,
|
||||
}}
|
||||
items={<UserWithWorkspaceList />}
|
||||
contentOptions={{
|
||||
style: {
|
||||
width: 300,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'var(--affine-shadow-2)',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
padding: '16px 12px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div></div>
|
||||
</Menu>
|
||||
</div>
|
||||
<AllWorkspaceModals />
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/docs",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
buildType,
|
||||
icnsPath,
|
||||
icoPath,
|
||||
iconPngPath,
|
||||
platform,
|
||||
productName,
|
||||
iconUrl,
|
||||
@@ -76,8 +77,7 @@ const makers = [
|
||||
name: '@reforged/maker-appimage',
|
||||
config: {
|
||||
name: 'AFFiNE',
|
||||
iconUrl: icoPath,
|
||||
setupIcon: icoPath,
|
||||
icon: iconPngPath,
|
||||
platforms: ['linux'],
|
||||
options: {
|
||||
bin: productName,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"author": "affine",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
@@ -45,8 +45,8 @@
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@types/uuid": "^9.0.3",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "^26.1.0",
|
||||
"electron-log": "^5.0.0-beta.28",
|
||||
"electron": "^26.3.0",
|
||||
"electron-log": "^5.0.0-beta.29",
|
||||
"electron-squirrel-startup": "1.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esbuild": "^0.19.2",
|
||||
|
||||
@@ -18,6 +18,7 @@ const icoPath = path.join(
|
||||
? `./resources/icons/icon_${buildType}.ico`
|
||||
: './resources/icons/icon.ico'
|
||||
);
|
||||
|
||||
const icnsPath = path.join(
|
||||
ROOT,
|
||||
!stableBuild
|
||||
@@ -25,6 +26,8 @@ const icnsPath = path.join(
|
||||
: './resources/icons/icon.icns'
|
||||
);
|
||||
|
||||
const iconPngPath = path.join(ROOT, './resources/icons/icon.png');
|
||||
|
||||
const iconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`;
|
||||
const arch =
|
||||
process.argv.indexOf('--arch') > 0
|
||||
@@ -42,6 +45,7 @@ module.exports = {
|
||||
productName,
|
||||
icoPath,
|
||||
icnsPath,
|
||||
iconPngPath,
|
||||
iconUrl,
|
||||
arch,
|
||||
platform,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/prototype",
|
||||
"private": true,
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host --port 3003",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppController } from './app.controller';
|
||||
import { ConfigModule } from './config';
|
||||
import { MetricsModule } from './metrics';
|
||||
import { BusinessModules } from './modules';
|
||||
import { AuthModule } from './modules/auth';
|
||||
import { PrismaModule } from './prisma';
|
||||
import { SessionModule } from './session';
|
||||
import { StorageModule } from './storage';
|
||||
@@ -17,6 +18,7 @@ import { RateLimiterModule } from './throttler';
|
||||
MetricsModule,
|
||||
SessionModule,
|
||||
RateLimiterModule,
|
||||
AuthModule,
|
||||
...BusinessModules,
|
||||
],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { User } from '@prisma/client';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
|
||||
import { PrismaService } from '../../prisma/service';
|
||||
@@ -120,9 +121,14 @@ export class UserResolver {
|
||||
@Public()
|
||||
async user(@Args('email') email: string) {
|
||||
if (!(await this.users.canEarlyAccess(email))) {
|
||||
return new HttpException(
|
||||
return new GraphQLError(
|
||||
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`,
|
||||
401
|
||||
{
|
||||
extensions: {
|
||||
status: HttpStatus[HttpStatus.PAYMENT_REQUIRED],
|
||||
code: HttpStatus.PAYMENT_REQUIRED,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
// TODO: need to limit a user can only get another user witch is in the same workspace
|
||||
|
||||
@@ -174,6 +174,9 @@ export class WorkspaceResolver {
|
||||
return this.prisma.userWorkspacePermission.count({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
userId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -214,6 +217,9 @@ export class WorkspaceResolver {
|
||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
userId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: take || 8,
|
||||
|
||||
@@ -173,6 +173,15 @@ type Query {
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
signUp(name: String!, email: String!, password: String!): UserType!
|
||||
signIn(email: String!, password: String!): UserType!
|
||||
changePassword(token: String!, newPassword: String!): UserType!
|
||||
changeEmail(token: String!): UserType!
|
||||
sendChangePasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
||||
sendSetPasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
||||
sendChangeEmail(email: String!, callbackUrl: String!): Boolean!
|
||||
sendVerifyChangeEmail(token: String!, email: String!, callbackUrl: String!): Boolean!
|
||||
|
||||
"""Create a new workspace"""
|
||||
createWorkspace(init: Upload!): WorkspaceType!
|
||||
|
||||
@@ -196,14 +205,6 @@ type Mutation {
|
||||
removeAvatar: RemoveAvatar!
|
||||
deleteAccount: DeleteAccount!
|
||||
addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList!
|
||||
signUp(name: String!, email: String!, password: String!): UserType!
|
||||
signIn(email: String!, password: String!): UserType!
|
||||
changePassword(token: String!, newPassword: String!): UserType!
|
||||
changeEmail(token: String!): UserType!
|
||||
sendChangePasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
||||
sendSetPasswordEmail(email: String!, callbackUrl: String!): Boolean!
|
||||
sendChangeEmail(email: String!, callbackUrl: String!): Boolean!
|
||||
sendVerifyChangeEmail(token: String!, email: String!, callbackUrl: String!): Boolean!
|
||||
}
|
||||
|
||||
"""The `Upload` scalar type represents a file upload."""
|
||||
|
||||
16
apps/server/tests/sync.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
import test from 'ava';
|
||||
|
||||
test('should be able to bootstrap sync server', async t => {
|
||||
// set env before import
|
||||
process.env.SERVER_FLAVOR = 'sync';
|
||||
const { AppModule } = await import('../src/app');
|
||||
await t.notThrowsAsync(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
await app.close();
|
||||
});
|
||||
process.env.SERVER_FLAVOR = '';
|
||||
});
|
||||
@@ -52,5 +52,5 @@
|
||||
"@blocksuite/lit": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export const AffineWorkspaceCard = () => {
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
currentWorkspaceId={null}
|
||||
isOwner={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,8 +17,10 @@ export default {
|
||||
let id = 0;
|
||||
const image = (
|
||||
<video autoPlay muted loop>
|
||||
<source src="/editingVideo.mp4" type="video/mp4" />
|
||||
<source src="/editingVideo.webm" type="video/webm" />
|
||||
<source
|
||||
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
);
|
||||
export const Basic = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/monorepo",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"private": true,
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
|
||||
2
packages/@types/env/package.json
vendored
@@ -7,5 +7,5 @@
|
||||
"@affine/env": "workspace:*",
|
||||
"@toeverything/infra": "workspace:*"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"peerDependencies": {
|
||||
"ts-node": "*"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
21
packages/cmdk/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Paco Coursey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -69,5 +69,5 @@
|
||||
"vite": "^4.4.9",
|
||||
"yjs": "^13.6.8"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -62,10 +62,12 @@ export const content = style({
|
||||
|
||||
export const postfix = style({
|
||||
justifySelf: 'flex-end',
|
||||
visibility: 'hidden',
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
visibility: 'visible',
|
||||
opacity: 1,
|
||||
pointerEvents: 'all',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
|
||||
import { Skeleton } from '@mui/material';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
@@ -10,46 +13,56 @@ import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
StyledCard,
|
||||
StyledIconContainer,
|
||||
StyledSettingLink,
|
||||
StyledWorkspaceInfo,
|
||||
StyledWorkspaceTitle,
|
||||
StyledWorkspaceTitleArea,
|
||||
StyledWorkspaceType,
|
||||
StyledWorkspaceTypeEllipse,
|
||||
StyledWorkspaceTypeText,
|
||||
} from './styles';
|
||||
|
||||
export interface WorkspaceTypeProps {
|
||||
flavour: WorkspaceFlavour;
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
const WorkspaceType = ({ flavour }: WorkspaceTypeProps) => {
|
||||
const WorkspaceType = ({ flavour, isOwner }: WorkspaceTypeProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
// fixme: cloud regression
|
||||
const isOwner = true;
|
||||
|
||||
if (flavour === WorkspaceFlavour.LOCAL) {
|
||||
return (
|
||||
<p
|
||||
style={{ fontSize: '10px' }}
|
||||
title={t['com.affine.workspaceType.local']()}
|
||||
>
|
||||
<span>{t['com.affine.workspaceType.local']()}</span>
|
||||
</p>
|
||||
<StyledWorkspaceType>
|
||||
<StyledWorkspaceTypeEllipse />
|
||||
<StyledWorkspaceTypeText>{t['Local']()}</StyledWorkspaceTypeText>
|
||||
</StyledWorkspaceType>
|
||||
);
|
||||
}
|
||||
|
||||
return isOwner ? (
|
||||
<p
|
||||
style={{ fontSize: '10px' }}
|
||||
title={t['com.affine.workspaceType.cloud']()}
|
||||
>
|
||||
<span>{t['com.affine.workspaceType.cloud']()}</span>
|
||||
</p>
|
||||
<StyledWorkspaceType>
|
||||
<StyledWorkspaceTypeEllipse cloud={true} />
|
||||
<StyledWorkspaceTypeText>
|
||||
{t['com.affine.brand.affineCloud']()}
|
||||
</StyledWorkspaceTypeText>
|
||||
</StyledWorkspaceType>
|
||||
) : (
|
||||
<p
|
||||
style={{ fontSize: '10px' }}
|
||||
title={t['com.affine.workspaceType.joined']()}
|
||||
>
|
||||
<span>{t['com.affine.workspaceType.joined']()}</span>
|
||||
</p>
|
||||
<StyledWorkspaceType>
|
||||
<StyledWorkspaceTypeEllipse cloud={true} />
|
||||
<StyledWorkspaceTypeText>
|
||||
{t['com.affine.brand.affineCloud']()}
|
||||
</StyledWorkspaceTypeText>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
size="thinner"
|
||||
style={{ margin: '0px 8px', height: '7px' }}
|
||||
/>
|
||||
<Tooltip content={t['com.affine.workspaceType.joined']()}>
|
||||
<StyledIconContainer>
|
||||
<CollaborationIcon />
|
||||
</StyledIconContainer>
|
||||
</Tooltip>
|
||||
</StyledWorkspaceType>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,19 +71,35 @@ export interface WorkspaceCardProps {
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: (workspaceId: string) => void;
|
||||
onSettingClick: (workspaceId: string) => void;
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceCardSkeleton = () => {
|
||||
return (
|
||||
<div>
|
||||
<StyledCard data-testid="workspace-card">
|
||||
<Skeleton variant="circular" width={28} height={28} />
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={43}
|
||||
width={220}
|
||||
style={{ marginLeft: '12px' }}
|
||||
/>
|
||||
</StyledCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceCard = ({
|
||||
onClick,
|
||||
onSettingClick,
|
||||
currentWorkspaceId,
|
||||
meta,
|
||||
isOwner = true,
|
||||
}: WorkspaceCardProps) => {
|
||||
// const t = useAFFiNEI18N();
|
||||
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace);
|
||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
|
||||
|
||||
return (
|
||||
<StyledCard
|
||||
data-testid="workspace-card"
|
||||
@@ -85,6 +114,7 @@ export const WorkspaceCard = ({
|
||||
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>
|
||||
|
||||
<StyledSettingLink
|
||||
size="small"
|
||||
className="setting-entry"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
@@ -92,17 +122,10 @@ export const WorkspaceCard = ({
|
||||
}}
|
||||
withoutHoverStyle={true}
|
||||
>
|
||||
<SettingsIcon style={{ margin: '0px' }} />
|
||||
<SettingsIcon />
|
||||
</StyledSettingLink>
|
||||
</StyledWorkspaceTitleArea>
|
||||
{/* {meta.flavour === WorkspaceFlavour.LOCAL && (
|
||||
<p title={t['com.affine.workspaceType.offline']()}>
|
||||
<LocalDataIcon />
|
||||
<WorkspaceType flavour={meta.flavour} />
|
||||
</p>
|
||||
|
||||
)} */}
|
||||
<WorkspaceType flavour={meta.flavour} />
|
||||
<WorkspaceType isOwner={isOwner} flavour={meta.flavour} />
|
||||
</StyledWorkspaceInfo>
|
||||
</StyledCard>
|
||||
);
|
||||
|
||||
@@ -5,30 +5,16 @@ import { displayFlex, styled, textEllipsis } from '../../../styles';
|
||||
export const StyledWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '12px',
|
||||
width: '202px',
|
||||
p: {
|
||||
height: '20px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
},
|
||||
svg: {
|
||||
marginRight: '10px',
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
span: {
|
||||
flexGrow: 1,
|
||||
...textEllipsis(1),
|
||||
},
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
maxWidth: '200px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 700,
|
||||
lineHeight: '22px',
|
||||
maxWidth: '190px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
@@ -38,13 +24,12 @@ export const StyledCard = styled('div')<{
|
||||
active?: boolean;
|
||||
}>(({ active }) => {
|
||||
const borderColor = active ? 'var(--affine-primary-color)' : 'transparent';
|
||||
const backgroundColor = active ? 'var(--affine-white)' : 'transparent';
|
||||
const backgroundColor = active ? 'var(--affine-white-30)' : 'transparent';
|
||||
return {
|
||||
width: '280px',
|
||||
height: '58px',
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
padding: '12px',
|
||||
borderRadius: '12px',
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${borderColor}`,
|
||||
...displayFlex('flex-start', 'flex-start'),
|
||||
transition: 'background .2s',
|
||||
@@ -91,8 +76,8 @@ export const StyledModalHeader = styled('div')(() => {
|
||||
export const StyledSettingLink = styled(IconButton)(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: '6px',
|
||||
bottom: '6px',
|
||||
right: '10px',
|
||||
top: '10px',
|
||||
opacity: 0,
|
||||
borderRadius: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
@@ -104,9 +89,11 @@ export const StyledSettingLink = styled(IconButton)(() => {
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceType = styled('p')(() => {
|
||||
export const StyledWorkspaceType = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 10,
|
||||
...displayFlex('flex-start', 'center'),
|
||||
width: '100%',
|
||||
height: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -116,3 +103,35 @@ export const StyledWorkspaceTitleArea = styled('div')(() => {
|
||||
justifyContent: 'space-between',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceTypeEllipse = styled('div')<{
|
||||
cloud?: boolean;
|
||||
}>(({ cloud }) => {
|
||||
return {
|
||||
width: '5px',
|
||||
height: '5px',
|
||||
borderRadius: '50%',
|
||||
background: cloud
|
||||
? 'var(--affine-palette-shape-blue)'
|
||||
: 'var(--affine-palette-shape-green)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceTypeText = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
lineHeight: '20px',
|
||||
marginLeft: '4px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledIconContainer = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('flex-start', 'center'),
|
||||
fontSize: '14px',
|
||||
gap: '8px',
|
||||
color: 'var(--affine-icon-secondary)',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,11 @@ import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
|
||||
import { NotFoundPattern } from './not-found-pattern';
|
||||
import { notFoundPageContainer, wrapper } from './styles.css';
|
||||
import {
|
||||
largeButtonEffect,
|
||||
notFoundPageContainer,
|
||||
wrapper,
|
||||
} from './styles.css';
|
||||
|
||||
export interface NotFoundPageProps {
|
||||
user: {
|
||||
@@ -29,12 +33,18 @@ export const NotFoundPage = ({
|
||||
<div className={wrapper}>
|
||||
<NotFoundPattern />
|
||||
</div>
|
||||
<p className={wrapper}>{t['404.hint']()}</p>
|
||||
<div className={wrapper}>
|
||||
<Button type="primary" size="extraLarge" onClick={onBack}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="extraLarge"
|
||||
onClick={onBack}
|
||||
className={largeButtonEffect}
|
||||
>
|
||||
{t['404.back']()}
|
||||
</Button>
|
||||
</div>
|
||||
<p className={wrapper}>{t['404.hint']()}</p>
|
||||
|
||||
{user ? (
|
||||
<div className={wrapper}>
|
||||
<Avatar url={user.avatar} name={user.name} />
|
||||
|
||||
@@ -16,3 +16,6 @@ export const wrapper = style({
|
||||
justifyContent: 'center',
|
||||
margin: '24px auto 0',
|
||||
});
|
||||
export const largeButtonEffect = style({
|
||||
boxShadow: 'var(--affine-large-button-effect) !important',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const menuItemStyle = style({
|
||||
padding: '4px',
|
||||
@@ -12,6 +12,7 @@ export const descriptionStyle = style({
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
textAlign: 'left',
|
||||
padding: '0 2px',
|
||||
});
|
||||
|
||||
export const buttonStyle = style({
|
||||
@@ -66,7 +67,7 @@ export const columnContainerStyle = style({
|
||||
justifyContent: 'center',
|
||||
padding: '0 4px',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const rowContainerStyle = style({
|
||||
@@ -131,3 +132,18 @@ export const shareIconStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const shareLinkStyle = style({
|
||||
padding: '4px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 500,
|
||||
lineHeight: '20px',
|
||||
transform: 'translateX(-4px)',
|
||||
gap: '4px',
|
||||
});
|
||||
globalStyle(`${shareLinkStyle} > span`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
globalStyle(`${shareLinkStyle} > div > svg`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { LinkIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
|
||||
import {
|
||||
ExportToHtmlMenuItem,
|
||||
@@ -7,9 +10,19 @@ import {
|
||||
ExportToPngMenuItem,
|
||||
} from '../page-list/operation-menu-items/export';
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
import { useSharingUrl } from './use-share-url';
|
||||
|
||||
export const ShareExport = () => {
|
||||
export const ShareExport = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaceId = props.workspace.id;
|
||||
const pageId = props.currentPage.id;
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType: 'workspace',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle} style={{ fontWeight: '500' }}>
|
||||
@@ -25,6 +38,17 @@ export const ShareExport = () => {
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.ShareViaExportDescription']()}
|
||||
</div>
|
||||
<Divider size="thinner" />
|
||||
<div>
|
||||
<Button
|
||||
className={styles.shareLinkStyle}
|
||||
onClick={onClickCopyLink}
|
||||
icon={<LinkIcon />}
|
||||
type="plain"
|
||||
>
|
||||
{t['Copy Link']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Menu } from '@toeverything/components/menu';
|
||||
import * as styles from './index.css';
|
||||
import { ShareExport } from './share-export';
|
||||
import { SharePage } from './share-page';
|
||||
|
||||
export interface ShareMenuProps<
|
||||
Workspace extends AffineOfficialWorkspace =
|
||||
| AffineCloudWorkspace
|
||||
@@ -43,7 +42,7 @@ export const ShareMenu = (props: ShareMenuProps) => {
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<Divider size="thinner" />
|
||||
</div>
|
||||
<ShareExport />
|
||||
<ShareExport {...props} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
|
||||
@@ -5,13 +5,14 @@ import { ArrowRightSmallIcon, WebIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import Input from '../../ui/input';
|
||||
import { toast } from '../../ui/toast';
|
||||
import { PublicLinkDisableModal } from './disable-public-link';
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
import { useSharingUrl } from './use-share-url';
|
||||
|
||||
const CloudSvg = () => (
|
||||
<svg
|
||||
@@ -42,6 +43,7 @@ const CloudSvg = () => (
|
||||
|
||||
export const LocalSharePage = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle}>
|
||||
@@ -75,28 +77,23 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
} = props;
|
||||
const [isPublic, setIsPublic] = props.useIsSharedPage(workspaceId, pageId);
|
||||
const [showDisable, setShowDisable] = useState(false);
|
||||
const { sharingUrl, onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType: 'share',
|
||||
});
|
||||
const t = useAFFiNEI18N();
|
||||
const sharingUrl = useMemo(() => {
|
||||
return `${runtimeConfig.serverUrlPrefix}/share/${workspaceId}/${pageId}`;
|
||||
}, [workspaceId, pageId]);
|
||||
|
||||
const onClickCreateLink = useCallback(() => {
|
||||
setIsPublic(true);
|
||||
}, [setIsPublic]);
|
||||
const onClickCopyLink = useCallback(() => {
|
||||
navigator.clipboard
|
||||
.writeText(sharingUrl)
|
||||
.then(() => {
|
||||
toast(t['Copied link to clipboard']());
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [sharingUrl, t]);
|
||||
|
||||
const onDisablePublic = useCallback(() => {
|
||||
setIsPublic(false);
|
||||
toast('Successfully disabled', {
|
||||
portal: document.body,
|
||||
});
|
||||
setShowDisable(false);
|
||||
}, [setIsPublic]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { toast } from '../../ui/toast';
|
||||
|
||||
type UrlType = 'share' | 'workspace';
|
||||
|
||||
type UseSharingUrl = {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
urlType: UrlType;
|
||||
};
|
||||
|
||||
export const generateUrl = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
}: UseSharingUrl) => {
|
||||
return `${runtimeConfig.serverUrlPrefix}/${urlType}/${workspaceId}/${pageId}`;
|
||||
};
|
||||
|
||||
export const useSharingUrl = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
}: UseSharingUrl) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const sharingUrl = useMemo(
|
||||
() => generateUrl({ workspaceId, pageId, urlType }),
|
||||
[urlType, workspaceId, pageId]
|
||||
);
|
||||
|
||||
const onClickCopyLink = useCallback(() => {
|
||||
navigator.clipboard
|
||||
.writeText(sharingUrl)
|
||||
.then(() => {
|
||||
toast(t['Copied link to clipboard']());
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [sharingUrl, t]);
|
||||
|
||||
return {
|
||||
sharingUrl,
|
||||
onClickCopyLink,
|
||||
};
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
/// <reference types="../../type.d.ts" />
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { Modal, type ModalProps } from '@toeverything/components/modal';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import editingVideo from './editingVideo.mp4';
|
||||
import {
|
||||
arrowStyle,
|
||||
buttonDisableStyle,
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
videoSlideStyle,
|
||||
videoStyle,
|
||||
} from './index.css';
|
||||
import switchVideo from './switchVideo.mp4';
|
||||
|
||||
export const TourModal = (props: ModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -92,8 +95,7 @@ export const TourModal = (props: ModalProps) => {
|
||||
})}
|
||||
data-testid="onboarding-modal-editing-video"
|
||||
>
|
||||
<source src="/editingVideo.mp4" type="video/mp4" />
|
||||
<source src="/editingVideo.webm" type="video/webm" />
|
||||
<source src={editingVideo} type="video/mp4" />
|
||||
</video>
|
||||
)}
|
||||
<video
|
||||
@@ -106,8 +108,7 @@ export const TourModal = (props: ModalProps) => {
|
||||
})}
|
||||
data-testid="onboarding-modal-switch-video"
|
||||
>
|
||||
<source src="/switchVideo.mp4" type="video/mp4" />
|
||||
<source src="/switchVideo.webm" type="video/webm" />
|
||||
<source src={switchVideo} type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,9 +16,12 @@ import {
|
||||
} from '@dnd-kit/modifiers';
|
||||
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { WorkspaceCard } from '../../components/card/workspace-card';
|
||||
import {
|
||||
WorkspaceCard,
|
||||
WorkspaceCardSkeleton,
|
||||
} from '../../components/card/workspace-card';
|
||||
import { workspaceItemStyle } from './index.css';
|
||||
|
||||
export interface WorkspaceListProps {
|
||||
@@ -28,16 +31,25 @@ export interface WorkspaceListProps {
|
||||
onClick: (workspaceId: string) => void;
|
||||
onSettingClick: (workspaceId: string) => void;
|
||||
onDragEnd: (event: DragEndEvent) => void;
|
||||
useIsWorkspaceOwner?: (workspaceId: string) => boolean;
|
||||
}
|
||||
|
||||
interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> {
|
||||
item: RootWorkspaceMetadata;
|
||||
useIsWorkspaceOwner?: (workspaceId: string) => boolean;
|
||||
}
|
||||
|
||||
const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => {
|
||||
const SortableWorkspaceItem = ({
|
||||
disabled,
|
||||
item,
|
||||
useIsWorkspaceOwner,
|
||||
currentWorkspaceId,
|
||||
onClick,
|
||||
onSettingClick,
|
||||
}: SortableWorkspaceItemProps) => {
|
||||
const { setNodeRef, attributes, listeners, transform, transition } =
|
||||
useSortable({
|
||||
id: props.item.id,
|
||||
id: item.id,
|
||||
});
|
||||
const style: CSSProperties = useMemo(
|
||||
() => ({
|
||||
@@ -45,11 +57,12 @@ const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => {
|
||||
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
|
||||
: undefined,
|
||||
transition,
|
||||
pointerEvents: props.disabled ? 'none' : undefined,
|
||||
opacity: props.disabled ? 0.6 : undefined,
|
||||
pointerEvents: disabled ? 'none' : undefined,
|
||||
opacity: disabled ? 0.6 : undefined,
|
||||
}),
|
||||
[props.disabled, transform, transition]
|
||||
[disabled, transform, transition]
|
||||
);
|
||||
const isOwner = useIsWorkspaceOwner?.(item.id);
|
||||
return (
|
||||
<div
|
||||
className={workspaceItemStyle}
|
||||
@@ -60,10 +73,11 @@ const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => {
|
||||
{...listeners}
|
||||
>
|
||||
<WorkspaceCard
|
||||
currentWorkspaceId={props.currentWorkspaceId}
|
||||
meta={props.item}
|
||||
onClick={props.onClick}
|
||||
onSettingClick={props.onSettingClick}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
meta={item}
|
||||
onClick={onClick}
|
||||
onSettingClick={onSettingClick}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -106,7 +120,9 @@ export const WorkspaceList = (props: WorkspaceListProps) => {
|
||||
<DndContext sensors={sensors} onDragEnd={onDragEnd} modifiers={modifiers}>
|
||||
<SortableContext items={optimisticList}>
|
||||
{optimisticList.map(item => (
|
||||
<SortableWorkspaceItem {...props} item={item} key={item.id} />
|
||||
<Suspense fallback={<WorkspaceCardSkeleton />} key={item.id}>
|
||||
<SortableWorkspaceItem {...props} item={item} key={item.id} />
|
||||
</Suspense>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
@@ -7,7 +7,6 @@ export const appStyle = style({
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
height: '100vh',
|
||||
transition: 'background-color .5s',
|
||||
display: 'flex',
|
||||
flexGrow: '1',
|
||||
flexDirection: 'row',
|
||||
|
||||
4
packages/component/src/type.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.mp4' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export type ScrollableContainerProps = {
|
||||
className?: string;
|
||||
viewPortClassName?: string;
|
||||
styles?: React.CSSProperties;
|
||||
scrollBarClassName?: string;
|
||||
};
|
||||
|
||||
export const ScrollableContainer = ({
|
||||
@@ -20,6 +21,7 @@ export const ScrollableContainer = ({
|
||||
className,
|
||||
styles: _styles,
|
||||
viewPortClassName,
|
||||
scrollBarClassName,
|
||||
}: PropsWithChildren<ScrollableContainerProps>) => {
|
||||
const [hasScrollTop, ref] = useHasScrollTop();
|
||||
return (
|
||||
@@ -39,7 +41,7 @@ export const ScrollableContainer = ({
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
orientation="vertical"
|
||||
className={clsx(styles.scrollbar, {
|
||||
className={clsx(styles.scrollbar, scrollBarClassName, {
|
||||
[styles.TableScrollbar]: inTableView,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"exclude": ["lib"],
|
||||
"include": ["./src/**/*", "./src/**/*.json"],
|
||||
"include": ["./src/**/*", "./src/**/*.json", "./src/type.d.ts"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.8"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
2
packages/env/package.json
vendored
@@ -27,5 +27,5 @@
|
||||
"dependencies": {
|
||||
"lit": "^2.8.0"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/graphql",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"description": "Autogenerated GraphQL client for affine.pro",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -580,5 +580,9 @@
|
||||
"Successfully enabled AFFiNE Cloud": "Successfully enabled AFFiNE Cloud",
|
||||
"404.hint": "Sorry, you do not have access or this content does not exist...",
|
||||
"404.back": "Back to My Content",
|
||||
"404.signOut": "Sign in to another account"
|
||||
"404.signOut": "Sign in to another account",
|
||||
"com.affine.workspaceList.addWorkspace.create": "Create Workspace",
|
||||
"com.affine.workspaceList.workspaceListType.local": "Local Storage",
|
||||
"com.affine.workspaceList.workspaceListType.cloud": "Cloud Sync",
|
||||
"Local": "Local"
|
||||
}
|
||||
|
||||
@@ -98,5 +98,5 @@
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ExpectedLayout } from '@affine/sdk/entry';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { atom, createStore } from 'jotai/vanilla';
|
||||
|
||||
import { getWorkspace, waitForWorkspace } from './__internal__/workspace.js';
|
||||
import { getActiveBlockSuiteWorkspaceAtom } from './__internal__/workspace';
|
||||
|
||||
// global store
|
||||
let rootStore = createStore();
|
||||
@@ -24,25 +24,10 @@ export const loadedPluginNameAtom = atom<string[]>([]);
|
||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||
export const currentPageIdAtom = atom<string | null>(null);
|
||||
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
|
||||
const currentWorkspaceId = get(currentWorkspaceIdAtom);
|
||||
assertExists(currentWorkspaceId, 'current workspace id');
|
||||
const workspace = getWorkspace(currentWorkspaceId);
|
||||
await waitForWorkspace(workspace);
|
||||
return workspace;
|
||||
});
|
||||
export const currentPageAtom = atom<Promise<Page>>(async get => {
|
||||
const currentWorkspaceId = get(currentWorkspaceIdAtom);
|
||||
assertExists(currentWorkspaceId, 'current workspace id');
|
||||
const currentPageId = get(currentPageIdAtom);
|
||||
assertExists(currentPageId, 'current page id');
|
||||
const workspace = getWorkspace(currentWorkspaceId);
|
||||
await waitForWorkspace(workspace);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
assertExists(page);
|
||||
if (!page.loaded) {
|
||||
await page.waitForLoaded();
|
||||
}
|
||||
return page;
|
||||
const workspaceId = get(currentWorkspaceIdAtom);
|
||||
assertExists(workspaceId);
|
||||
const currentWorkspaceAtom = getActiveBlockSuiteWorkspaceAtom(workspaceId);
|
||||
return get(currentWorkspaceAtom);
|
||||
});
|
||||
|
||||
const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import assert from 'node:assert';
|
||||
import { test } from 'node:test';
|
||||
import test from 'ava';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { SqliteConnection, ValidationResult } from '../index';
|
||||
|
||||
test('db', { concurrency: false }, async t => {
|
||||
await t.test('validate', async () => {
|
||||
const path = fileURLToPath(
|
||||
new URL('./fixtures/test01.affine', import.meta.url)
|
||||
);
|
||||
const result = await SqliteConnection.validate(path);
|
||||
assert.equal(result, ValidationResult.MissingVersionColumn);
|
||||
});
|
||||
test('db validate', async t => {
|
||||
const path = fileURLToPath(
|
||||
new URL('./fixtures/test01.affine', import.meta.url)
|
||||
);
|
||||
const result = await SqliteConnection.validate(path);
|
||||
t.is(result, ValidationResult.MissingVersionColumn);
|
||||
});
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import assert, { doesNotThrow } from 'node:assert';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { lastValueFrom, Subject } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FsWatcher } from '../index';
|
||||
|
||||
test('fs watch', { concurrency: false }, async t => {
|
||||
let watcher: FsWatcher;
|
||||
let fixture: string;
|
||||
t.beforeEach(async () => {
|
||||
const fixtureName = `fs-${v4()}.fixture`;
|
||||
fixture = join(fileURLToPath(import.meta.url), '..', fixtureName);
|
||||
await fs.writeFile(fixture, '\n');
|
||||
watcher = FsWatcher.watch(fixture);
|
||||
});
|
||||
|
||||
t.afterEach(async () => {
|
||||
FsWatcher.close();
|
||||
await fs.unlink(fixture).catch(() => false);
|
||||
});
|
||||
|
||||
await t.test('should watch without error', () => {
|
||||
doesNotThrow(() => {
|
||||
const subscription = watcher.subscribe(() => {});
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
await t.test('should watch file change', () => {
|
||||
return (async () => {
|
||||
const defer = new Subject<void>();
|
||||
const subscription = watcher.subscribe(
|
||||
event => {
|
||||
assert.deepEqual(event.paths, [fixture]);
|
||||
subscription.unsubscribe();
|
||||
defer.next();
|
||||
defer.complete();
|
||||
},
|
||||
err => {
|
||||
subscription.unsubscribe();
|
||||
defer.error(err);
|
||||
}
|
||||
);
|
||||
await fs.appendFile(fixture, 'test');
|
||||
return lastValueFrom(defer.asObservable());
|
||||
})();
|
||||
});
|
||||
|
||||
await t.test('should watch file delete', () => {
|
||||
return (async () => {
|
||||
const defer = new Subject<void>();
|
||||
const subscription = watcher.subscribe(
|
||||
event => {
|
||||
if (typeof event.type === 'object' && 'rename' in event.type) {
|
||||
assert.deepEqual(event.paths, [fixture]);
|
||||
assert.deepEqual(event.type, {
|
||||
remove: {
|
||||
kind: 'file',
|
||||
},
|
||||
});
|
||||
}
|
||||
subscription.unsubscribe();
|
||||
defer.next();
|
||||
defer.complete();
|
||||
},
|
||||
err => {
|
||||
subscription.unsubscribe();
|
||||
defer.error(err);
|
||||
}
|
||||
);
|
||||
await fs.unlink(fixture);
|
||||
return lastValueFrom(defer.asObservable());
|
||||
})();
|
||||
});
|
||||
});
|
||||
3
packages/native/fs-watcher.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
import type { FsWatcher } from './index';
|
||||
|
||||
export function createFSWatcher(): typeof FsWatcher;
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports.createFSWatcher = function createFSWatcher() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { FsWatcher } = require('./index');
|
||||
return FsWatcher;
|
||||
};
|
||||
36
packages/native/index.d.ts
vendored
@@ -3,26 +3,6 @@
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
export interface WatchOptions {
|
||||
recursive?: boolean;
|
||||
}
|
||||
/** Watcher kind enumeration */
|
||||
export enum WatcherKind {
|
||||
/** inotify backend (linux) */
|
||||
Inotify = 'Inotify',
|
||||
/** FS-Event backend (mac) */
|
||||
Fsevent = 'Fsevent',
|
||||
/** KQueue backend (bsd,optionally mac) */
|
||||
Kqueue = 'Kqueue',
|
||||
/** Polling based backend (fallback) */
|
||||
PollWatcher = 'PollWatcher',
|
||||
/** Windows backend */
|
||||
ReadDirectoryChangesWatcher = 'ReadDirectoryChangesWatcher',
|
||||
/** Fake watcher for testing */
|
||||
NullWatcher = 'NullWatcher',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
export function moveFile(src: string, dst: string): Promise<void>;
|
||||
export interface BlobRow {
|
||||
key: string;
|
||||
data: Buffer;
|
||||
@@ -45,22 +25,6 @@ export enum ValidationResult {
|
||||
GeneralError = 3,
|
||||
Valid = 4,
|
||||
}
|
||||
export class Subscription {
|
||||
toString(): string;
|
||||
unsubscribe(): void;
|
||||
}
|
||||
export type FSWatcher = FsWatcher;
|
||||
export class FsWatcher {
|
||||
static watch(p: string, options?: WatchOptions | undefined | null): FsWatcher;
|
||||
static kind(): WatcherKind;
|
||||
toString(): string;
|
||||
subscribe(
|
||||
callback: (event: import('./event').NotifyEvent) => void,
|
||||
errorCallback?: (err: Error) => void
|
||||
): Subscription;
|
||||
static unwatch(p: string): void;
|
||||
static close(): void;
|
||||
}
|
||||
export class SqliteConnection {
|
||||
constructor(path: string);
|
||||
connect(): Promise<void>;
|
||||
|
||||
@@ -263,18 +263,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`);
|
||||
}
|
||||
|
||||
const {
|
||||
WatcherKind,
|
||||
Subscription,
|
||||
FsWatcher,
|
||||
moveFile,
|
||||
SqliteConnection,
|
||||
ValidationResult,
|
||||
} = nativeBinding;
|
||||
const { SqliteConnection, ValidationResult } = nativeBinding;
|
||||
|
||||
module.exports.WatcherKind = WatcherKind;
|
||||
module.exports.Subscription = Subscription;
|
||||
module.exports.FsWatcher = FsWatcher;
|
||||
module.exports.moveFile = moveFile;
|
||||
module.exports.SqliteConnection = SqliteConnection;
|
||||
module.exports.ValidationResult = ValidationResult;
|
||||
|
||||
@@ -17,10 +17,28 @@
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"ava": {
|
||||
"extensions": {
|
||||
"mts": "module"
|
||||
},
|
||||
"nodeArguments": [
|
||||
"--loader",
|
||||
"ts-node/esm.mjs",
|
||||
"--es-module-specifier-resolution",
|
||||
"node"
|
||||
],
|
||||
"files": [
|
||||
"__tests__/*.spec.mts"
|
||||
],
|
||||
"environmentVariables": {
|
||||
"TS_NODE_PROJECT": "./tsconfig.json"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.16.2",
|
||||
"@types/node": "^18.17.12",
|
||||
"@types/uuid": "^9.0.3",
|
||||
"@napi-rs/cli": "^2.16.3",
|
||||
"@types/node": "^18.18.5",
|
||||
"@types/uuid": "^9.0.5",
|
||||
"ava": "^5.3.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"rxjs": "^7.8.1",
|
||||
"ts-node": "^10.9.1",
|
||||
@@ -35,8 +53,8 @@
|
||||
"build": "napi build --platform --release --no-const-enum",
|
||||
"build:debug": "napi build --platform --no-const-enum",
|
||||
"universal": "napi universal",
|
||||
"test": "cross-env TS_NODE_TRANSPILE_ONLY=1 TS_NODE_PROJECT=./tsconfig.json node --test --loader ts-node/esm --experimental-specifier-resolution=node ./__tests__/**/*.mts",
|
||||
"test": "ava",
|
||||
"version": "napi version"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
use std::{collections::BTreeMap, path::Path, sync::Arc};
|
||||
|
||||
use napi::{
|
||||
bindgen_prelude::{FromNapiValue, ToNapiValue},
|
||||
threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use napi_derive::napi;
|
||||
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
static GLOBAL_WATCHER: Lazy<napi::Result<GlobalWatcher>> = Lazy::new(|| {
|
||||
let event_emitter = Arc::new(Mutex::new(EventEmitter {
|
||||
listeners: Default::default(),
|
||||
error_callbacks: Default::default(),
|
||||
}));
|
||||
let event_emitter_in_handler = event_emitter.clone();
|
||||
let watcher: RecommendedWatcher =
|
||||
notify::recommended_watcher(move |res: notify::Result<Event>| {
|
||||
event_emitter_in_handler.lock().on(res);
|
||||
})
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(GlobalWatcher {
|
||||
inner: Mutex::new(watcher),
|
||||
event_emitter,
|
||||
})
|
||||
});
|
||||
|
||||
struct GlobalWatcher {
|
||||
inner: Mutex<RecommendedWatcher>,
|
||||
event_emitter: Arc<Mutex<EventEmitter>>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Default)]
|
||||
pub struct WatchOptions {
|
||||
pub recursive: Option<bool>,
|
||||
}
|
||||
|
||||
#[napi(string_enum)]
|
||||
/// Watcher kind enumeration
|
||||
pub enum WatcherKind {
|
||||
/// inotify backend (linux)
|
||||
Inotify,
|
||||
/// FS-Event backend (mac)
|
||||
Fsevent,
|
||||
/// KQueue backend (bsd,optionally mac)
|
||||
Kqueue,
|
||||
/// Polling based backend (fallback)
|
||||
PollWatcher,
|
||||
/// Windows backend
|
||||
ReadDirectoryChangesWatcher,
|
||||
/// Fake watcher for testing
|
||||
NullWatcher,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<notify::WatcherKind> for WatcherKind {
|
||||
fn from(value: notify::WatcherKind) -> Self {
|
||||
match value {
|
||||
notify::WatcherKind::Inotify => WatcherKind::Inotify,
|
||||
notify::WatcherKind::Fsevent => WatcherKind::Fsevent,
|
||||
notify::WatcherKind::Kqueue => WatcherKind::Kqueue,
|
||||
notify::WatcherKind::PollWatcher => WatcherKind::PollWatcher,
|
||||
notify::WatcherKind::ReadDirectoryChangesWatcher => WatcherKind::ReadDirectoryChangesWatcher,
|
||||
notify::WatcherKind::NullWatcher => WatcherKind::NullWatcher,
|
||||
_ => WatcherKind::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct Subscription {
|
||||
id: uuid::Uuid,
|
||||
error_uuid: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Subscription {
|
||||
#[napi]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn unsubscribe(&mut self) -> napi::Result<()> {
|
||||
let mut event_emitter = GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.event_emitter
|
||||
.lock();
|
||||
event_emitter.listeners.remove(&self.id);
|
||||
if let Some(error_uuid) = &self.error_uuid {
|
||||
event_emitter.error_callbacks.remove(error_uuid);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct FSWatcher {
|
||||
path: String,
|
||||
recursive: RecursiveMode,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FSWatcher {
|
||||
#[napi(factory)]
|
||||
pub fn watch(p: String, options: Option<WatchOptions>) -> Self {
|
||||
let options = options.unwrap_or_default();
|
||||
FSWatcher {
|
||||
path: p,
|
||||
recursive: if options.recursive == Some(false) {
|
||||
RecursiveMode::NonRecursive
|
||||
} else {
|
||||
RecursiveMode::Recursive
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn kind() -> WatcherKind {
|
||||
RecommendedWatcher::kind().into()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn to_string(&self) -> napi::Result<String> {
|
||||
Ok(format!(
|
||||
"{:?}",
|
||||
GLOBAL_WATCHER.as_ref().map_err(|err| err.clone())?.inner
|
||||
))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn subscribe(
|
||||
&mut self,
|
||||
#[napi(ts_arg_type = "(event: import('./event').NotifyEvent) => void")]
|
||||
callback: ThreadsafeFunction<serde_json::Value, ErrorStrategy::Fatal>,
|
||||
#[napi(ts_arg_type = "(err: Error) => void")] error_callback: Option<ThreadsafeFunction<()>>,
|
||||
) -> napi::Result<Subscription> {
|
||||
GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.inner
|
||||
.lock()
|
||||
.watch(Path::new(&self.path), self.recursive)
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
let mut event_emitter = GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.event_emitter
|
||||
.lock();
|
||||
event_emitter
|
||||
.listeners
|
||||
.insert(uuid, (self.path.clone(), callback));
|
||||
let mut error_uuid = None;
|
||||
if let Some(error_callback) = error_callback {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
event_emitter.error_callbacks.insert(uuid, error_callback);
|
||||
error_uuid = Some(uuid);
|
||||
}
|
||||
drop(event_emitter);
|
||||
Ok(Subscription {
|
||||
id: uuid,
|
||||
error_uuid,
|
||||
})
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn unwatch(p: String) -> napi::Result<()> {
|
||||
let mut watcher = GLOBAL_WATCHER
|
||||
.as_ref()
|
||||
.map_err(|err| err.clone())?
|
||||
.inner
|
||||
.lock();
|
||||
watcher
|
||||
.unwatch(Path::new(&p))
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn close() -> napi::Result<()> {
|
||||
let global_watcher = GLOBAL_WATCHER.as_ref().map_err(|err| err.clone())?;
|
||||
global_watcher.event_emitter.lock().stop();
|
||||
let mut inner = global_watcher.inner.lock();
|
||||
*inner = notify::recommended_watcher(|_| {}).map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EventEmitter {
|
||||
listeners: BTreeMap<
|
||||
uuid::Uuid,
|
||||
(
|
||||
String,
|
||||
ThreadsafeFunction<serde_json::Value, ErrorStrategy::Fatal>,
|
||||
),
|
||||
>,
|
||||
error_callbacks: BTreeMap<uuid::Uuid, ThreadsafeFunction<()>>,
|
||||
}
|
||||
|
||||
impl EventEmitter {
|
||||
fn on(&self, event: notify::Result<Event>) {
|
||||
match event {
|
||||
Ok(e) => match serde_json::value::to_value(&e) {
|
||||
Err(err) => {
|
||||
let err: napi::Error = anyhow::Error::from(err).into();
|
||||
for on_error in self.error_callbacks.values() {
|
||||
on_error.call(Err(err.clone()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
}
|
||||
}
|
||||
Ok(v) => {
|
||||
for (path, on_event) in self.listeners.values() {
|
||||
if e.paths.iter().any(|p| p.to_str() == Some(path)) {
|
||||
on_event.call(v.clone(), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
let err: napi::Error = anyhow::Error::from(err).into();
|
||||
for on_error in self.error_callbacks.values() {
|
||||
on_error.call(Err(err.clone()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&mut self) {
|
||||
self.listeners.clear();
|
||||
self.error_callbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn move_file(src: String, dst: String) -> napi::Result<()> {
|
||||
tokio::fs::rename(src, dst).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod fs;
|
||||
pub mod sqlite;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/plugin-cli",
|
||||
"type": "module",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"bin": {
|
||||
"af": "./src/af.mjs"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/sdk",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
|
||||
@@ -60,6 +60,6 @@ export declare const pushLayoutAtom: WritableAtom<
|
||||
void
|
||||
>;
|
||||
export declare const deleteLayoutAtom: WritableAtom<null, [string], void>;
|
||||
export declare const currentPageAtom: Atom<Promise<Page>>;
|
||||
export declare const currentPageIdAtom: Atom<string | null>;
|
||||
export declare const currentWorkspaceAtom: Atom<Promise<Workspace>>;
|
||||
export declare const rootStore: ReturnType<typeof getDefaultStore>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/storage",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0 < 11 || >= 11.8.0"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"./v1/*.json": "./v1/*.json",
|
||||
"./preloading.json": "./preloading.json"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "(put a symbol) at the bottom right of the page to insert a new block, list,database, etc."
|
||||
"insert": "(put a symbol) at the bottom right of the page to insert a new block, list, database, etc."
|
||||
}
|
||||
],
|
||||
"prop:checked": false
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"prop:type": "text",
|
||||
"prop:text": [
|
||||
{
|
||||
"insert": "No matter you're "
|
||||
"insert": "No matter if you're "
|
||||
},
|
||||
{
|
||||
"insert": "organizing your personal life",
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": ", our templates gallery has got you covered! "
|
||||
"insert": ", our templates galleries have got you covered! "
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -79,7 +79,7 @@
|
||||
"sys:children": [],
|
||||
"prop:text": [
|
||||
{
|
||||
"insert": "Here We offer a wide range of resources to meet your unique needs and help you achieve your goals, whether it's in your personal or professional life."
|
||||
"insert": "Here We offer a wide range of resources to meet your unique needs and help you achieve your goals, whether in your personal or professional life."
|
||||
}
|
||||
],
|
||||
"prop:type": "text"
|
||||
@@ -96,7 +96,7 @@
|
||||
"prop:type": "quote",
|
||||
"prop:text": [
|
||||
{
|
||||
"insert": "Tired of managing your notes or coming up with a effient work plan? Whether you're a student, parent, or have diverse interests, here we provide few templates to kick off your journey and unlock your true potential with AFFiNE and reap incredible benefits."
|
||||
"insert": "Tired of managing your notes or coming up with a effient work plan? Whether you're a student, parent, or have diverse interests, here we provide a few templates to kick off your journey and unlock your true potential with AFFiNE and reap incredible benefits."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/workers",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "wrangler dev"
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"@types/ws": "^8.5.5",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"version": "0.9.0-canary.13"
|
||||
"version": "0.9.0-beta.3"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@toeverything/y-indexeddb",
|
||||
"type": "module",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"description": "IndexedDB database adapter for Yjs",
|
||||
"repository": "toeverything/AFFiNE",
|
||||
"author": "toeverything",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/y-provider",
|
||||
"type": "module",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"description": "Yjs provider utilities for AFFiNE",
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/bookmark-plugin",
|
||||
"type": "module",
|
||||
"version": "0.9.0-canary.13",
|
||||
"version": "0.9.0-beta.3",
|
||||
"description": "Bookmark Plugin",
|
||||
"affinePlugin": {
|
||||
"release": true,
|
||||
|
||||