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

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 }}

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).

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',
},
{

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

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,
},

View File

@@ -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) {

View File

@@ -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>
);
};

View File

@@ -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',
});

View File

@@ -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>

View File

@@ -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',
});

View File

@@ -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',
});

View File

@@ -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>

View File

@@ -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',
});

View File

@@ -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>
);
};

View File

@@ -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',
});

View File

@@ -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>
);
};

View File

@@ -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)',
}
: {}),
};
});

View File

@@ -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)',
});

View File

@@ -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>
);
};

View File

@@ -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',
});

View File

@@ -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>
);
};

View File

@@ -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
);

View File

@@ -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',

View File

@@ -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}

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,
]
);
}

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 />
</>

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": {

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,

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",

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,

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",

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": {

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],

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

View File

@@ -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,

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."""

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 = '';
});

View File

@@ -52,5 +52,5 @@
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}

View File

@@ -37,6 +37,7 @@ export const AffineWorkspaceCard = () => {
onClick={() => {}}
onSettingClick={() => {}}
currentWorkspaceId={null}
isOwner={true}
/>
);
};

View File

@@ -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 = () => {

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",

View File

@@ -7,5 +7,5 @@
"@affine/env": "workspace:*",
"@toeverything/infra": "workspace:*"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}

View File

@@ -20,5 +20,5 @@
"peerDependencies": {
"ts-node": "*"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}

21
packages/cmdk/LICENSE.md Normal file
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.

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"
}

View File

@@ -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',
},
},
});

View File

@@ -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>
);

View File

@@ -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)',
};
});

View File

@@ -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} />

View File

@@ -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',
});

View File

@@ -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)',
});

View File

@@ -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>
</>
);

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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,
};
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -7,7 +7,6 @@ export const appStyle = style({
width: '100%',
position: 'relative',
height: '100vh',
transition: 'background-color .5s',
display: 'flex',
flexGrow: '1',
flexDirection: 'row',

4
packages/component/src/type.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*.mp4' {
const src: string;
export default src;
}

View File

@@ -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,
})}
>

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,

View File

@@ -8,5 +8,5 @@
"devDependencies": {
"@types/debug": "^4.1.8"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}

View File

@@ -27,5 +27,5 @@
"dependencies": {
"lit": "^2.8.0"
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}

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",

View File

@@ -55,5 +55,5 @@
"optional": true
}
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}

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"
}

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"
}

View File

@@ -98,5 +98,5 @@
"optional": true
}
},
"version": "0.9.0-canary.13"
"version": "0.9.0-beta.3"
}

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');

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);
});

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());
})();
});
});

View File

@@ -1,3 +0,0 @@
import type { FsWatcher } from './index';
export function createFSWatcher(): typeof FsWatcher;

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;
};

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>;

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;

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"
}

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(())
}

View File

@@ -1,2 +1 @@
pub mod fs;
pub mod sqlite;

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"
},

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",

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>;

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"
},

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"
}

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

View File

@@ -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."
}
]
},

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"

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"
}

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",

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",

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