mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e70c91e8 | ||
|
|
05ac3dbdcb | ||
|
|
994b539507 | ||
|
|
d5edadabe6 | ||
|
|
05247bb24e | ||
|
|
f69f026ac3 | ||
|
|
015247345c | ||
|
|
0ba516866f | ||
|
|
7afba6b8b5 | ||
|
|
08cc15a55c | ||
|
|
24c34eb3fc | ||
|
|
ba5ba71f35 | ||
|
|
d4065fee78 | ||
|
|
d86f7f41dc |
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
4
packages/common/env/package.json
vendored
4
packages/common/env/package.json
vendored
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']()}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}`,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
211
yarn.lock
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user