Compare commits

...

30 Commits

Author SHA1 Message Date
LongYinan fcb12a8329 v0.9.0-beta.3 2023-10-20 11:40:35 +08:00
Joooye_34 1fa48a8ee5 feat(core): change favicon (#4663) 2023-10-20 11:39:54 +08:00
Alex Yang 9b376cbe19 refactor(native): remove unused code (#4651) 2023-10-20 11:39:01 +08:00
Peng Xiao 5acd6bf64c fix(electron): app image icon (#4442) 2023-10-20 11:21:26 +08:00
LongYinan 46f8e3af14 v0.9.0-beta.2 2023-10-10 11:46:12 +08:00
dependabot[bot] cf8b62d804 chore: bump electron from 26.2.2 to 26.3.0 (#4564)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 11:40:58 +08:00
Zero King a5df724f4e chore: reword template galleries introduction (#4548) 2023-10-10 11:37:36 +08:00
Qinghao Huang ea0487316c fix: spacing issue in getting-started template (#4540) 2023-10-10 11:35:51 +08:00
Joooye_34 ae3087a203 fix(component): content should subtract height of the header (#4507) 2023-10-10 11:35:34 +08:00
liuyi 148db56eaa fix(core): setting ui regression (#4525) 2023-10-10 11:35:19 +08:00
JimmFly bef184c0e1 feat(component): add private copy link button (#4508) 2023-10-10 11:35:02 +08:00
liuyi bd64ee3af0 fix(server): wrong member count query (#4506) 2023-10-10 11:34:31 +08:00
Alex Yang a7e79c6c53 fix: current page atom (#4515) 2023-10-10 11:34:25 +08:00
Alex Yang b7df15b6ac fix(core): page update date (#4502) 2023-10-10 11:32:37 +08:00
JimmFly 184ede66bb fix: adjust 404 page style (#4491) 2023-10-10 11:31:53 +08:00
JimmFly 8e987231d4 fix: unexpected pop ups (#4468) 2023-10-10 11:31:29 +08:00
Joooye_34 37c6e81cbc fix(component): background animation is different (#4495) 2023-10-10 11:31:12 +08:00
JimmFly eab5ad3f12 fix: unexpected hover behavior of collection sidebar (#4490) 2023-10-10 11:30:31 +08:00
Peng Xiao 08c65b1227 fix: register command re-rendering (#4476) 2023-10-10 11:29:41 +08:00
LongYinan 3075a29777 0.9.0-beta.1 2023-09-23 02:07:08 -07:00
LongYinan d67facbc9c fix(core): error state for non early access user while signing in with email (#4467) 2023-09-23 02:06:56 -07:00
LongYinan 2418e2c0a8 fix(server): missing dependency in sync app (#4465) 2023-09-23 02:06:51 -07:00
Alex Yang 15e18036dc test: fix flaky (#4463) 2023-09-23 02:06:46 -07:00
Alex Yang 86ca624fa6 refactor(infra): simplify currentWorkspaceAtom (#4462) 2023-09-23 02:06:39 -07:00
Alex Yang 3c5a36e042 docs: upload LICENSE.md 2023-09-23 02:06:33 -07:00
Alex Yang 07c20830f7 docs: update README.md
There are no core members actually, people is just a paid guy.
2023-09-23 02:06:25 -07:00
Peng Xiao 32ffa36604 chore: bump components version (#4454) 2023-09-23 02:06:02 -07:00
JimmFly 818476d3ab refactor: workspace list (#4432) 2023-09-23 02:05:47 -07:00
Alex Yang bade53987f fix(electron): missing video (#4451) 2023-09-23 02:05:37 -07:00
LongYinan a04d7616e1 v0.9.0-beta.0 2023-09-21 23:04:36 -07:00
120 changed files with 1325 additions and 1425 deletions
+19
View File
@@ -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 }}
+1 -12
View File
@@ -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).
+1 -1
View File
@@ -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 -2
View File
@@ -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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+3 -3
View File
@@ -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}
+23 -11
View File
@@ -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,
]
);
}
+22 -7
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@affine/docs",
"version": "0.9.0-canary.13",
"version": "0.9.0-beta.3",
"type": "module",
"private": true,
"scripts": {
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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",
+4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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": {
+2
View File
@@ -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],
+9 -3
View File
@@ -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,
+9 -8
View File
@@ -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
View 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 = '';
});
+1 -1
View File
@@ -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 -1
View File
@@ -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",
+1 -1
View File
@@ -7,5 +7,5 @@
"@affine/env": "workspace:*",
"@toeverything/infra": "workspace:*"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}
+1 -1
View File
@@ -20,5 +20,5 @@
"peerDependencies": {
"ts-node": "*"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}
+21
View 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.
+1 -1
View File
@@ -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
View 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 -1
View File
@@ -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,
+1 -1
View File
@@ -8,5 +8,5 @@
"devDependencies": {
"@types/debug": "^4.1.8"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}
+1 -1
View File
@@ -27,5 +27,5 @@
"dependencies": {
"lit": "^2.8.0"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -55,5 +55,5 @@
"optional": true
}
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}
+1 -1
View File
@@ -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"
}
+5 -1
View File
@@ -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"
}
+1 -1
View File
@@ -98,5 +98,5 @@
"optional": true
}
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}
+6 -21
View File
@@ -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');
+7 -10
View File
@@ -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);
});
-80
View File
@@ -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
View File
@@ -1,3 +0,0 @@
import type { FsWatcher } from './index';
export function createFSWatcher(): typeof FsWatcher;
-5
View File
@@ -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
View File
@@ -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>;
+1 -12
View File
@@ -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;
+23 -5
View File
@@ -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"
}
-243
View File
@@ -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
View File
@@ -1,2 +1 @@
pub mod fs;
pub mod sqlite;
+1 -1
View File
@@ -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 -1
View File
@@ -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",
+1 -1
View File
@@ -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 -1
View File
@@ -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"
},
+1 -1
View File
@@ -7,5 +7,5 @@
"./v1/*.json": "./v1/*.json",
"./preloading.json": "./preloading.json"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}
+1 -1
View File
@@ -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 -1
View File
@@ -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"
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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,

Some files were not shown because too many files have changed in this diff Show More