Compare commits

...

14 Commits

Author SHA1 Message Date
forehalo
99e70c91e8 perf(core): avoid page init when only id required (#7867) 2024-08-14 05:22:18 +00:00
doouding
05ac3dbdcb feat: bump bs (#7866) 2024-08-14 04:42:20 +00:00
JimmFly
994b539507 fix(admin): user form not dynamically updating as expected (#7858)
- Fixed the issue that a certain feature must be enabled when creating a user
- Fixed the issue that the modified content was not reset to the default content when exiting the user form without saving after modification
- Fixed the issue that the content did not switch as expected when switching user forms of different users

https://github.com/user-attachments/assets/02567021-9342-4ed1-be77-3bdcbb3d86ab
2024-08-14 04:28:15 +00:00
pengx17
d5edadabe6 fix(electron): cmd+num not working on mac (#7865)
fix AF-1248
hidden menu group + acceleratorWorksWhenHidden does not work on mac
2024-08-14 04:14:16 +00:00
JimmFly
05247bb24e fix(admin): frequent query requests in the search (#7854) 2024-08-14 03:49:45 +00:00
forehalo
f69f026ac3 fix(admin): avoid frequent refetch (#7863) 2024-08-14 03:34:41 +00:00
forehalo
015247345c chore(admin): organize massive routes (#7857) 2024-08-14 03:34:38 +00:00
forehalo
0ba516866f fix(server): change password with token should be public (#7855) 2024-08-14 03:34:35 +00:00
forehalo
7afba6b8b5 fix(server): prelude should load both local and remote config file (#7852) 2024-08-14 03:34:33 +00:00
renovate
08cc15a55c chore: bump up oxlint version to v0.7.1 (#7846)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [oxlint](https://oxc.rs) ([source](https://togithub.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`0.7.0` -> `0.7.1`](https://renovatebot.com/diffs/npm/oxlint/0.7.0/0.7.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxlint/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxlint/0.7.0/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/0.7.0/0.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v0.7.1`](https://togithub.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#071---2024-08-12)

[Compare Source](3ac02fd838...972492cc4d)

##### Features

-   [`cc922f4`](https://togithub.com/oxc-project/oxc/commit/cc922f4) vscode: Provide config's schema to oxlint config files ([#&#8203;4826](https://togithub.com/oxc-project/oxc/issues/4826)) (Don Isaac)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yMC4xIiwidXBkYXRlZEluVmVyIjoiMzguMjAuMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-08-13 14:02:00 +00:00
DarkSky
24c34eb3fc fix: admin panel schema (#7851) 2024-08-13 09:16:56 +00:00
JimmFly
ba5ba71f35 chore: add test for all collection and all tag (#7687) 2024-08-13 08:19:54 +00:00
pengx17
d4065fee78 fix(electron): adjust app-tabs-header styles (#7849) 2024-08-13 08:06:30 +00:00
pengx17
d86f7f41dc fix: center peek support open in new tab (#7848) 2024-08-13 07:52:03 +00:00
51 changed files with 731 additions and 608 deletions

View File

@@ -95,7 +95,7 @@
"nanoid": "^5.0.7",
"nx": "^19.0.0",
"nyc": "^17.0.0",
"oxlint": "0.7.0",
"oxlint": "0.7.1",
"prettier": "^3.2.5",
"semver": "^7.6.0",
"serve": "^14.2.1",

View File

@@ -16,6 +16,7 @@ import {
EmailTokenNotFound,
EmailVerificationRequired,
InvalidEmailToken,
LinkExpired,
SameEmailProvided,
SkipThrottle,
Throttle,
@@ -89,12 +90,17 @@ export class AuthResolver {
};
}
@Mutation(() => UserType)
@Public()
@Mutation(() => Boolean)
async changePassword(
@CurrentUser() user: CurrentUser,
@Args('token') token: string,
@Args('newPassword') newPassword: string
@Args('newPassword') newPassword: string,
@Args('userId', { type: () => String, nullable: true }) userId?: string
) {
if (!userId) {
throw new LinkExpired();
}
const config = await this.config.runtime.fetchAll({
'auth/password.max': true,
'auth/password.min': true,
@@ -108,7 +114,7 @@ export class AuthResolver {
TokenType.ChangePassword,
token,
{
credential: user.id,
credential: userId,
}
);
@@ -116,10 +122,10 @@ export class AuthResolver {
throw new InvalidEmailToken();
}
await this.auth.changePassword(user.id, newPassword);
await this.auth.revokeUserSessions(user.id);
await this.auth.changePassword(userId, newPassword);
await this.auth.revokeUserSessions(userId);
return user;
return true;
}
@Mutation(() => UserType)
@@ -163,7 +169,7 @@ export class AuthResolver {
user.id
);
const url = this.url.link(callbackUrl, { token });
const url = this.url.link(callbackUrl, { userId: user.id, token });
const res = await this.auth.sendChangePasswordEmail(user.email, url);
@@ -176,19 +182,7 @@ export class AuthResolver {
@Args('callbackUrl') callbackUrl: string,
@Args('email', { nullable: true }) _email?: string
) {
if (!user.emailVerified) {
throw new EmailVerificationRequired();
}
const token = await this.token.createToken(
TokenType.ChangePassword,
user.id
);
const url = this.url.link(callbackUrl, { token });
const res = await this.auth.sendSetPasswordEmail(user.email, url);
return !res.rejected.length;
return this.sendChangePasswordEmail(user, callbackUrl);
}
// The change email step is:
@@ -305,6 +299,7 @@ export class AuthResolver {
TokenType.ChangePassword,
userId
);
return this.url.link(callbackUrl, { token });
return this.url.link(callbackUrl, { userId, token });
}
}

View File

@@ -4,6 +4,7 @@ import { Module } from '@nestjs/common';
import {
ServerConfigResolver,
ServerFeatureConfigResolver,
ServerRuntimeConfigResolver,
ServerServiceConfigResolver,
} from './resolver';
@@ -11,6 +12,7 @@ import {
@Module({
providers: [
ServerConfigResolver,
ServerFeatureConfigResolver,
ServerRuntimeConfigResolver,
ServerServiceConfigResolver,
],

View File

@@ -279,6 +279,10 @@ export const USER_FRIENDLY_ERRORS = {
type: 'invalid_input',
message: 'An invalid email token provided.',
},
link_expired: {
type: 'bad_request',
message: 'The link has expired.',
},
// Authentication & Permission Errors
authentication_required: {

View File

@@ -137,6 +137,12 @@ export class InvalidEmailToken extends UserFriendlyError {
}
}
export class LinkExpired extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'link_expired', message);
}
}
export class AuthenticationRequired extends UserFriendlyError {
constructor(message?: string) {
super('authentication_required', 'authentication_required', message);
@@ -520,6 +526,7 @@ export enum ErrorNames {
SIGN_UP_FORBIDDEN,
EMAIL_TOKEN_NOT_FOUND,
INVALID_EMAIL_TOKEN,
LINK_EXPIRED,
AUTHENTICATION_REQUIRED,
ACTION_FORBIDDEN,
ACCESS_DENIED,

View File

@@ -1,7 +1,7 @@
import 'reflect-metadata';
import { cpSync } from 'node:fs';
import { join } from 'node:path';
import { join, parse } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { config } from 'dotenv';
@@ -12,20 +12,28 @@ import {
} from './fundamentals/config';
import { enablePlugin } from './plugins';
const configDir = join(fileURLToPath(import.meta.url), '../config');
const PROJECT_CONFIG_PATH = join(fileURLToPath(import.meta.url), '../config');
async function loadRemote(remoteDir: string, file: string) {
const filePath = join(configDir, file);
if (configDir !== remoteDir) {
cpSync(join(remoteDir, file), filePath, {
let fileToLoad = join(PROJECT_CONFIG_PATH, file);
if (PROJECT_CONFIG_PATH !== remoteDir) {
const remoteFile = join(remoteDir, file);
const remoteFileAtLocal = join(
PROJECT_CONFIG_PATH,
parse(file).name + '.remote.js'
);
cpSync(remoteFile, remoteFileAtLocal, {
force: true,
});
fileToLoad = remoteFileAtLocal;
}
await import(pathToFileURL(filePath).href);
await import(pathToFileURL(fileToLoad).href);
}
async function load() {
const AFFiNE_CONFIG_PATH = process.env.AFFINE_CONFIG_PATH ?? configDir;
const AFFiNE_CONFIG_PATH =
process.env.AFFINE_CONFIG_PATH ?? PROJECT_CONFIG_PATH;
// Initializing AFFiNE config
//
// 1. load dotenv file to `process.env`
@@ -44,15 +52,22 @@ async function load() {
// TODO(@forehalo):
// Modules may contribute to ENV_MAP, figure out a good way to involve them instead of hardcoding in `./config/affine.env`
// 3. load env => config map to `globalThis.AFFiNE.ENV_MAP
// load local env map as well in case there are new env added
await loadRemote(PROJECT_CONFIG_PATH, 'affine.env.js');
const projectEnvMap = AFFiNE.ENV_MAP;
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.env.js');
const customEnvMap = AFFiNE.ENV_MAP;
AFFiNE.ENV_MAP = { ...projectEnvMap, ...customEnvMap };
// 4. load `config/affine` to patch custom configs
// load local config as well in case there are new default configurations added
await loadRemote(PROJECT_CONFIG_PATH, 'affine.js');
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.js');
// 5. load `config/affine.self` to patch custom configs
// This is the file only take effect in [AFFiNE Cloud]
if (!AFFiNE.isSelfhosted) {
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.self.js');
await loadRemote(PROJECT_CONFIG_PATH, 'affine.self.js');
}
// 6. apply `process.env` map overriding to `globalThis.AFFiNE`

View File

@@ -235,6 +235,7 @@ enum ErrorNames {
INVALID_OAUTH_CALLBACK_STATE
INVALID_PASSWORD_LENGTH
INVALID_RUNTIME_CONFIG_TYPE
LINK_EXPIRED
MAILER_SERVICE_IS_NOT_CONFIGURED
MEMBER_QUOTA_EXCEEDED
MISSING_OAUTH_QUERY_PARAMETER
@@ -409,7 +410,7 @@ type Mutation {
addWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
cancelSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
changeEmail(email: String!, token: String!): UserType!
changePassword(newPassword: String!, token: String!): UserType!
changePassword(newPassword: String!, token: String!, userId: String): Boolean!
"""Cleanup sessions"""
cleanupCopilotSession(options: DeleteSessionInput!): [String!]!
@@ -619,6 +620,9 @@ type SameSubscriptionRecurringDataType {
}
type ServerConfigType {
"""Features for user that can be configured"""
availableUserFeatures: [FeatureType!]!
"""server base url"""
baseUrl: String!

View File

@@ -132,13 +132,14 @@ test('set and change password', async t => {
);
const newPassword = randomBytes(16).toString('hex');
const userId = await changePassword(
const success = await changePassword(
app,
u1.token.token,
u1.id,
setPasswordToken as string,
newPassword
);
t.is(u1.id, userId, 'failed to set password');
t.true(success, 'failed to change password');
const ret = auth.signIn(u1Email, newPassword);
t.notThrowsAsync(ret, 'failed to check password');
@@ -201,7 +202,7 @@ test('should revoke token after change user identify', async t => {
await sendSetPasswordEmail(app, u3.token.token, u3Email, 'affine.pro');
const token = await getTokenFromLatestMailMessage();
const newPassword = randomBytes(16).toString('hex');
await changePassword(app, u3.token.token, token as string, newPassword);
await changePassword(app, u3.id, token as string, newPassword);
const user = await currentUser(app, u3.token.token);
t.is(user, null, 'token should be revoked');

View File

@@ -129,26 +129,23 @@ export async function sendSetPasswordEmail(
export async function changePassword(
app: INestApplication,
userToken: string,
userId: string,
token: string,
password: string
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation changePassword($token: String!, $password: String!) {
changePassword(token: $token, newPassword: $password) {
id
}
mutation changePassword($token: String!, $userId: String!, $password: String!) {
changePassword(token: $token, userId: $userId, newPassword: $password)
}
`,
variables: { token, password },
variables: { token, password, userId },
})
.expect(200);
return res.body.data.changePassword.id;
return res.body.data.changePassword;
}
export async function sendVerifyChangeEmail(

View File

@@ -3,8 +3,8 @@
"private": true,
"type": "module",
"devDependencies": {
"@blocksuite/global": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/store": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/global": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/store": "0.17.0-canary-202408121434-8bc42f0",
"react": "18.3.1",
"react-dom": "18.3.1",
"vitest": "1.6.0"

View File

@@ -14,10 +14,10 @@
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/templates": "workspace:*",
"@blocksuite/blocks": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/global": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/presets": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/store": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/blocks": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/global": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/presets": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/store": "0.17.0-canary-202408121434-8bc42f0",
"@datastructures-js/binary-search-tree": "^5.3.2",
"foxact": "^0.2.33",
"fuse.js": "^7.0.0",
@@ -34,8 +34,8 @@
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine/templates": "workspace:*",
"@blocksuite/block-std": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/presets": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/block-std": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/presets": "0.17.0-canary-202408121434-8bc42f0",
"@testing-library/react": "^16.0.0",
"async-call-rpc": "^6.4.0",
"fake-indexeddb": "^6.0.0",

View File

@@ -48,6 +48,7 @@
"react-resizable-panels": "^2.0.19",
"react-router-dom": "^6.23.1",
"sonner": "^1.5.0",
"swr": "^2.2.5",
"vaul": "^0.9.1",
"zod": "^3.23.8"
},

View File

@@ -4,12 +4,17 @@ import { wrapCreateBrowserRouter } from '@sentry/react';
import { useEffect } from 'react';
import {
createBrowserRouter as reactRouterCreateBrowserRouter,
Navigate,
Outlet,
RouterProvider,
useLocation,
useNavigate,
} from 'react-router-dom';
import { toast } from 'sonner';
import { SWRConfig } from 'swr';
import { TooltipProvider } from './components/ui/tooltip';
import { isAdmin, useCurrentUser, useServerConfig } from './modules/common';
import { Layout } from './modules/layout';
const createBrowserRouter = wrapCreateBrowserRouter(
reactRouterCreateBrowserRouter
@@ -19,53 +24,76 @@ const _createBrowserRouter = window.SENTRY_RELEASE
? createBrowserRouter
: reactRouterCreateBrowserRouter;
const Redirect = function Redirect() {
const location = useLocation();
const navigate = useNavigate();
function AuthenticatedRoutes() {
const user = useCurrentUser();
useEffect(() => {
if (!location.pathname.startsWith('/admin/accounts')) {
navigate('/admin/accounts', { replace: true });
if (user && !isAdmin(user)) {
toast.error('You are not an admin, please login the admin account.');
}
}, [location, navigate]);
return null;
};
}, [user]);
if (!user || !isAdmin(user)) {
return <Navigate to="/admin/auth" />;
}
return (
<Layout>
<Outlet />
</Layout>
);
}
function RootRoutes() {
const config = useServerConfig();
const location = useLocation();
if (!config.initialized) {
return <Navigate to="/admin/setup" />;
}
if (/^\/admin\/?$/.test(location.pathname)) {
return <Navigate to="/admin/accounts" />;
}
return <Outlet />;
}
export const router = _createBrowserRouter(
[
{
path: '/',
element: <Redirect />,
},
{
path: '/admin',
element: <RootRoutes />,
children: [
{
path: '',
element: <Redirect />,
},
{
path: '/admin/accounts',
lazy: () => import('./modules/accounts'),
},
{
path: '/admin/auth',
lazy: () => import('./modules/auth'),
},
{
path: '/admin/ai',
lazy: () => import('./modules/ai'),
},
{
path: '/admin/setup',
lazy: () => import('./modules/setup'),
},
{
path: '/admin/config',
lazy: () => import('./modules/config'),
},
{
path: '/admin/settings',
lazy: () => import('./modules/settings'),
path: '/admin/*',
element: <AuthenticatedRoutes />,
children: [
{
path: 'accounts',
lazy: () => import('./modules/accounts'),
},
// {
// path: 'ai',
// lazy: () => import('./modules/ai'),
// },
{
path: 'config',
lazy: () => import('./modules/config'),
},
{
path: 'settings',
lazy: () => import('./modules/settings'),
},
],
},
],
},
@@ -81,7 +109,14 @@ export const App = () => {
return (
<TooltipProvider>
<Telemetry />
<RouterProvider router={router} />
<SWRConfig
value={{
revalidateOnFocus: false,
revalidateOnMount: false,
}}
>
<RouterProvider router={router} />
</SWRConfig>
<Toaster />
</TooltipProvider>
);

View File

@@ -4,7 +4,13 @@ import { useQuery } from '@affine/core/hooks/use-query';
import { getUserByEmailQuery } from '@affine/graphql';
import { PlusIcon } from 'lucide-react';
import type { SetStateAction } from 'react';
import { startTransition, useCallback, useEffect, useState } from 'react';
import {
startTransition,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useRightPanel } from '../../layout';
import { DiscardChanges } from './discard-changes';
@@ -15,6 +21,21 @@ interface DataTableToolbarProps<TData> {
setDataTable: (data: TData[]) => void;
}
const useSearch = () => {
const [value, setValue] = useState('');
const { data } = useQuery({
query: getUserByEmailQuery,
variables: { email: value },
});
const result = useMemo(() => data?.userByEmail, [data]);
return {
result,
query: setValue,
};
};
function useDebouncedValue<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
@@ -37,9 +58,10 @@ export function DataTableToolbar<TData>({
}: DataTableToolbarProps<TData>) {
const [value, setValue] = useState('');
const [dialogOpen, setDialogOpen] = useState(false);
const debouncedValue = useDebouncedValue(value, 500);
const debouncedValue = useDebouncedValue(value, 1000);
const { setRightPanelContent, openPanel, closePanel, isOpen } =
useRightPanel();
const { result, query } = useSearch();
const handleConfirm = useCallback(() => {
setRightPanelContent(<CreateUserForm onComplete={closePanel} />);
@@ -51,12 +73,9 @@ export function DataTableToolbar<TData>({
}
}, [setRightPanelContent, closePanel, dialogOpen, isOpen, openPanel]);
const result = useQuery({
query: getUserByEmailQuery,
variables: {
email: value,
},
}).data.userByEmail;
useEffect(() => {
query(debouncedValue);
}, [debouncedValue, query]);
useEffect(() => {
startTransition(() => {
@@ -68,13 +87,11 @@ export function DataTableToolbar<TData>({
setDataTable([]);
}
});
}, [data, debouncedValue, result, setDataTable, value]);
}, [data, debouncedValue, result, setDataTable]);
const onValueChange = useCallback(
(e: { currentTarget: { value: SetStateAction<string> } }) => {
startTransition(() => {
setValue(e.currentTarget.value);
});
setValue(e.currentTarget.value);
},
[]
);

View File

@@ -5,8 +5,6 @@ import {
TableCell,
TableRow,
} from '@affine/admin/components/ui/table';
import { useQuery } from '@affine/core/hooks/use-query';
import { getUsersCountQuery } from '@affine/graphql';
import type { ColumnDef, PaginationState } from '@tanstack/react-table';
import {
flexRender,
@@ -17,6 +15,7 @@ import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
import { DataTablePagination } from './data-table-pagination';
import { DataTableToolbar } from './data-table-toolbar';
import { useUserCount } from './use-user-management';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -36,11 +35,7 @@ export function DataTable<TData, TValue>({
pagination,
onPaginationChange,
}: DataTableProps<TData, TValue>) {
const {
data: { usersCount },
} = useQuery({
query: getUsersCountQuery,
});
const usersCount = useUserCount();
const [tableData, setTableData] = useState(data);
const table = useReactTable({

View File

@@ -3,10 +3,12 @@ import {
useMutateQueryResource,
useMutation,
} from '@affine/core/hooks/use-mutation';
import { useQuery } from '@affine/core/hooks/use-query';
import {
createChangePasswordUrlMutation,
createUserMutation,
deleteUserMutation,
getUsersCountQuery,
listUsersQuery,
updateAccountFeaturesMutation,
updateAccountMutation,
@@ -159,3 +161,12 @@ export const useDeleteUser = () => {
return deleteById;
};
export const useUserCount = () => {
const {
data: { usersCount },
} = useQuery({
query: getUsersCountQuery,
});
return usersCount;
};

View File

@@ -31,9 +31,16 @@ function UserForm({
}: UserFormProps) {
const serverConfig = useServerConfig();
const [changes, setChanges] = useState<Partial<UserInput>>({
features: defaultValue?.features ?? [],
});
const defaultUser: Partial<UserInput> = useMemo(
() => ({
name: defaultValue?.name ?? '',
email: defaultValue?.email ?? '',
features: defaultValue?.features ?? [],
}),
[defaultValue?.email, defaultValue?.features, defaultValue?.name]
);
const [changes, setChanges] = useState<Partial<UserInput>>(defaultUser);
const setField = useCallback(
<K extends keyof UserInput>(
@@ -74,6 +81,15 @@ function UserForm({
[setField]
);
const handleClose = useCallback(() => {
setChanges(defaultUser);
onClose();
}, [defaultUser, onClose]);
useEffect(() => {
setChanges(defaultUser);
}, [defaultUser]);
return (
<div className="flex flex-col h-full gap-1">
<div className=" flex justify-between items-center py-[10px] px-6">
@@ -82,7 +98,7 @@ function UserForm({
size="icon"
className="w-7 h-7"
variant="ghost"
onClick={onClose}
onClick={handleClose}
>
<XIcon size={20} />
</Button>
@@ -104,14 +120,14 @@ function UserForm({
<InputItem
label="Name"
field="name"
value={changes.name ?? defaultValue?.name}
value={changes.name}
onChange={setField}
/>
<Separator />
<InputItem
label="Email"
field="email"
value={changes.email ?? defaultValue?.email}
value={changes.email}
onChange={setField}
/>
</div>
@@ -121,11 +137,7 @@ function UserForm({
<div key={feature}>
<ToggleItem
name={feature}
checked={(
changes.features ??
defaultValue?.features ??
[]
).includes(feature)}
checked={changes.features?.includes(feature) ?? false}
onChange={onFeatureChanged}
/>
{i < serverConfig.availableUserFeatures.length - 1 && (
@@ -188,7 +200,7 @@ function InputItem({
<Input
type="text"
className="py-2 px-3 text-base font-normal"
defaultValue={value}
value={value}
onChange={onValueChange}
/>
</div>

View File

@@ -1,32 +1,11 @@
import { Separator } from '@affine/admin/components/ui/separator';
import { useQuery } from '@affine/core/hooks/use-query';
import { listUsersQuery } from '@affine/graphql';
import { useState } from 'react';
import { Layout } from '../layout';
import { columns } from './components/columns';
import { DataTable } from './components/data-table';
export function Accounts() {
return <Layout content={<AccountPage />} />;
}
import { useUserList } from './use-user-list';
export function AccountPage() {
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const {
data: { users },
} = useQuery({
query: listUsersQuery,
variables: {
filter: {
first: pagination.pageSize,
skip: pagination.pageIndex * pagination.pageSize,
},
},
});
const { users, pagination, setPagination } = useUserList();
return (
<div className=" h-screen flex-1 flex-col flex">
@@ -45,4 +24,4 @@ export function AccountPage() {
</div>
);
}
export { Accounts as Component };
export { AccountPage as Component };

View File

@@ -0,0 +1,27 @@
import { useQuery } from '@affine/core/hooks/use-query';
import { listUsersQuery } from '@affine/graphql';
import { useState } from 'react';
export const useUserList = () => {
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const {
data: { users },
} = useQuery({
query: listUsersQuery,
variables: {
filter: {
first: pagination.pageSize,
skip: pagination.pageIndex * pagination.pageSize,
},
},
});
return {
users,
pagination,
setPagination,
};
};

View File

@@ -8,7 +8,7 @@ export function Ai() {
return null;
// hide ai config in admin until it's ready
// return <Layout content={<AiPage />} />;
// return <AiPage />;
}
export function AiPage() {

View File

@@ -1,90 +1,80 @@
import { Button } from '@affine/admin/components/ui/button';
import { Input } from '@affine/admin/components/ui/input';
import { Label } from '@affine/admin/components/ui/label';
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
import {
FeatureType,
getCurrentUserFeaturesQuery,
getUserFeaturesQuery,
} from '@affine/graphql';
import { useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { FeatureType, getUserFeaturesQuery } from '@affine/graphql';
import type { FormEvent } from 'react';
import { useCallback, useRef } from 'react';
import { Navigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useCurrentUser, useServerConfig } from '../common';
import { isAdmin, useCurrentUser, useRevalidateCurrentUser } from '../common';
import logo from './logo.svg';
export function Auth() {
const currentUser = useCurrentUser();
const serverConfig = useServerConfig();
const revalidate = useMutateQueryResource();
const revalidate = useRevalidateCurrentUser();
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
const login = useCallback(() => {
if (!emailRef.current || !passwordRef.current) return;
fetch('/api/auth/sign-in', {
method: 'POST',
body: JSON.stringify({
email: emailRef.current?.value,
password: passwordRef.current?.value,
}),
headers: {
'Content-Type': 'application/json',
},
})
.then(async response => {
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || 'Failed to login');
}
await revalidate(getCurrentUserFeaturesQuery);
return response.json();
const login = useCallback(
(e: FormEvent) => {
e.preventDefault();
e.stopPropagation();
if (!emailRef.current || !passwordRef.current) return;
fetch('/api/auth/sign-in', {
method: 'POST',
body: JSON.stringify({
email: emailRef.current?.value,
password: passwordRef.current?.value,
}),
headers: {
'Content-Type': 'application/json',
},
})
.then(() =>
fetch('/graphql', {
method: 'POST',
body: JSON.stringify({
operationName: getUserFeaturesQuery.operationName,
query: getUserFeaturesQuery.query,
variables: {},
}),
headers: {
'Content-Type': 'application/json',
},
})
)
.then(res => res.json())
.then(
({
data: {
currentUser: { features },
},
}) => {
if (features.includes(FeatureType.Admin)) {
toast.success('Logged in successfully');
navigate('/admin');
} else {
toast.error('You are not an admin');
.then(async response => {
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || 'Failed to login');
}
}
)
.catch(err => {
toast.error(`Failed to login: ${err.message}`);
});
}, [navigate, revalidate]);
return response.json();
})
.then(() =>
fetch('/graphql', {
method: 'POST',
body: JSON.stringify({
operationName: getUserFeaturesQuery.operationName,
query: getUserFeaturesQuery.query,
variables: {},
}),
headers: {
'Content-Type': 'application/json',
},
})
)
.then(res => res.json())
.then(
({
data: {
currentUser: { features },
},
}) => {
if (features.includes(FeatureType.Admin)) {
toast.success('Logged in successfully');
revalidate();
} else {
toast.error('You are not an admin');
}
}
)
.catch(err => {
toast.error(`Failed to login: ${err.message}`);
});
},
[revalidate]
);
useEffect(() => {
if (serverConfig.initialized === false) {
navigate('/admin/setup');
return;
} else if (!currentUser) {
return;
} else if (!currentUser?.features.includes?.(FeatureType.Admin)) {
toast.error('You are not an admin, please login the admin account.');
return;
}
}, [currentUser, navigate, serverConfig.initialized]);
if (currentUser && isAdmin(currentUser)) {
return <Navigate to="/admin" />;
}
return (
<div className="w-full lg:grid lg:min-h-[600px] lg:grid-cols-2 xl:min-h-[800px] h-screen">
@@ -96,27 +86,36 @@ export function Auth() {
Enter your email below to login to your account
</p>
</div>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
ref={emailRef}
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<form onSubmit={login} action="#">
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
ref={emailRef}
placeholder="m@example.com"
autoComplete="email"
required
/>
</div>
<Input id="password" type="password" ref={passwordRef} required />
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input
id="password"
type="password"
ref={passwordRef}
autoComplete="current-password"
required
/>
</div>
<Button onClick={login} type="submit" className="w-full">
Login
</Button>
</div>
<Button onClick={login} type="submit" className="w-full">
Login
</Button>
</div>
</form>
</div>
</div>
<div className="hidden bg-muted lg:flex lg:justify-center">

View File

@@ -1,6 +1,9 @@
import { useMutateQueryResource } from '@affine/core/hooks/use-mutation';
import { useQueryImmutable } from '@affine/core/hooks/use-query';
import type { GetCurrentUserFeaturesQuery } from '@affine/graphql';
import {
adminServerConfigQuery,
FeatureType,
getCurrentUserFeaturesQuery,
} from '@affine/graphql';
@@ -12,10 +15,22 @@ export const useServerConfig = () => {
return data.serverConfig;
};
export const useRevalidateCurrentUser = () => {
const revalidate = useMutateQueryResource();
return () => {
revalidate(getCurrentUserFeaturesQuery);
};
};
export const useCurrentUser = () => {
const { data } = useQueryImmutable({
query: getCurrentUserFeaturesQuery,
});
return data.currentUser;
};
export function isAdmin(
user: NonNullable<GetCurrentUserFeaturesQuery['currentUser']>
) {
return user.features.includes(FeatureType.Admin);
}

View File

@@ -6,41 +6,14 @@ import {
} from '@affine/admin/components/ui/card';
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
import { Separator } from '@affine/admin/components/ui/separator';
import { useQueryImmutable } from '@affine/core/hooks/use-query';
import { getServerServiceConfigsQuery } from '@affine/graphql';
import { Layout } from '../layout';
import { AboutAFFiNE } from './about';
type ServerConfig = {
externalUrl: string;
https: boolean;
host: string;
port: number;
path: string;
};
type MailerConfig = {
host: string;
port: number;
sender: string;
};
type DatabaseConfig = {
host: string;
port: number;
user: string;
database: string;
};
type ServerServiceConfig = {
name: string;
config: ServerConfig | MailerConfig | DatabaseConfig;
};
export function Config() {
return <Layout content={<ConfigPage />} />;
}
import type {
DatabaseConfig,
MailerConfig,
ServerConfig,
} from './use-server-service-configs';
import { useServerServiceConfigs } from './use-server-service-configs';
export function ConfigPage() {
return (
@@ -171,22 +144,8 @@ const MailerCard = ({ mailerConfig }: { mailerConfig?: MailerConfig }) => {
};
export function ServerServiceConfig() {
const { data } = useQueryImmutable({
query: getServerServiceConfigsQuery,
});
const server = data.serverServiceConfigs.find(
(service: ServerServiceConfig) => service.name === 'server'
);
const mailer = data.serverServiceConfigs.find(
(service: ServerServiceConfig) => service.name === 'mailer'
);
const database = data.serverServiceConfigs.find(
(service: ServerServiceConfig) => service.name === 'database'
);
const serverConfig = server?.config as ServerConfig | undefined;
const mailerConfig = mailer?.config as MailerConfig | undefined;
const databaseConfig = database?.config as DatabaseConfig | undefined;
const { serverConfig, mailerConfig, databaseConfig } =
useServerServiceConfigs();
return (
<div className="flex flex-col py-5 px-6">
@@ -218,4 +177,4 @@ export function ServerServiceConfig() {
);
}
export { Config as Component };
export { ConfigPage as Component };

View File

@@ -0,0 +1,62 @@
import { useQueryImmutable } from '@affine/core/hooks/use-query';
import { getServerServiceConfigsQuery } from '@affine/graphql';
import { useMemo } from 'react';
export type ServerConfig = {
externalUrl: string;
https: boolean;
host: string;
port: number;
path: string;
};
export type MailerConfig = {
host: string;
port: number;
sender: string;
};
export type DatabaseConfig = {
host: string;
port: number;
user: string;
database: string;
};
export type ServerServiceConfig = {
name: string;
config: ServerConfig | MailerConfig | DatabaseConfig;
};
export const useServerServiceConfigs = () => {
const { data } = useQueryImmutable({
query: getServerServiceConfigsQuery,
});
const server = useMemo(
() =>
data.serverServiceConfigs.find(
(service: ServerServiceConfig) => service.name === 'server'
),
[data.serverServiceConfigs]
);
const mailer = useMemo(
() =>
data.serverServiceConfigs.find(
(service: ServerServiceConfig) => service.name === 'mailer'
),
[data.serverServiceConfigs]
);
const database = useMemo(
() =>
data.serverServiceConfigs.find(
(service: ServerServiceConfig) => service.name === 'database'
),
[data.serverServiceConfigs]
);
const serverConfig = server?.config as ServerConfig | undefined;
const mailerConfig = mailer?.config as MailerConfig | undefined;
const databaseConfig = database?.config as DatabaseConfig | undefined;
return { serverConfig, mailerConfig, databaseConfig };
};

View File

@@ -6,10 +6,8 @@ import {
import { Separator } from '@affine/admin/components/ui/separator';
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
import { cn } from '@affine/admin/utils';
import { useQuery } from '@affine/core/hooks/use-query';
import { FeatureType, getCurrentUserFeaturesQuery } from '@affine/graphql';
import { AlignJustifyIcon } from 'lucide-react';
import type { ReactNode, RefObject } from 'react';
import type { PropsWithChildren, ReactNode, RefObject } from 'react';
import {
createContext,
useCallback,
@@ -19,8 +17,6 @@ import {
useState,
} from 'react';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { Button } from '../components/ui/button';
import {
@@ -32,14 +28,9 @@ import {
SheetTrigger,
} from '../components/ui/sheet';
import { Logo } from './accounts/components/logo';
import { useServerConfig } from './common';
import { NavContext } from './nav/context';
import { Nav } from './nav/nav';
interface LayoutProps {
content: ReactNode;
}
interface RightPanelContextType {
isOpen: boolean;
rightPanelContent: ReactNode;
@@ -81,14 +72,7 @@ export function useMediaQuery(query: string) {
return value;
}
export function Layout({ content }: LayoutProps) {
const serverConfig = useServerConfig();
const {
data: { currentUser },
} = useQuery({
query: getCurrentUserFeaturesQuery,
});
export function Layout({ children }: PropsWithChildren) {
const [rightPanelContent, setRightPanelContent] = useState<ReactNode>(null);
const [open, setOpen] = useState(false);
const rightPanelRef = useRef<ImperativePanelHandle>(null);
@@ -126,26 +110,6 @@ export function Layout({ content }: LayoutProps) {
[closePanel, openPanel]
);
const navigate = useNavigate();
useEffect(() => {
if (serverConfig.initialized === false) {
navigate('/admin/setup');
return;
} else if (!currentUser) {
navigate('/admin/auth');
return;
} else if (!currentUser?.features.includes?.(FeatureType.Admin)) {
toast.error('You are not an admin, please login the admin account.');
navigate('/admin/auth');
return;
}
}, [currentUser, navigate, serverConfig.initialized]);
if (serverConfig.initialized === false || !currentUser) {
return null;
}
return (
<RightPanelContext.Provider
value={{
@@ -172,7 +136,7 @@ export function Layout({ content }: LayoutProps) {
<LeftPanel />
<ResizablePanelGroup direction="horizontal">
<ResizablePanel id="0" order={0} minSize={50}>
{content}
{children}
</ResizablePanel>
<RightPanel
rightPanelRef={rightPanelRef}

View File

@@ -12,48 +12,26 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@affine/admin/components/ui/dropdown-menu';
import { FeatureType } from '@affine/graphql';
import { CircleUser, MoreVertical } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCallback } from 'react';
import { toast } from 'sonner';
import { useCurrentUser, useServerConfig } from '../common';
import { useCurrentUser, useRevalidateCurrentUser } from '../common';
export function UserDropdown() {
const currentUser = useCurrentUser();
const serverConfig = useServerConfig();
const navigate = useNavigate();
const relative = useRevalidateCurrentUser();
const handleLogout = useCallback(() => {
fetch('/api/auth/sign-out', {
method: 'POST',
})
fetch('/api/auth/sign-out')
.then(() => {
toast.success('Logged out successfully');
navigate('/admin/auth');
relative();
})
.catch(err => {
toast.error(`Failed to logout: ${err.message}`);
});
}, [navigate]);
useEffect(() => {
if (serverConfig.initialized === false) {
navigate('/admin/setup');
return;
}
if (!currentUser) {
navigate('/admin/auth');
return;
}
if (!currentUser?.features.includes?.(FeatureType.Admin)) {
toast.error('You are not an admin, please login the admin account.');
navigate('/admin/auth');
return;
}
}, [currentUser, navigate, serverConfig.initialized]);
}, [relative]);
return (
<div className="flex flex-none items-center justify-between px-4 py-3 flex-nowrap">

View File

@@ -6,7 +6,6 @@ import { CheckIcon } from 'lucide-react';
import type { Dispatch, SetStateAction } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Layout } from '../layout';
import { useNav } from '../nav/context';
import { ConfirmChanges } from './confirm-changes';
import { RuntimeSettingRow } from './runtime-setting-row';
@@ -25,10 +24,6 @@ export type ModifiedValues = {
newValue: any;
};
export function Settings() {
return <Layout content={<SettingsPage />} />;
}
export function SettingsPage() {
const { trigger } = useUpdateServerRuntimeConfigs();
const { serverRuntimeConfig } = useGetServerRuntimeConfig();
@@ -190,4 +185,4 @@ export const AdminPanel = ({
);
};
export { Settings as Component };
export { SettingsPage as Component };

View File

@@ -1,7 +1,16 @@
import { Navigate } from 'react-router-dom';
import { useServerConfig } from '../common';
import { Form } from './form';
import logo from './logo.svg';
export function Setup() {
const config = useServerConfig();
if (config.initialized) {
return <Navigate to="/admin" />;
}
return (
<div className="w-full lg:grid lg:grid-cols-2 h-screen">
<div className="flex items-center justify-center py-12 h-full">

View File

@@ -78,12 +78,12 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@blocksuite/block-std": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/blocks": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/global": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/block-std": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/blocks": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/global": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/icons": "2.1.62",
"@blocksuite/presets": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/store": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/presets": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/store": "0.17.0-canary-202408121434-8bc42f0",
"@storybook/addon-actions": "^7.6.17",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-interactions": "^7.6.17",

View File

@@ -7,19 +7,12 @@ import { Button } from '../../ui/button';
import { notify } from '../../ui/notification';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
import type { User } from './type';
export const ChangePasswordPage: FC<{
user: User;
passwordLimits: PasswordLimitsFragment;
onSetPassword: (password: string) => Promise<void>;
onOpenAffine: () => void;
}> = ({
user: { email },
passwordLimits,
onSetPassword: propsOnSetPassword,
onOpenAffine,
}) => {
}> = ({ passwordLimits, onSetPassword: propsOnSetPassword, onOpenAffine }) => {
const t = useI18n();
const [hasSetUp, setHasSetUp] = useState(false);
@@ -45,17 +38,12 @@ export const ChangePasswordPage: FC<{
: t['com.affine.auth.reset.password.page.title']()
}
subtitle={
hasSetUp ? (
t['com.affine.auth.sent.reset.password.success.message']()
) : (
<>
{t['com.affine.auth.page.sent.email.subtitle']({
hasSetUp
? t['com.affine.auth.sent.reset.password.success.message']()
: t['com.affine.auth.page.sent.email.subtitle']({
min: String(passwordLimits.minLength),
max: String(passwordLimits.maxLength),
})}
<a href={`mailto:${email}`}>{email}</a>
</>
)
})
}
>
{hasSetUp ? (

View File

@@ -7,19 +7,12 @@ import { Button } from '../../ui/button';
import { notify } from '../../ui/notification';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
import type { User } from './type';
export const SetPasswordPage: FC<{
user: User;
passwordLimits: PasswordLimitsFragment;
onSetPassword: (password: string) => Promise<void>;
onOpenAffine: () => void;
}> = ({
user: { email },
passwordLimits,
onSetPassword: propsOnSetPassword,
onOpenAffine,
}) => {
}> = ({ passwordLimits, onSetPassword: propsOnSetPassword, onOpenAffine }) => {
const t = useI18n();
const [hasSetUp, setHasSetUp] = useState(false);
@@ -45,17 +38,12 @@ export const SetPasswordPage: FC<{
: t['com.affine.auth.set.password.page.title']()
}
subtitle={
hasSetUp ? (
t['com.affine.auth.sent.set.password.success.message']()
) : (
<>
{t['com.affine.auth.page.sent.email.subtitle']({
hasSetUp
? t['com.affine.auth.sent.set.password.success.message']()
: t['com.affine.auth.page.sent.email.subtitle']({
min: String(passwordLimits.minLength),
max: String(passwordLimits.maxLength),
})}
<a href={`mailto:${email}`}>{email}</a>
</>
)
})
}
>
{hasSetUp ? (

View File

@@ -19,13 +19,13 @@
"@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@blocksuite/block-std": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/blocks": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/global": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/block-std": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/blocks": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/global": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/icons": "2.1.62",
"@blocksuite/inline": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/presets": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/store": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/inline": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/presets": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/store": "0.17.0-canary-202408121434-8bc42f0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",

View File

@@ -19,7 +19,11 @@ export const CollectionListHeader = ({
<div className={styles.collectionListHeaderTitle}>
{t['com.affine.collections.header']()}
</div>
<Button className={styles.newCollectionButton} onClick={onCreate}>
<Button
className={styles.newCollectionButton}
onClick={onCreate}
data-testid="all-collection-new-button"
>
{t['com.affine.collections.empty.new-collection-button']()}
</Button>
</div>

View File

@@ -481,6 +481,7 @@ export const CollectionOperationCell = ({
</MenuIcon>
}
type="danger"
data-testid="delete-collection"
>
{t['Delete']()}
</MenuItem>
@@ -490,7 +491,7 @@ export const CollectionOperationCell = ({
align: 'end',
}}
>
<IconButton>
<IconButton data-testid="collection-item-operation-button">
<MoreVerticalIcon />
</IconButton>
</Menu>
@@ -560,6 +561,7 @@ export const TagOperationCell = ({
}
type="danger"
onSelect={handleDelete}
data-testid="delete-tag"
>
{t['Delete']()}
</MenuItem>
@@ -568,7 +570,7 @@ export const TagOperationCell = ({
align: 'end',
}}
>
<IconButton>
<IconButton data-testid="tag-item-operation-button">
<MoreVerticalIcon />
</IconButton>
</Menu>

View File

@@ -134,7 +134,11 @@ export const CreateOrEditTag = ({
}
return (
<div className={styles.createTagWrapper} data-show={open}>
<div
className={styles.createTagWrapper}
data-show={open}
data-testid="edit-tag-modal"
>
<Menu
rootOptions={{
open: menuOpen,
@@ -154,11 +158,17 @@ export const CreateOrEditTag = ({
value={tagName}
onChange={handleChangeName}
autoFocus
data-testid="edit-tag-input"
/>
<Button className={styles.cancelBtn} onClick={onClose}>
{t['Cancel']()}
</Button>
<Button variant="primary" onClick={onConfirm} disabled={!tagName}>
<Button
variant="primary"
onClick={onConfirm}
disabled={!tagName}
data-testid="save-tag"
>
{tagMeta ? t['Save']() : t['Create']()}
</Button>
</div>

View File

@@ -8,7 +8,11 @@ export const TagListHeader = ({ onOpen }: { onOpen: () => void }) => {
return (
<div className={styles.tagListHeader}>
<div className={styles.tagListHeaderTitle}>{t['Tags']()}</div>
<Button className={styles.newTagButton} onClick={onOpen}>
<Button
className={styles.newTagButton}
onClick={onOpen}
data-testid="all-tags-new-button"
>
{t['com.affine.tags.empty.new-tag-button']()}
</Button>
</div>

View File

@@ -88,7 +88,7 @@ export const CreateCollection = ({
[isNameEmpty]
);
return (
<div>
<div data-testid="edit-collection-modal">
<div className={styles.content}>
<div className={styles.label}>
{t['com.affine.editCollectionName.name']()}

View File

@@ -63,16 +63,14 @@ export const useJournalHelper = (docCollection: DocCollection) => {
const getJournalsByDate = useCallback(
(maybeDate: MaybeDate) => {
const day = dayjs(maybeDate);
return Array.from(docCollection.docs.values())
.map(blockCollection => blockCollection.getDoc())
.filter(page => {
const pageId = page.id;
if (!isPageJournal(pageId)) return false;
if (page.meta?.trash) return false;
const journalDate = adapter.getJournalPageDateString(page.id);
if (!journalDate) return false;
return day.isSame(journalDate, 'day');
});
return Array.from(docCollection.docs.values()).filter(page => {
const pageId = page.id;
if (!isPageJournal(pageId)) return false;
if (page.meta?.trash) return false;
const journalDate = adapter.getJournalPageDateString(page.id);
if (!journalDate) return false;
return day.isSame(journalDate, 'day');
});
},
[adapter, isPageJournal, docCollection.docs]
);
@@ -83,7 +81,7 @@ export const useJournalHelper = (docCollection: DocCollection) => {
const getJournalByDate = useCallback(
(maybeDate: MaybeDate) => {
const pages = getJournalsByDate(maybeDate);
if (pages.length) return pages[0];
if (pages.length) return pages[0].getDoc();
return _createJournal(maybeDate);
},
[_createJournal, getJournalsByDate]
@@ -140,9 +138,9 @@ export const useJournalHelper = (docCollection: DocCollection) => {
appendContentToToday,
}),
[
getJournalsByDate,
getJournalByDate,
getJournalDateString,
getJournalsByDate,
getLocalizedJournalDateString,
isPageJournal,
isPageTodayJournal,

View File

@@ -208,6 +208,7 @@ const WorkbenchTab = ({
data-testid="workbench-tab"
data-active={tabActive}
data-pinned={workbench.pinned}
data-padding-right={tabsLength > 1 && !workbench.pinned}
className={styles.tab}
>
{workbench.views.map((view, viewIdx) => {

View File

@@ -75,6 +75,7 @@ export const tabWrapper = style({
export const tab = style({
height: 32,
minWidth: 32,
maxWidth: 200,
overflow: 'clip',
background: cssVar('backgroundSecondaryColor'),
display: 'flex',
@@ -93,7 +94,7 @@ export const tab = style({
background: cssVar('backgroundPrimaryColor'),
boxShadow: cssVar('shadow1'),
},
'&[data-pinned="false"]': {
'&[data-padding-right="true"]': {
paddingRight: 20,
},
'&[data-pinned="true"]': {
@@ -113,7 +114,6 @@ export const splitViewLabel = style({
gap: '4px',
fontWeight: 500,
alignItems: 'center',
maxWidth: 180,
cursor: 'default',
':last-of-type': {
paddingRight: 0,
@@ -172,7 +172,7 @@ export const tabCloseButtonWrapper = style({
top: 0,
bottom: 0,
height: '100%',
width: 16,
width: 24,
overflow: 'clip',
display: 'flex',
alignItems: 'center',

View File

@@ -3,8 +3,8 @@ import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useI18n } from '@affine/i18n';
import {
CloseIcon,
DualLinkIcon,
ExpandFullIcon,
OpenInNewIcon,
SplitViewIcon,
} from '@blocksuite/icons/rc';
import { type DocMode, useService } from '@toeverything/infra';
@@ -124,6 +124,15 @@ export const DocPeekViewControls = ({
peekView.close('none');
},
},
{
icon: <OpenInNewIcon />,
nameKey: 'new-tab',
name: t['com.affine.peek-view-controls.open-doc-in-new-tab'](),
onClick: () => {
workbench.openDoc(docId, { at: 'new-tab' });
peekView.close('none');
},
},
environment.isDesktop && {
icon: <SplitViewIcon />,
nameKey: 'split-view',
@@ -133,18 +142,6 @@ export const DocPeekViewControls = ({
peekView.close('none');
},
},
!environment.isDesktop && {
icon: <DualLinkIcon />,
nameKey: 'new-tab',
name: t['com.affine.peek-view-controls.open-doc-in-new-tab'](),
onClick: () => {
window.open(
`/workspace/${workspace.id}/${docId}#${blockId ?? ''}`,
'_blank'
);
peekView.close('none');
},
},
].filter((opt): opt is ControlButtonProps => Boolean(opt));
}, [
blockId,

View File

@@ -17,8 +17,7 @@ import {
} from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import type { ReactElement } from 'react';
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect, useParams, useSearchParams } from 'react-router-dom';
import { z } from 'zod';
@@ -39,7 +38,7 @@ const authTypeSchema = z.enum([
'verify-email',
]);
export const AuthPage = (): ReactElement | null => {
export const Component = () => {
const authService = useService(AuthService);
const account = useLiveData(authService.session.account$);
const t = useI18n();
@@ -89,6 +88,7 @@ export const AuthPage = (): ReactElement | null => {
async (password: string) => {
await changePassword({
token: searchParams.get('token') || '',
userId: searchParams.get('userId') || '',
newPassword: password,
});
},
@@ -98,22 +98,26 @@ export const AuthPage = (): ReactElement | null => {
jumpToIndex(RouteLogic.REPLACE);
}, [jumpToIndex]);
if (!passwordLimits || !account) {
if (!passwordLimits) {
// TODO(@eyhn): loading UI
return null;
}
switch (authType) {
case 'onboarding':
return <OnboardingPage user={account} onOpenAffine={onOpenAffine} />;
return (
account && <OnboardingPage user={account} onOpenAffine={onOpenAffine} />
);
case 'signUp': {
return (
<SignUpPage
user={account}
passwordLimits={passwordLimits}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
/>
account && (
<SignUpPage
user={account}
passwordLimits={passwordLimits}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
/>
)
);
}
case 'signIn': {
@@ -122,7 +126,6 @@ export const AuthPage = (): ReactElement | null => {
case 'changePassword': {
return (
<ChangePasswordPage
user={account}
passwordLimits={passwordLimits}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
@@ -132,7 +135,6 @@ export const AuthPage = (): ReactElement | null => {
case 'setPassword': {
return (
<SetPasswordPage
user={account}
passwordLimits={passwordLimits}
onSetPassword={onSetPassword}
onOpenAffine={onOpenAffine}
@@ -198,25 +200,3 @@ export const loader: LoaderFunction = async args => {
}
return null;
};
export const Component = () => {
const authService = useService(AuthService);
const isRevalidating = useLiveData(authService.session.isRevalidating$);
const loginStatus = useLiveData(authService.session.status$);
const { jumpToExpired } = useNavigateHelper();
useEffect(() => {
authService.session.revalidate();
}, [authService]);
if (loginStatus === 'unauthenticated' && !isRevalidating) {
jumpToExpired(RouteLogic.REPLACE);
}
if (loginStatus === 'authenticated') {
return <AuthPage />;
}
// TODO(@eyhn): loading UI
return null;
};

View File

@@ -29,10 +29,10 @@
"@affine/env": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/block-std": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/blocks": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/presets": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/store": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/block-std": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/blocks": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/presets": "0.17.0-canary-202408121434-8bc42f0",
"@blocksuite/store": "0.17.0-canary-202408121434-8bc42f0",
"@electron-forge/cli": "^7.3.0",
"@electron-forge/core": "^7.3.0",
"@electron-forge/core-utils": "^7.3.0",

View File

@@ -139,22 +139,19 @@ export function createApplicationMenu() {
undoCloseTab().catch(console.error);
},
},
{
label: 'Switch to tab',
acceleratorWorksWhenHidden: true,
visible: false,
submenu: [1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => {
const shortcut = `CommandOrControl+${n}`;
const listener = () => {
switchTab(n);
};
return {
label: `Switch to tab ${n}`,
accelerator: shortcut,
click: listener,
};
}),
},
...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => {
const shortcut = `CommandOrControl+${n}`;
const listener = () => {
switchTab(n);
};
return {
acceleratorWorksWhenHidden: true,
label: `Switch to tab ${n}`,
accelerator: shortcut,
click: listener,
visible: false,
};
}),
],
},
{

View File

@@ -1,5 +1,7 @@
mutation changePassword($token: String!, $newPassword: String!) {
changePassword(token: $token, newPassword: $newPassword) {
id
}
mutation changePassword(
$token: String!
$userId: String!
$newPassword: String!
) {
changePassword(token: $token, userId: $userId, newPassword: $newPassword)
}

View File

@@ -121,10 +121,8 @@ export const changePasswordMutation = {
definitionName: 'changePassword',
containsFile: false,
query: `
mutation changePassword($token: String!, $newPassword: String!) {
changePassword(token: $token, newPassword: $newPassword) {
id
}
mutation changePassword($token: String!, $userId: String!, $newPassword: String!) {
changePassword(token: $token, userId: $userId, newPassword: $newPassword)
}`,
};

View File

@@ -306,6 +306,7 @@ export enum ErrorNames {
INVALID_OAUTH_CALLBACK_STATE = 'INVALID_OAUTH_CALLBACK_STATE',
INVALID_PASSWORD_LENGTH = 'INVALID_PASSWORD_LENGTH',
INVALID_RUNTIME_CONFIG_TYPE = 'INVALID_RUNTIME_CONFIG_TYPE',
LINK_EXPIRED = 'LINK_EXPIRED',
MAILER_SERVICE_IS_NOT_CONFIGURED = 'MAILER_SERVICE_IS_NOT_CONFIGURED',
MEMBER_QUOTA_EXCEEDED = 'MEMBER_QUOTA_EXCEEDED',
MISSING_OAUTH_QUERY_PARAMETER = 'MISSING_OAUTH_QUERY_PARAMETER',
@@ -467,7 +468,7 @@ export interface Mutation {
addWorkspaceFeature: Scalars['Int']['output'];
cancelSubscription: UserSubscription;
changeEmail: UserType;
changePassword: UserType;
changePassword: Scalars['Boolean']['output'];
/** Cleanup sessions */
cleanupCopilotSession: Array<Scalars['String']['output']>;
/** Create change password url */
@@ -557,6 +558,7 @@ export interface MutationChangeEmailArgs {
export interface MutationChangePasswordArgs {
newPassword: Scalars['String']['input'];
token: Scalars['String']['input'];
userId: InputMaybe<Scalars['String']['input']>;
}
export interface MutationCleanupCopilotSessionArgs {
@@ -1321,12 +1323,13 @@ export type CreateChangePasswordUrlMutation = {
export type ChangePasswordMutationVariables = Exact<{
token: Scalars['String']['input'];
userId: Scalars['String']['input'];
newPassword: Scalars['String']['input'];
}>;
export type ChangePasswordMutation = {
__typename?: 'Mutation';
changePassword: { __typename?: 'UserType'; id: string };
changePassword: boolean;
};
export type CopilotQuotaQueryVariables = Exact<{ [key: string]: never }>;

View File

@@ -154,7 +154,9 @@ async function enableSplitView(page: Page) {
})
);
});
await page.reload();
await page.reload({
timeout: 30000,
});
}
test('open new tab via cmd+click page link', async ({ page }) => {

View File

@@ -404,3 +404,68 @@ test('select three pages with shiftKey and delete', async ({ page }) => {
expect(await getPagesCount(page)).toBe(pageCount - 3);
});
test('create a collection and delete it', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
await page.getByTestId('workspace-collections-button').click();
// create a collection
await page.getByTestId('all-collection-new-button').click();
await expect(page.getByTestId('edit-collection-modal')).toBeVisible();
await page.getByTestId('input-collection-title').fill('test collection');
await page.getByTestId('save-collection').click();
// check the collection is created
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
await page.getByTestId('workspace-collections-button').click();
const cell = page
.getByTestId('collection-list-item')
.getByText('test collection');
await expect(cell).toBeVisible();
// delete the collection
await page.getByTestId('collection-item-operation-button').click();
await page.getByTestId('delete-collection').click();
await page.waitForURL(url => url.pathname.endsWith('collection'));
const newCell = page
.getByTestId('collection-list-item')
.getByText('test collection');
await expect(newCell).not.toBeVisible();
});
test('create a tag and delete it', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
await page.getByTestId('workspace-tags-button').click();
// create a tag
await page.getByTestId('all-tags-new-button').click();
await expect(page.getByTestId('edit-tag-modal')).toBeVisible();
await page.getByTestId('edit-tag-input').fill('test-tag');
await page.getByTestId('save-tag').click();
// check the tag is created
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
await page.getByTestId('workspace-tags-button').click();
const cell = page.getByTestId('tag-list-item').getByText('test-tag');
await expect(cell).toBeVisible();
// delete the tag
await page.getByTestId('tag-item-operation-button').click();
await page.getByTestId('delete-tag').click();
await page.getByTestId('confirm-modal-confirm').getByText('Delete').click();
await page.waitForURL(url => url.pathname.endsWith('tag'));
const newCell = page.getByTestId('tag-list-item').getByText('test-tag');
await expect(newCell).not.toBeVisible();
});

View File

@@ -6,7 +6,7 @@
"@affine/env": "workspace:*",
"@affine/templates": "workspace:*",
"@aws-sdk/client-s3": "^3.620.0",
"@blocksuite/presets": "0.17.0-canary-202408121434-ff85a54",
"@blocksuite/presets": "0.17.0-canary-202408121434-8bc42f0",
"@clack/core": "^0.3.4",
"@clack/prompts": "^0.7.0",
"@magic-works/i18n-codegen": "^0.6.0",

211
yarn.lock
View File

@@ -202,6 +202,7 @@ __metadata:
react-router-dom: "npm:^6.23.1"
shadcn-ui: "npm:^0.8.0"
sonner: "npm:^1.5.0"
swr: "npm:^2.2.5"
tailwind-merge: "npm:^2.3.0"
tailwindcss: "npm:^3.4.4"
tailwindcss-animate: "npm:^1.0.7"
@@ -227,7 +228,7 @@ __metadata:
"@affine/env": "workspace:*"
"@affine/templates": "workspace:*"
"@aws-sdk/client-s3": "npm:^3.620.0"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-8bc42f0"
"@clack/core": "npm:^0.3.4"
"@clack/prompts": "npm:^0.7.0"
"@magic-works/i18n-codegen": "npm:^0.6.0"
@@ -285,12 +286,12 @@ __metadata:
"@affine/i18n": "workspace:*"
"@atlaskit/pragmatic-drag-and-drop": "npm:^1.2.1"
"@atlaskit/pragmatic-drag-and-drop-hitbox": "npm:^1.0.3"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/icons": "npm:2.1.62"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-8bc42f0"
"@dnd-kit/core": "npm:^6.1.0"
"@dnd-kit/modifiers": "npm:^7.0.0"
"@dnd-kit/sortable": "npm:^8.0.0"
@@ -386,13 +387,13 @@ __metadata:
"@affine/graphql": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/templates": "workspace:*"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/icons": "npm:2.1.62"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-8bc42f0"
"@dnd-kit/core": "npm:^6.1.0"
"@dnd-kit/modifiers": "npm:^7.0.0"
"@dnd-kit/sortable": "npm:^8.0.0"
@@ -522,10 +523,10 @@ __metadata:
"@affine/env": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/native": "workspace:*"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-8bc42f0"
"@electron-forge/cli": "npm:^7.3.0"
"@electron-forge/core": "npm:^7.3.0"
"@electron-forge/core-utils": "npm:^7.3.0"
@@ -581,8 +582,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@affine/env@workspace:packages/common/env"
dependencies:
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-8bc42f0"
lit: "npm:^3.1.2"
react: "npm:18.3.1"
react-dom: "npm:18.3.1"
@@ -678,7 +679,7 @@ __metadata:
nanoid: "npm:^5.0.7"
nx: "npm:^19.0.0"
nyc: "npm:^17.0.0"
oxlint: "npm:0.7.0"
oxlint: "npm:0.7.1"
prettier: "npm:^3.2.5"
semver: "npm:^7.6.0"
serve: "npm:^14.2.1"
@@ -3451,11 +3452,11 @@ __metadata:
languageName: node
linkType: hard
"@blocksuite/block-std@npm:0.17.0-canary-202408121434-ff85a54":
version: 0.17.0-canary-202408121434-ff85a54
resolution: "@blocksuite/block-std@npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/block-std@npm:0.17.0-canary-202408121434-8bc42f0":
version: 0.17.0-canary-202408121434-8bc42f0
resolution: "@blocksuite/block-std@npm:0.17.0-canary-202408121434-8bc42f0"
dependencies:
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@lit-labs/preact-signals": "npm:^1.0.2"
"@lit/context": "npm:^1.1.2"
"@types/hast": "npm:^3.0.4"
@@ -3467,21 +3468,21 @@ __metadata:
w3c-keyname: "npm:^2.2.8"
zod: "npm:^3.23.8"
peerDependencies:
"@blocksuite/inline": 0.17.0-canary-202408121434-ff85a54
"@blocksuite/store": 0.17.0-canary-202408121434-ff85a54
checksum: 10/d412fc3aef51f2e3855db4c57c20375ad8928f5009a8f0470e2795efd8b109cc8ea8ff1156434f735f8028b5af61bdff4a400dc1900daac455736529b9773f0f
"@blocksuite/inline": 0.17.0-canary-202408121434-8bc42f0
"@blocksuite/store": 0.17.0-canary-202408121434-8bc42f0
checksum: 10/b949cf99584f56821541be126aa7bdafd73cb5b292b3d0d7ab1ca04ca72ea5dca953ceb880d9e4c24de6d96f4cb82f945c114d95a385e0de02b9820e33d3df58
languageName: node
linkType: hard
"@blocksuite/blocks@npm:0.17.0-canary-202408121434-ff85a54":
version: 0.17.0-canary-202408121434-ff85a54
resolution: "@blocksuite/blocks@npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/blocks@npm:0.17.0-canary-202408121434-8bc42f0":
version: 0.17.0-canary-202408121434-8bc42f0
resolution: "@blocksuite/blocks@npm:0.17.0-canary-202408121434-8bc42f0"
dependencies:
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/icons": "npm:^2.1.62"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-8bc42f0"
"@dotlottie/player-component": "npm:^2.7.12"
"@floating-ui/dom": "npm:^1.6.8"
"@lit-labs/preact-signals": "npm:^1.0.2"
@@ -3520,17 +3521,17 @@ __metadata:
sortablejs: "npm:^1.15.2"
unified: "npm:^11.0.5"
zod: "npm:^3.23.8"
checksum: 10/4dfcc2277476a83e328ee123d2c8de0a311ac9711e15074c3a720749a9972ea5f87a4a7cdc42441845be5ccf96c326fe2b27c64ef2ddb8c1fd628948ea5ca151
checksum: 10/26d9c8fa3f1b4e36c8d6eb5f91ad50372e8c91fe23b7a4b1e03bf33f4545cf6b02dc70199c27f64db3e764b24f6002a592ae88ad8a36d3ee97e0d442cab37ffa
languageName: node
linkType: hard
"@blocksuite/global@npm:0.17.0-canary-202408121434-ff85a54":
version: 0.17.0-canary-202408121434-ff85a54
resolution: "@blocksuite/global@npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global@npm:0.17.0-canary-202408121434-8bc42f0":
version: 0.17.0-canary-202408121434-8bc42f0
resolution: "@blocksuite/global@npm:0.17.0-canary-202408121434-8bc42f0"
dependencies:
lib0: "npm:^0.2.95"
zod: "npm:^3.23.8"
checksum: 10/eb9525c7843fbfc6681ebb097075008354295b56f0a8c1a2cdaf092691b1e32b1b4dd1f66016f82302e5fb3425c83cea1873a9a7f6173bd3af71e10515267efd
checksum: 10/986fe67f8b67ea5dabc7120db529c1662b6ad04cffeb614da551a8caa36085019c415cf38cddd3cbf58a235be0c3780d325f2876a299b592280d3abb6de01b0c
languageName: node
linkType: hard
@@ -3550,28 +3551,28 @@ __metadata:
languageName: node
linkType: hard
"@blocksuite/inline@npm:0.17.0-canary-202408121434-ff85a54":
version: 0.17.0-canary-202408121434-ff85a54
resolution: "@blocksuite/inline@npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/inline@npm:0.17.0-canary-202408121434-8bc42f0":
version: 0.17.0-canary-202408121434-8bc42f0
resolution: "@blocksuite/inline@npm:0.17.0-canary-202408121434-8bc42f0"
dependencies:
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
zod: "npm:^3.23.8"
peerDependencies:
lit: ^3.1.1
yjs: ^13.6.15
checksum: 10/9892bd35b63bc384ce09fe14a0deeb434e420f019dda52db963f1a77fb1e1ee6b19395da10159cac69d317a3ce0337ac4358373b0390377416e18557183e5da6
checksum: 10/e0a03c6378b636e3fcf189cf6ca48811d59a3fe08d08f07eb4cac1fa14505cefe13f6321777f4d6189d9b3e63216904ad468670b833f142d3bea4427287bca13
languageName: node
linkType: hard
"@blocksuite/presets@npm:0.17.0-canary-202408121434-ff85a54":
version: 0.17.0-canary-202408121434-ff85a54
resolution: "@blocksuite/presets@npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/presets@npm:0.17.0-canary-202408121434-8bc42f0":
version: 0.17.0-canary-202408121434-8bc42f0
resolution: "@blocksuite/presets@npm:0.17.0-canary-202408121434-8bc42f0"
dependencies:
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-8bc42f0"
"@dotlottie/player-component": "npm:^2.7.12"
"@fal-ai/serverless-client": "npm:^0.13.0"
"@floating-ui/dom": "npm:^1.6.8"
@@ -3580,17 +3581,17 @@ __metadata:
lit: "npm:^3.1.4"
openai: "npm:^4.53.2"
zod: "npm:^3.23.8"
checksum: 10/de52608d00c931a6e0430f9cccb6431623fbfa4816744efe02e0b67084c1e0ae995b70dafaa54f1aa5a352e93b0f45400100ef6849cc65ff86ddb16d194bb483
checksum: 10/a9d09bb08896d188078f2fb741094c41cb2b6684054fed6702153bf7ecd9c89edc267f04c855c4514b2866f720f282f0d30af471b86707078927ef3f2dc6de64
languageName: node
linkType: hard
"@blocksuite/store@npm:0.17.0-canary-202408121434-ff85a54":
version: 0.17.0-canary-202408121434-ff85a54
resolution: "@blocksuite/store@npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store@npm:0.17.0-canary-202408121434-8bc42f0":
version: 0.17.0-canary-202408121434-8bc42f0
resolution: "@blocksuite/store@npm:0.17.0-canary-202408121434-8bc42f0"
dependencies:
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/sync": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/inline": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/sync": "npm:0.17.0-canary-202408121434-8bc42f0"
"@preact/signals-core": "npm:^1.7.0"
"@types/flexsearch": "npm:^0.7.6"
"@types/lodash.ismatch": "npm:^4.4.9"
@@ -3604,21 +3605,21 @@ __metadata:
zod: "npm:^3.23.8"
peerDependencies:
yjs: ^13.6.15
checksum: 10/70a92df99185898b2d52459dd134584c5c4505ae369d6988e7c52e901b32136a747ad4a6b970c67b837a0f7fa40fff2d5cb9d95e80ce32c2a68ed7b929fee723
checksum: 10/ce63ef449eed81c68d8add17925fe353fa1ad39f36f3144044465bde93b4b8879881e81802c721e607922b77810c18648d6a61247afab47831e8112f1268551e
languageName: node
linkType: hard
"@blocksuite/sync@npm:0.17.0-canary-202408121434-ff85a54":
version: 0.17.0-canary-202408121434-ff85a54
resolution: "@blocksuite/sync@npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/sync@npm:0.17.0-canary-202408121434-8bc42f0":
version: 0.17.0-canary-202408121434-8bc42f0
resolution: "@blocksuite/sync@npm:0.17.0-canary-202408121434-8bc42f0"
dependencies:
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
idb: "npm:^8.0.0"
idb-keyval: "npm:^6.2.1"
y-protocols: "npm:^1.0.6"
peerDependencies:
yjs: ^13.6.15
checksum: 10/a5a7a777725edf5463db8bdc1a50c9985080175bf231b56699953cbe388b25dc4c396119a813e6d8d8d32cc0718c8df4a480997881be1c88b610f5f059edeb5b
checksum: 10/a0537af4d086489a36927685299c902a8d0e4ef13b405e96257571e55bb5a9605110ae40ca51fc787802c32e0505fac76ed8c2e6a838b9c64239acf02dfc8272
languageName: node
linkType: hard
@@ -10094,58 +10095,58 @@ __metadata:
languageName: node
linkType: hard
"@oxlint/darwin-arm64@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/darwin-arm64@npm:0.7.0"
"@oxlint/darwin-arm64@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/darwin-arm64@npm:0.7.1"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@oxlint/darwin-x64@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/darwin-x64@npm:0.7.0"
"@oxlint/darwin-x64@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/darwin-x64@npm:0.7.1"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@oxlint/linux-arm64-gnu@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/linux-arm64-gnu@npm:0.7.0"
"@oxlint/linux-arm64-gnu@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/linux-arm64-gnu@npm:0.7.1"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@oxlint/linux-arm64-musl@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/linux-arm64-musl@npm:0.7.0"
"@oxlint/linux-arm64-musl@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/linux-arm64-musl@npm:0.7.1"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@oxlint/linux-x64-gnu@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/linux-x64-gnu@npm:0.7.0"
"@oxlint/linux-x64-gnu@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/linux-x64-gnu@npm:0.7.1"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@oxlint/linux-x64-musl@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/linux-x64-musl@npm:0.7.0"
"@oxlint/linux-x64-musl@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/linux-x64-musl@npm:0.7.1"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@oxlint/win32-arm64@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/win32-arm64@npm:0.7.0"
"@oxlint/win32-arm64@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/win32-arm64@npm:0.7.1"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@oxlint/win32-x64@npm:0.7.0":
version: 0.7.0
resolution: "@oxlint/win32-x64@npm:0.7.0"
"@oxlint/win32-x64@npm:0.7.1":
version: 0.7.1
resolution: "@oxlint/win32-x64@npm:0.7.1"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@@ -14921,11 +14922,11 @@ __metadata:
"@affine/debug": "workspace:*"
"@affine/env": "workspace:*"
"@affine/templates": "workspace:*"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-ff85a54"
"@blocksuite/block-std": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/blocks": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/global": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/presets": "npm:0.17.0-canary-202408121434-8bc42f0"
"@blocksuite/store": "npm:0.17.0-canary-202408121434-8bc42f0"
"@datastructures-js/binary-search-tree": "npm:^5.3.2"
"@testing-library/react": "npm:^16.0.0"
async-call-rpc: "npm:^6.4.0"
@@ -31304,18 +31305,18 @@ __metadata:
languageName: node
linkType: hard
"oxlint@npm:0.7.0":
version: 0.7.0
resolution: "oxlint@npm:0.7.0"
"oxlint@npm:0.7.1":
version: 0.7.1
resolution: "oxlint@npm:0.7.1"
dependencies:
"@oxlint/darwin-arm64": "npm:0.7.0"
"@oxlint/darwin-x64": "npm:0.7.0"
"@oxlint/linux-arm64-gnu": "npm:0.7.0"
"@oxlint/linux-arm64-musl": "npm:0.7.0"
"@oxlint/linux-x64-gnu": "npm:0.7.0"
"@oxlint/linux-x64-musl": "npm:0.7.0"
"@oxlint/win32-arm64": "npm:0.7.0"
"@oxlint/win32-x64": "npm:0.7.0"
"@oxlint/darwin-arm64": "npm:0.7.1"
"@oxlint/darwin-x64": "npm:0.7.1"
"@oxlint/linux-arm64-gnu": "npm:0.7.1"
"@oxlint/linux-arm64-musl": "npm:0.7.1"
"@oxlint/linux-x64-gnu": "npm:0.7.1"
"@oxlint/linux-x64-musl": "npm:0.7.1"
"@oxlint/win32-arm64": "npm:0.7.1"
"@oxlint/win32-x64": "npm:0.7.1"
dependenciesMeta:
"@oxlint/darwin-arm64":
optional: true
@@ -31335,7 +31336,7 @@ __metadata:
optional: true
bin:
oxlint: bin/oxlint
checksum: 10/4842c09698b862fad290b75b271e0a5d8f8a6a44ed15d71c47e277fae3aec6dc4d6645b685a44fcf479e4afc37ce5dd2cd7a0b50173e297646ecf1b507ea5480
checksum: 10/7003e3d8730d84224e301322bc084dc61ccb91c2ca40f84add696a85713bb2faaa223b975b5b3f9c5e89966d277954d8e7c4f1f49428d7c7821ba1b38a1b2152
languageName: node
linkType: hard