feat(core): desktop multiple server support (#8979)
This commit is contained in:
@@ -35,6 +35,7 @@ import { TokenService, TokenType } from './token';
|
|||||||
interface PreflightResponse {
|
interface PreflightResponse {
|
||||||
registered: boolean;
|
registered: boolean;
|
||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
|
magicLink: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SignInCredential {
|
interface SignInCredential {
|
||||||
@@ -82,16 +83,20 @@ export class AuthController {
|
|||||||
params.email
|
params.email
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const magicLinkAvailable = !!this.config.mailer.host;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
registered: false,
|
registered: false,
|
||||||
hasPassword: false,
|
hasPassword: false,
|
||||||
|
magicLink: magicLinkAvailable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
registered: user.registered,
|
registered: user.registered,
|
||||||
hasPassword: !!user.password,
|
hasPassword: !!user.password,
|
||||||
|
magicLink: magicLinkAvailable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { createApp } from './app';
|
|||||||
import { URLHelper } from './fundamentals';
|
import { URLHelper } from './fundamentals';
|
||||||
|
|
||||||
const app = await createApp();
|
const app = await createApp();
|
||||||
const listeningHost = AFFiNE.deploy ? '0.0.0.0' : 'localhost';
|
const listeningHost = '0.0.0.0';
|
||||||
await app.listen(AFFiNE.server.port, listeningHost);
|
await app.listen(AFFiNE.server.port, listeningHost);
|
||||||
const url = app.get(URLHelper);
|
const url = app.get(URLHelper);
|
||||||
|
|
||||||
|
|||||||
7
packages/common/env/src/workspace.ts
vendored
7
packages/common/env/src/workspace.ts
vendored
@@ -1,7 +0,0 @@
|
|||||||
export enum WorkspaceFlavour {
|
|
||||||
/**
|
|
||||||
* New AFFiNE Cloud Workspace using Nest.js Server.
|
|
||||||
*/
|
|
||||||
AFFINE_CLOUD = 'affine-cloud',
|
|
||||||
LOCAL = 'local',
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@ export class EventBus {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const handler of handlers.values()) {
|
for (const handler of handlers.values()) {
|
||||||
this.on(handler.event.id, handler.handler);
|
this.on(handler.event, handler.handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,23 +41,25 @@ export class EventBus {
|
|||||||
return this.parent?.root ?? this;
|
return this.parent?.root ?? this;
|
||||||
}
|
}
|
||||||
|
|
||||||
on<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
|
on<T>(event: FrameworkEvent<T>, listener: (event: T) => void) {
|
||||||
if (!this.listeners[id]) {
|
if (!this.listeners[event.id]) {
|
||||||
this.listeners[id] = [];
|
this.listeners[event.id] = [];
|
||||||
}
|
}
|
||||||
this.listeners[id].push(listener);
|
this.listeners[event.id].push(listener);
|
||||||
const off = this.parent?.on(id, listener);
|
const off = this.parent?.on(event, listener);
|
||||||
return () => {
|
return () => {
|
||||||
this.off(id, listener);
|
this.off(event, listener);
|
||||||
off?.();
|
off?.();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
off<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
|
off<T>(event: FrameworkEvent<T>, listener: (event: T) => void) {
|
||||||
if (!this.listeners[id]) {
|
if (!this.listeners[event.id]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.listeners[id] = this.listeners[id].filter(l => l !== listener);
|
this.listeners[event.id] = this.listeners[event.id].filter(
|
||||||
|
l => l !== listener
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit<T>(event: FrameworkEvent<T>, payload: T) {
|
emit<T>(event: FrameworkEvent<T>, payload: T) {
|
||||||
|
|||||||
@@ -216,6 +216,15 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: false,
|
configurable: false,
|
||||||
defaultState: isMobile,
|
defaultState: isMobile,
|
||||||
},
|
},
|
||||||
|
enable_multiple_cloud_servers: {
|
||||||
|
category: 'affine',
|
||||||
|
displayName:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.name',
|
||||||
|
description:
|
||||||
|
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description',
|
||||||
|
configurable: isDesktopEnvironment,
|
||||||
|
defaultState: false,
|
||||||
|
},
|
||||||
} satisfies { [key in string]: FlagInfo };
|
} satisfies { [key in string]: FlagInfo };
|
||||||
|
|
||||||
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;
|
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export class GlobalContext extends Entity {
|
|||||||
memento = new MemoryMemento();
|
memento = new MemoryMemento();
|
||||||
|
|
||||||
workspaceId = this.define<string>('workspaceId');
|
workspaceId = this.define<string>('workspaceId');
|
||||||
|
workspaceFlavour = this.define<string>('workspaceFlavour');
|
||||||
|
|
||||||
|
serverId = this.define<string>('serverId');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is in doc page
|
* is in doc page
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import { Framework } from '../../../framework';
|
import { Framework } from '../../../framework';
|
||||||
@@ -22,7 +21,7 @@ describe('Workspace System', () => {
|
|||||||
expect(workspaceService.list.workspaces$.value.length).toBe(0);
|
expect(workspaceService.list.workspaces$.value.length).toBe(0);
|
||||||
|
|
||||||
const workspace = workspaceService.open({
|
const workspace = workspaceService.open({
|
||||||
metadata: await workspaceService.create(WorkspaceFlavour.LOCAL),
|
metadata: await workspaceService.create('local'),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(workspace.workspace).toBeInstanceOf(Workspace);
|
expect(workspace.workspace).toBeInstanceOf(Workspace);
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
|
import { combineLatest, map, of, switchMap } from 'rxjs';
|
||||||
|
|
||||||
import { Entity } from '../../../framework';
|
import { Entity } from '../../../framework';
|
||||||
import { LiveData } from '../../../livedata';
|
import { LiveData } from '../../../livedata';
|
||||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
import type { WorkspaceMetadata } from '../metadata';
|
||||||
|
import type { WorkspaceFlavoursService } from '../services/flavours';
|
||||||
|
|
||||||
export class WorkspaceList extends Entity {
|
export class WorkspaceList extends Entity {
|
||||||
workspaces$ = new LiveData(this.providers.map(p => p.workspaces$))
|
workspaces$ = LiveData.from<WorkspaceMetadata[]>(
|
||||||
.map(v => {
|
this.flavoursService.flavours$.pipe(
|
||||||
return v;
|
switchMap(flavours =>
|
||||||
})
|
combineLatest(flavours.map(flavour => flavour.workspaces$)).pipe(
|
||||||
.flat()
|
map(workspaces => workspaces.flat())
|
||||||
.map(workspaces => {
|
|
||||||
return workspaces.flat();
|
|
||||||
});
|
|
||||||
isRevalidating$ = new LiveData(
|
|
||||||
this.providers.map(p => p.isRevalidating$ ?? new LiveData(false))
|
|
||||||
)
|
)
|
||||||
.flat()
|
)
|
||||||
.map(isLoadings => isLoadings.some(isLoading => isLoading));
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
isRevalidating$ = LiveData.from<boolean>(
|
||||||
|
this.flavoursService.flavours$.pipe(
|
||||||
|
switchMap(flavours =>
|
||||||
|
combineLatest(
|
||||||
|
flavours.map(flavour => flavour.isRevalidating$ ?? of(false))
|
||||||
|
).pipe(map(isLoadings => isLoadings.some(isLoading => isLoading)))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
workspace$(id: string) {
|
workspace$(id: string) {
|
||||||
return this.workspaces$.map(workspaces =>
|
return this.workspaces$.map(workspaces =>
|
||||||
@@ -23,12 +34,14 @@ export class WorkspaceList extends Entity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
|
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidate() {
|
revalidate() {
|
||||||
this.providers.forEach(provider => provider.revalidate?.());
|
this.flavoursService.flavours$.value.forEach(provider => {
|
||||||
|
provider.revalidate?.();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRevalidation(signal?: AbortSignal) {
|
waitForRevalidation(signal?: AbortSignal) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '../../../livedata';
|
} from '../../../livedata';
|
||||||
import type { WorkspaceMetadata } from '../metadata';
|
import type { WorkspaceMetadata } from '../metadata';
|
||||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
||||||
|
import type { WorkspaceFlavoursService } from '../services/flavours';
|
||||||
import type { WorkspaceProfileCacheStore } from '../stores/profile-cache';
|
import type { WorkspaceProfileCacheStore } from '../stores/profile-cache';
|
||||||
import type { Workspace } from './workspace';
|
import type { Workspace } from './workspace';
|
||||||
|
|
||||||
@@ -47,12 +48,14 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly cache: WorkspaceProfileCacheStore,
|
private readonly cache: WorkspaceProfileCacheStore,
|
||||||
providers: WorkspaceFlavourProvider[]
|
flavoursService: WorkspaceFlavoursService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.provider =
|
this.provider =
|
||||||
providers.find(p => p.flavour === this.props.metadata.flavour) ?? null;
|
flavoursService.flavours$.value.find(
|
||||||
|
p => p.flavour === this.props.metadata.flavour
|
||||||
|
) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setProfile(info: WorkspaceProfileInfo) {
|
private setProfile(info: WorkspaceProfileInfo) {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export { getAFFiNEWorkspaceSchema } from './global-schema';
|
|||||||
export type { WorkspaceMetadata } from './metadata';
|
export type { WorkspaceMetadata } from './metadata';
|
||||||
export type { WorkspaceOpenOptions } from './open-options';
|
export type { WorkspaceOpenOptions } from './open-options';
|
||||||
export type { WorkspaceEngineProvider } from './providers/flavour';
|
export type { WorkspaceEngineProvider } from './providers/flavour';
|
||||||
export { WorkspaceFlavourProvider } from './providers/flavour';
|
export type { WorkspaceFlavourProvider } from './providers/flavour';
|
||||||
|
export { WorkspaceFlavoursProvider } from './providers/flavour';
|
||||||
export { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
|
export { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
|
||||||
export { WorkspaceScope } from './scopes/workspace';
|
export { WorkspaceScope } from './scopes/workspace';
|
||||||
export { WorkspaceService } from './services/workspace';
|
export { WorkspaceService } from './services/workspace';
|
||||||
@@ -21,12 +22,13 @@ import {
|
|||||||
WorkspaceLocalCacheImpl,
|
WorkspaceLocalCacheImpl,
|
||||||
WorkspaceLocalStateImpl,
|
WorkspaceLocalStateImpl,
|
||||||
} from './impls/storage';
|
} from './impls/storage';
|
||||||
import { WorkspaceFlavourProvider } from './providers/flavour';
|
import { WorkspaceFlavoursProvider } from './providers/flavour';
|
||||||
import { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
|
import { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
|
||||||
import { WorkspaceScope } from './scopes/workspace';
|
import { WorkspaceScope } from './scopes/workspace';
|
||||||
import { WorkspaceDestroyService } from './services/destroy';
|
import { WorkspaceDestroyService } from './services/destroy';
|
||||||
import { WorkspaceEngineService } from './services/engine';
|
import { WorkspaceEngineService } from './services/engine';
|
||||||
import { WorkspaceFactoryService } from './services/factory';
|
import { WorkspaceFactoryService } from './services/factory';
|
||||||
|
import { WorkspaceFlavoursService } from './services/flavours';
|
||||||
import { WorkspaceListService } from './services/list';
|
import { WorkspaceListService } from './services/list';
|
||||||
import { WorkspaceProfileService } from './services/profile';
|
import { WorkspaceProfileService } from './services/profile';
|
||||||
import { WorkspaceRepositoryService } from './services/repo';
|
import { WorkspaceRepositoryService } from './services/repo';
|
||||||
@@ -34,12 +36,12 @@ import { WorkspaceTransformService } from './services/transform';
|
|||||||
import { WorkspaceService } from './services/workspace';
|
import { WorkspaceService } from './services/workspace';
|
||||||
import { WorkspacesService } from './services/workspaces';
|
import { WorkspacesService } from './services/workspaces';
|
||||||
import { WorkspaceProfileCacheStore } from './stores/profile-cache';
|
import { WorkspaceProfileCacheStore } from './stores/profile-cache';
|
||||||
import { TestingWorkspaceLocalProvider } from './testing/testing-provider';
|
import { TestingWorkspaceFlavoursProvider } from './testing/testing-provider';
|
||||||
|
|
||||||
export function configureWorkspaceModule(framework: Framework) {
|
export function configureWorkspaceModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.service(WorkspacesService, [
|
.service(WorkspacesService, [
|
||||||
[WorkspaceFlavourProvider],
|
WorkspaceFlavoursService,
|
||||||
WorkspaceListService,
|
WorkspaceListService,
|
||||||
WorkspaceProfileService,
|
WorkspaceProfileService,
|
||||||
WorkspaceTransformService,
|
WorkspaceTransformService,
|
||||||
@@ -47,22 +49,23 @@ export function configureWorkspaceModule(framework: Framework) {
|
|||||||
WorkspaceFactoryService,
|
WorkspaceFactoryService,
|
||||||
WorkspaceDestroyService,
|
WorkspaceDestroyService,
|
||||||
])
|
])
|
||||||
.service(WorkspaceDestroyService, [[WorkspaceFlavourProvider]])
|
.service(WorkspaceFlavoursService, [[WorkspaceFlavoursProvider]])
|
||||||
|
.service(WorkspaceDestroyService, [WorkspaceFlavoursService])
|
||||||
.service(WorkspaceListService)
|
.service(WorkspaceListService)
|
||||||
.entity(WorkspaceList, [[WorkspaceFlavourProvider]])
|
.entity(WorkspaceList, [WorkspaceFlavoursService])
|
||||||
.service(WorkspaceProfileService)
|
.service(WorkspaceProfileService)
|
||||||
.store(WorkspaceProfileCacheStore, [GlobalCache])
|
.store(WorkspaceProfileCacheStore, [GlobalCache])
|
||||||
.entity(WorkspaceProfile, [
|
.entity(WorkspaceProfile, [
|
||||||
WorkspaceProfileCacheStore,
|
WorkspaceProfileCacheStore,
|
||||||
[WorkspaceFlavourProvider],
|
WorkspaceFlavoursService,
|
||||||
])
|
])
|
||||||
.service(WorkspaceFactoryService, [[WorkspaceFlavourProvider]])
|
.service(WorkspaceFactoryService, [WorkspaceFlavoursService])
|
||||||
.service(WorkspaceTransformService, [
|
.service(WorkspaceTransformService, [
|
||||||
WorkspaceFactoryService,
|
WorkspaceFactoryService,
|
||||||
WorkspaceDestroyService,
|
WorkspaceDestroyService,
|
||||||
])
|
])
|
||||||
.service(WorkspaceRepositoryService, [
|
.service(WorkspaceRepositoryService, [
|
||||||
[WorkspaceFlavourProvider],
|
WorkspaceFlavoursService,
|
||||||
WorkspaceProfileService,
|
WorkspaceProfileService,
|
||||||
])
|
])
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
@@ -82,8 +85,8 @@ export function configureWorkspaceModule(framework: Framework) {
|
|||||||
|
|
||||||
export function configureTestingWorkspaceProvider(framework: Framework) {
|
export function configureTestingWorkspaceProvider(framework: Framework) {
|
||||||
framework.impl(
|
framework.impl(
|
||||||
WorkspaceFlavourProvider('LOCAL'),
|
WorkspaceFlavoursProvider('LOCAL'),
|
||||||
TestingWorkspaceLocalProvider,
|
TestingWorkspaceFlavoursProvider,
|
||||||
[GlobalState]
|
[GlobalState]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
|
|
||||||
export type WorkspaceMetadata = {
|
export type WorkspaceMetadata = {
|
||||||
id: string;
|
id: string;
|
||||||
flavour: WorkspaceFlavour;
|
flavour: string;
|
||||||
initialized?: boolean;
|
initialized?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import type { DocCollection } from '@blocksuite/affine/store';
|
import type { DocCollection } from '@blocksuite/affine/store';
|
||||||
|
|
||||||
import { createIdentifier } from '../../../framework';
|
import { createIdentifier } from '../../../framework';
|
||||||
@@ -22,7 +21,7 @@ export interface WorkspaceEngineProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceFlavourProvider {
|
export interface WorkspaceFlavourProvider {
|
||||||
flavour: WorkspaceFlavour;
|
flavour: string;
|
||||||
|
|
||||||
deleteWorkspace(id: string): Promise<void>;
|
deleteWorkspace(id: string): Promise<void>;
|
||||||
|
|
||||||
@@ -60,5 +59,9 @@ export interface WorkspaceFlavourProvider {
|
|||||||
onWorkspaceInitialized?(workspace: Workspace): void;
|
onWorkspaceInitialized?(workspace: Workspace): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceFlavourProvider =
|
export interface WorkspaceFlavoursProvider {
|
||||||
createIdentifier<WorkspaceFlavourProvider>('WorkspaceFlavourProvider');
|
workspaceFlavours$: LiveData<WorkspaceFlavourProvider[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkspaceFlavoursProvider =
|
||||||
|
createIdentifier<WorkspaceFlavoursProvider>('WorkspaceFlavoursProvider');
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Service } from '../../../framework';
|
import { Service } from '../../../framework';
|
||||||
import type { WorkspaceMetadata } from '../metadata';
|
import type { WorkspaceMetadata } from '../metadata';
|
||||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
import type { WorkspaceFlavoursService } from './flavours';
|
||||||
|
|
||||||
export class WorkspaceDestroyService extends Service {
|
export class WorkspaceDestroyService extends Service {
|
||||||
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
|
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteWorkspace = async (metadata: WorkspaceMetadata) => {
|
deleteWorkspace = async (metadata: WorkspaceMetadata) => {
|
||||||
const provider = this.providers.find(p => p.flavour === metadata.flavour);
|
const provider = this.flavoursService.flavours$.value.find(
|
||||||
|
p => p.flavour === metadata.flavour
|
||||||
|
);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
|
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import type { DocCollection } from '@blocksuite/affine/store';
|
import type { DocCollection } from '@blocksuite/affine/store';
|
||||||
|
|
||||||
import { Service } from '../../../framework';
|
import { Service } from '../../../framework';
|
||||||
import type { BlobStorage, DocStorage } from '../../../sync';
|
import type { BlobStorage, DocStorage } from '../../../sync';
|
||||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
import type { WorkspaceFlavoursService } from './flavours';
|
||||||
|
|
||||||
export class WorkspaceFactoryService extends Service {
|
export class WorkspaceFactoryService extends Service {
|
||||||
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
|
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,14 +16,16 @@ export class WorkspaceFactoryService extends Service {
|
|||||||
* @returns workspace id
|
* @returns workspace id
|
||||||
*/
|
*/
|
||||||
create = async (
|
create = async (
|
||||||
flavour: WorkspaceFlavour,
|
flavour: string,
|
||||||
initial: (
|
initial: (
|
||||||
docCollection: DocCollection,
|
docCollection: DocCollection,
|
||||||
blobStorage: BlobStorage,
|
blobStorage: BlobStorage,
|
||||||
docStorage: DocStorage
|
docStorage: DocStorage
|
||||||
) => Promise<void> = () => Promise.resolve()
|
) => Promise<void> = () => Promise.resolve()
|
||||||
) => {
|
) => {
|
||||||
const provider = this.providers.find(x => x.flavour === flavour);
|
const provider = this.flavoursService.flavours$.value.find(
|
||||||
|
x => x.flavour === flavour
|
||||||
|
);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error(`Unknown workspace flavour: ${flavour}`);
|
throw new Error(`Unknown workspace flavour: ${flavour}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { combineLatest, map } from 'rxjs';
|
||||||
|
|
||||||
|
import { Service } from '../../../framework';
|
||||||
|
import { LiveData } from '../../../livedata';
|
||||||
|
import type { WorkspaceFlavoursProvider } from '../providers/flavour';
|
||||||
|
|
||||||
|
export class WorkspaceFlavoursService extends Service {
|
||||||
|
constructor(private readonly providers: WorkspaceFlavoursProvider[]) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
flavours$ = LiveData.from(
|
||||||
|
combineLatest(this.providers.map(p => p.workspaceFlavours$)).pipe(
|
||||||
|
map(flavours => flavours.flat())
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,11 +5,9 @@ import { ObjectPool } from '../../../utils';
|
|||||||
import type { Workspace } from '../entities/workspace';
|
import type { Workspace } from '../entities/workspace';
|
||||||
import { WorkspaceInitialized } from '../events';
|
import { WorkspaceInitialized } from '../events';
|
||||||
import type { WorkspaceOpenOptions } from '../open-options';
|
import type { WorkspaceOpenOptions } from '../open-options';
|
||||||
import type {
|
import type { WorkspaceEngineProvider } from '../providers/flavour';
|
||||||
WorkspaceEngineProvider,
|
|
||||||
WorkspaceFlavourProvider,
|
|
||||||
} from '../providers/flavour';
|
|
||||||
import { WorkspaceScope } from '../scopes/workspace';
|
import { WorkspaceScope } from '../scopes/workspace';
|
||||||
|
import type { WorkspaceFlavoursService } from './flavours';
|
||||||
import type { WorkspaceProfileService } from './profile';
|
import type { WorkspaceProfileService } from './profile';
|
||||||
import { WorkspaceService } from './workspace';
|
import { WorkspaceService } from './workspace';
|
||||||
|
|
||||||
@@ -17,7 +15,7 @@ const logger = new DebugLogger('affine:workspace-repository');
|
|||||||
|
|
||||||
export class WorkspaceRepositoryService extends Service {
|
export class WorkspaceRepositoryService extends Service {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly providers: WorkspaceFlavourProvider[],
|
private readonly flavoursService: WorkspaceFlavoursService,
|
||||||
private readonly profileRepo: WorkspaceProfileService
|
private readonly profileRepo: WorkspaceProfileService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -83,7 +81,7 @@ export class WorkspaceRepositoryService extends Service {
|
|||||||
logger.info(
|
logger.info(
|
||||||
`open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} `
|
`open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} `
|
||||||
);
|
);
|
||||||
const flavourProvider = this.providers.find(
|
const flavourProvider = this.flavoursService.flavours$.value.find(
|
||||||
p => p.flavour === openOptions.metadata.flavour
|
p => p.flavour === openOptions.metadata.flavour
|
||||||
);
|
);
|
||||||
const provider =
|
const provider =
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { assertEquals } from '@blocksuite/affine/global/utils';
|
import { assertEquals } from '@blocksuite/affine/global/utils';
|
||||||
import { applyUpdate } from 'yjs';
|
import { applyUpdate } from 'yjs';
|
||||||
|
|
||||||
@@ -26,12 +25,12 @@ export class WorkspaceTransformService extends Service {
|
|||||||
local: Workspace,
|
local: Workspace,
|
||||||
accountId: string
|
accountId: string
|
||||||
): Promise<WorkspaceMetadata> => {
|
): Promise<WorkspaceMetadata> => {
|
||||||
assertEquals(local.flavour, WorkspaceFlavour.LOCAL);
|
assertEquals(local.flavour, 'local');
|
||||||
|
|
||||||
const localDocStorage = local.engine.doc.storage.behavior;
|
const localDocStorage = local.engine.doc.storage.behavior;
|
||||||
|
|
||||||
const newMetadata = await this.factory.create(
|
const newMetadata = await this.factory.create(
|
||||||
WorkspaceFlavour.AFFINE_CLOUD,
|
'affine-cloud',
|
||||||
async (docCollection, blobStorage, docStorage) => {
|
async (docCollection, blobStorage, docStorage) => {
|
||||||
const rootDocBinary = await localDocStorage.doc.get(
|
const rootDocBinary = await localDocStorage.doc.get(
|
||||||
local.docCollection.doc.guid
|
local.docCollection.doc.guid
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Service } from '../../../framework';
|
import { Service } from '../../../framework';
|
||||||
import type { WorkspaceMetadata } from '..';
|
import type { WorkspaceMetadata } from '..';
|
||||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
|
||||||
import type { WorkspaceDestroyService } from './destroy';
|
import type { WorkspaceDestroyService } from './destroy';
|
||||||
import type { WorkspaceFactoryService } from './factory';
|
import type { WorkspaceFactoryService } from './factory';
|
||||||
|
import type { WorkspaceFlavoursService } from './flavours';
|
||||||
import type { WorkspaceListService } from './list';
|
import type { WorkspaceListService } from './list';
|
||||||
import type { WorkspaceProfileService } from './profile';
|
import type { WorkspaceProfileService } from './profile';
|
||||||
import type { WorkspaceRepositoryService } from './repo';
|
import type { WorkspaceRepositoryService } from './repo';
|
||||||
@@ -14,7 +14,7 @@ export class WorkspacesService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly providers: WorkspaceFlavourProvider[],
|
private readonly flavoursService: WorkspaceFlavoursService,
|
||||||
private readonly listService: WorkspaceListService,
|
private readonly listService: WorkspaceListService,
|
||||||
private readonly profileRepo: WorkspaceProfileService,
|
private readonly profileRepo: WorkspaceProfileService,
|
||||||
private readonly transform: WorkspaceTransformService,
|
private readonly transform: WorkspaceTransformService,
|
||||||
@@ -46,7 +46,7 @@ export class WorkspacesService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaceBlob(meta: WorkspaceMetadata, blob: string) {
|
async getWorkspaceBlob(meta: WorkspaceMetadata, blob: string) {
|
||||||
return await this.providers
|
return await this.flavoursService.flavours$.value
|
||||||
.find(x => x.flavour === meta.flavour)
|
.find(x => x.flavour === meta.flavour)
|
||||||
?.getWorkspaceBlob(meta.id, blob);
|
?.getWorkspaceBlob(meta.id, blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { DocCollection, nanoid } from '@blocksuite/affine/store';
|
import { DocCollection, nanoid } from '@blocksuite/affine/store';
|
||||||
import { map } from 'rxjs';
|
import { map } from 'rxjs';
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
@@ -19,21 +18,17 @@ import type { WorkspaceMetadata } from '../metadata';
|
|||||||
import type {
|
import type {
|
||||||
WorkspaceEngineProvider,
|
WorkspaceEngineProvider,
|
||||||
WorkspaceFlavourProvider,
|
WorkspaceFlavourProvider,
|
||||||
|
WorkspaceFlavoursProvider,
|
||||||
} from '../providers/flavour';
|
} from '../providers/flavour';
|
||||||
|
|
||||||
export class TestingWorkspaceLocalProvider
|
class TestingWorkspaceLocalProvider implements WorkspaceFlavourProvider {
|
||||||
extends Service
|
flavour = 'local';
|
||||||
implements WorkspaceFlavourProvider
|
|
||||||
{
|
|
||||||
flavour: WorkspaceFlavour = WorkspaceFlavour.LOCAL;
|
|
||||||
|
|
||||||
store = wrapMemento(this.globalStore, 'testing/');
|
store = wrapMemento(this.globalStore, 'testing/');
|
||||||
workspaceListStore = wrapMemento(this.store, 'workspaces/');
|
workspaceListStore = wrapMemento(this.store, 'workspaces/');
|
||||||
docStorage = new MemoryDocStorage(wrapMemento(this.store, 'docs/'));
|
docStorage = new MemoryDocStorage(wrapMemento(this.store, 'docs/'));
|
||||||
|
|
||||||
constructor(private readonly globalStore: GlobalState) {
|
constructor(private readonly globalStore: GlobalState) {}
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteWorkspace(id: string): Promise<void> {
|
async deleteWorkspace(id: string): Promise<void> {
|
||||||
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
|
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
|
||||||
@@ -48,7 +43,7 @@ export class TestingWorkspaceLocalProvider
|
|||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
): Promise<WorkspaceMetadata> {
|
): Promise<WorkspaceMetadata> {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const meta = { id, flavour: WorkspaceFlavour.LOCAL };
|
const meta = { id, flavour: 'local' };
|
||||||
|
|
||||||
const blobStorage = new MemoryBlobStorage(
|
const blobStorage = new MemoryBlobStorage(
|
||||||
wrapMemento(this.store, id + '/blobs/')
|
wrapMemento(this.store, id + '/blobs/')
|
||||||
@@ -75,7 +70,7 @@ export class TestingWorkspaceLocalProvider
|
|||||||
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
|
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
|
||||||
this.workspaceListStore.set('list', [...list, meta]);
|
this.workspaceListStore.set('list', [...list, meta]);
|
||||||
|
|
||||||
return { id, flavour: WorkspaceFlavour.LOCAL };
|
return { id, flavour: 'local' };
|
||||||
}
|
}
|
||||||
workspaces$ = LiveData.from<WorkspaceMetadata[]>(
|
workspaces$ = LiveData.from<WorkspaceMetadata[]>(
|
||||||
this.workspaceListStore
|
this.workspaceListStore
|
||||||
@@ -132,3 +127,15 @@ export class TestingWorkspaceLocalProvider
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TestingWorkspaceFlavoursProvider
|
||||||
|
extends Service
|
||||||
|
implements WorkspaceFlavoursProvider
|
||||||
|
{
|
||||||
|
constructor(private readonly globalStore: GlobalState) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
workspaceFlavours$ = new LiveData<WorkspaceFlavourProvider[]>([
|
||||||
|
new TestingWorkspaceLocalProvider(this.globalStore),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"@sentry/esbuild-plugin": "^2.16.1",
|
"@sentry/esbuild-plugin": "^2.16.1",
|
||||||
"@sentry/react": "^8.0.0",
|
"@sentry/react": "^8.0.0",
|
||||||
"@toeverything/infra": "workspace:*",
|
"@toeverything/infra": "workspace:*",
|
||||||
|
"@types/set-cookie-parser": "^2.4.10",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||||
"builder-util-runtime": "^9.2.5-alpha.2",
|
"builder-util-runtime": "^9.2.5-alpha.2",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"electron-updater": "^6.2.1",
|
"electron-updater": "^6.2.1",
|
||||||
"link-preview-js": "^3.0.5",
|
"link-preview-js": "^3.0.5",
|
||||||
"next-themes": "^0.4.0",
|
"next-themes": "^0.4.0",
|
||||||
|
"set-cookie-parser": "^2.7.1",
|
||||||
"yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch"
|
"yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
@@ -22,13 +22,3 @@ export const buildType = ReleaseTypeSchema.parse(envBuildType);
|
|||||||
|
|
||||||
export const mode = process.env.NODE_ENV;
|
export const mode = process.env.NODE_ENV;
|
||||||
export const isDev = mode === 'development';
|
export const isDev = mode === 'development';
|
||||||
|
|
||||||
const API_URL_MAPPING = {
|
|
||||||
stable: `https://app.affine.pro`,
|
|
||||||
beta: `https://insider.affine.pro`,
|
|
||||||
canary: `https://affine.fail`,
|
|
||||||
internal: `https://insider.affine.pro`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CLOUD_BASE_URL =
|
|
||||||
process.env.DEV_SERVER_URL || API_URL_MAPPING[buildType];
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ async function handleAffineUrl(url: string) {
|
|||||||
if (urlObj.hostname === 'authentication') {
|
if (urlObj.hostname === 'authentication') {
|
||||||
const method = urlObj.searchParams.get('method');
|
const method = urlObj.searchParams.get('method');
|
||||||
const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false');
|
const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false');
|
||||||
|
const server = urlObj.searchParams.get('server') || undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!method ||
|
!method ||
|
||||||
@@ -97,6 +98,7 @@ async function handleAffineUrl(url: string) {
|
|||||||
uiSubjects.authenticationRequest$.next({
|
uiSubjects.authenticationRequest$.next({
|
||||||
method,
|
method,
|
||||||
payload,
|
payload,
|
||||||
|
server,
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
urlObj.searchParams.get('new-tab') &&
|
urlObj.searchParams.get('new-tab') &&
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { net, protocol, session } from 'electron';
|
import { net, protocol, session } from 'electron';
|
||||||
|
import cookieParser from 'set-cookie-parser';
|
||||||
|
|
||||||
import { CLOUD_BASE_URL } from './config';
|
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { isOfflineModeEnabled } from './utils';
|
import { isOfflineModeEnabled } from './utils';
|
||||||
import { getCookies } from './windows-manager';
|
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
{
|
{
|
||||||
@@ -46,13 +45,6 @@ async function handleFileRequest(request: Request) {
|
|||||||
bypassCustomProtocolHandlers: true,
|
bypassCustomProtocolHandlers: true,
|
||||||
});
|
});
|
||||||
const urlObject = new URL(request.url);
|
const urlObject = new URL(request.url);
|
||||||
if (isNetworkResource(urlObject.pathname)) {
|
|
||||||
// just pass through (proxy)
|
|
||||||
return net.fetch(
|
|
||||||
CLOUD_BASE_URL + urlObject.pathname + urlObject.search,
|
|
||||||
clonedRequest
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// this will be file types (in the web-static folder)
|
// this will be file types (in the web-static folder)
|
||||||
let filepath = '';
|
let filepath = '';
|
||||||
// if is a file type, load the file in resources
|
// if is a file type, load the file in resources
|
||||||
@@ -71,7 +63,6 @@ async function handleFileRequest(request: Request) {
|
|||||||
filepath = join(webStaticDir, 'index.html');
|
filepath = join(webStaticDir, 'index.html');
|
||||||
}
|
}
|
||||||
return net.fetch('file://' + filepath, clonedRequest);
|
return net.fetch('file://' + filepath, clonedRequest);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerProtocol() {
|
export function registerProtocol() {
|
||||||
@@ -83,31 +74,58 @@ export function registerProtocol() {
|
|||||||
return handleFileRequest(request);
|
return handleFileRequest(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
// todo(@pengx17): remove this
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived(
|
session.defaultSession.webRequest.onHeadersReceived(
|
||||||
(responseDetails, callback) => {
|
(responseDetails, callback) => {
|
||||||
const { responseHeaders } = responseDetails;
|
const { responseHeaders } = responseDetails;
|
||||||
|
(async () => {
|
||||||
if (responseHeaders) {
|
if (responseHeaders) {
|
||||||
// replace SameSite=Lax with SameSite=None
|
|
||||||
const originalCookie =
|
const originalCookie =
|
||||||
responseHeaders['set-cookie'] || responseHeaders['Set-Cookie'];
|
responseHeaders['set-cookie'] || responseHeaders['Set-Cookie'];
|
||||||
|
|
||||||
if (originalCookie) {
|
if (originalCookie) {
|
||||||
delete responseHeaders['set-cookie'];
|
// save the cookies, to support third party cookies
|
||||||
delete responseHeaders['Set-Cookie'];
|
for (const cookies of originalCookie) {
|
||||||
responseHeaders['Set-Cookie'] = originalCookie.map(cookie => {
|
const parsedCookies = cookieParser.parse(cookies);
|
||||||
let newCookie = cookie.replace(/SameSite=Lax/gi, 'SameSite=None');
|
for (const parsedCookie of parsedCookies) {
|
||||||
|
if (!parsedCookie.value) {
|
||||||
// if the cookie is not secure, set it to secure
|
await session.defaultSession.cookies.remove(
|
||||||
if (!newCookie.includes('Secure')) {
|
responseDetails.url,
|
||||||
newCookie = newCookie + '; Secure';
|
parsedCookie.name
|
||||||
}
|
);
|
||||||
return newCookie;
|
} else {
|
||||||
|
await session.defaultSession.cookies.set({
|
||||||
|
url: responseDetails.url,
|
||||||
|
domain: parsedCookie.domain,
|
||||||
|
expirationDate: parsedCookie.expires?.getTime(),
|
||||||
|
httpOnly: parsedCookie.httpOnly,
|
||||||
|
secure: parsedCookie.secure,
|
||||||
|
value: parsedCookie.value,
|
||||||
|
name: parsedCookie.name,
|
||||||
|
path: parsedCookie.path,
|
||||||
|
sameSite: parsedCookie.sameSite?.toLowerCase() as
|
||||||
|
| 'unspecified'
|
||||||
|
| 'no_restriction'
|
||||||
|
| 'lax'
|
||||||
|
| 'strict'
|
||||||
|
| undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete responseHeaders['access-control-allow-origin'];
|
||||||
|
delete responseHeaders['access-control-allow-headers'];
|
||||||
|
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
|
||||||
|
responseHeaders['Access-Control-Allow-Headers'] = ['*'];
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.catch(err => {
|
||||||
|
logger.error('error handling headers received', err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
callback({ responseHeaders });
|
callback({ responseHeaders });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,9 +135,6 @@ export function registerProtocol() {
|
|||||||
const protocol = url.protocol;
|
const protocol = url.protocol;
|
||||||
const origin = url.origin;
|
const origin = url.origin;
|
||||||
|
|
||||||
const sameSite =
|
|
||||||
url.host === new URL(CLOUD_BASE_URL).host || protocol === 'file:';
|
|
||||||
|
|
||||||
// offline whitelist
|
// offline whitelist
|
||||||
// 1. do not block non-api request for http://localhost || file:// (local dev assets)
|
// 1. do not block non-api request for http://localhost || file:// (local dev assets)
|
||||||
// 2. do not block devtools
|
// 2. do not block devtools
|
||||||
@@ -148,22 +163,34 @@ export function registerProtocol() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
// session cookies are set to file:// on production
|
// session cookies are set to file:// on production
|
||||||
// if sending request to the cloud, attach the session cookie (to affine cloud server)
|
// if sending request to the cloud, attach the session cookie (to affine cloud server)
|
||||||
if (isNetworkResource(pathname) && sameSite) {
|
if (
|
||||||
const cookie = getCookies();
|
url.protocol === 'http:' ||
|
||||||
if (cookie) {
|
url.protocol === 'https:' ||
|
||||||
const cookieString = cookie.map(c => `${c.name}=${c.value}`).join('; ');
|
url.protocol === 'ws:' ||
|
||||||
details.requestHeaders['cookie'] = cookieString;
|
url.protocol === 'wss:'
|
||||||
}
|
) {
|
||||||
|
const cookies = await session.defaultSession.cookies.get({
|
||||||
|
url: details.url,
|
||||||
|
});
|
||||||
|
|
||||||
// add the referer and origin headers
|
const cookieString = cookies
|
||||||
details.requestHeaders['referer'] ??= CLOUD_BASE_URL;
|
.map(c => `${c.name}=${c.value}`)
|
||||||
details.requestHeaders['origin'] ??= CLOUD_BASE_URL;
|
.join('; ');
|
||||||
|
delete details.requestHeaders['cookie'];
|
||||||
|
details.requestHeaders['Cookie'] = cookieString;
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
|
.catch(err => {
|
||||||
|
logger.error('error handling before send headers', err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
callback({
|
callback({
|
||||||
cancel: false,
|
cancel: false,
|
||||||
requestHeaders: details.requestHeaders,
|
requestHeaders: details.requestHeaders,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface AuthenticationRequest {
|
export interface AuthenticationRequest {
|
||||||
method: 'magic-link' | 'oauth';
|
method: 'magic-link' | 'oauth';
|
||||||
payload: Record<string, any>;
|
payload: Record<string, any>;
|
||||||
|
server?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
session,
|
|
||||||
type View,
|
type View,
|
||||||
type WebContents,
|
type WebContents,
|
||||||
WebContentsView,
|
WebContentsView,
|
||||||
@@ -26,7 +25,7 @@ import {
|
|||||||
|
|
||||||
import { isMacOS } from '../../shared/utils';
|
import { isMacOS } from '../../shared/utils';
|
||||||
import { beforeAppQuit } from '../cleanup';
|
import { beforeAppQuit } from '../cleanup';
|
||||||
import { CLOUD_BASE_URL, isDev } from '../config';
|
import { isDev } from '../config';
|
||||||
import { mainWindowOrigin, shellViewUrl } from '../constants';
|
import { mainWindowOrigin, shellViewUrl } from '../constants';
|
||||||
import { ensureHelperProcess } from '../helper-process';
|
import { ensureHelperProcess } from '../helper-process';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
@@ -722,23 +721,6 @@ export class WebContentViewsManager {
|
|||||||
// add shell view
|
// add shell view
|
||||||
this.createAndAddView('shell').catch(err => logger.error(err));
|
this.createAndAddView('shell').catch(err => logger.error(err));
|
||||||
(async () => {
|
(async () => {
|
||||||
const updateCookies = () => {
|
|
||||||
session.defaultSession.cookies
|
|
||||||
.get({
|
|
||||||
url: CLOUD_BASE_URL,
|
|
||||||
})
|
|
||||||
.then(cookies => {
|
|
||||||
this.cookies = cookies;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error('failed to get cookies', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
updateCookies();
|
|
||||||
session.defaultSession.cookies.on('changed', () => {
|
|
||||||
updateCookies();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.tabViewsMeta.workbenches.length === 0) {
|
if (this.tabViewsMeta.workbenches.length === 0) {
|
||||||
// create a default view (e.g., on first launch)
|
// create a default view (e.g., on first launch)
|
||||||
await this.addTab();
|
await this.addTab();
|
||||||
@@ -921,10 +903,6 @@ export class WebContentViewsManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCookies() {
|
|
||||||
return WebContentViewsManager.instance.cookies;
|
|
||||||
}
|
|
||||||
|
|
||||||
// there is no proper way to listen to webContents resize event
|
// there is no proper way to listen to webContents resize event
|
||||||
// we will rely on window.resize event in renderer instead
|
// we will rely on window.resize event in renderer instead
|
||||||
export async function handleWebContentsResize(webContents?: WebContents) {
|
export async function handleWebContentsResize(webContents?: WebContents) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import type { InputProps } from '../../ui/input';
|
import type { InputProps } from '../../ui/input';
|
||||||
import { Input } from '../../ui/input';
|
import { Input } from '../../ui/input';
|
||||||
@@ -6,7 +7,7 @@ import * as styles from './share.css';
|
|||||||
export type AuthInputProps = InputProps & {
|
export type AuthInputProps = InputProps & {
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
errorHint?: string;
|
errorHint?: ReactNode;
|
||||||
withoutHint?: boolean;
|
withoutHint?: boolean;
|
||||||
onEnter?: () => void;
|
onEnter?: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export * from './back-button';
|
|||||||
export * from './change-email-page';
|
export * from './change-email-page';
|
||||||
export * from './change-password-page';
|
export * from './change-password-page';
|
||||||
export * from './count-down-render';
|
export * from './count-down-render';
|
||||||
export * from './modal';
|
|
||||||
export * from './modal-header';
|
export * from './modal-header';
|
||||||
export * from './onboarding-page';
|
export * from './onboarding-page';
|
||||||
export * from './password-input';
|
export * from './password-input';
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import type { FC } from 'react';
|
|||||||
import { modalHeaderWrapper } from './share.css';
|
import { modalHeaderWrapper } from './share.css';
|
||||||
export const ModalHeader: FC<{
|
export const ModalHeader: FC<{
|
||||||
title: string;
|
title: string;
|
||||||
subTitle: string;
|
subTitle?: string;
|
||||||
}> = ({ title, subTitle }) => {
|
}> = ({ title, subTitle }) => {
|
||||||
return (
|
return (
|
||||||
<div className={modalHeaderWrapper}>
|
<div className={modalHeaderWrapper}>
|
||||||
<p>
|
<p>
|
||||||
<Logo1Icon className="logo" />
|
{title === 'AFFiNE Cloud' && <Logo1Icon className="logo" />}
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<p>{subTitle}</p>
|
<p>{subTitle}</p>
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { FC, PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
import { Modal } from '../../ui/modal';
|
|
||||||
|
|
||||||
export type AuthModalProps = {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AuthModal: FC<PropsWithChildren<AuthModalProps>> = ({
|
|
||||||
children,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
width={400}
|
|
||||||
minHeight={500}
|
|
||||||
contentOptions={{
|
|
||||||
['data-testid' as string]: 'auth-modal',
|
|
||||||
style: { padding: '44px 40px 0' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -42,6 +42,7 @@ export const formHint = style({
|
|||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '4px',
|
bottom: '4px',
|
||||||
|
height: '22px',
|
||||||
left: 0,
|
left: 0,
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
selectors: {
|
selectors: {
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import { notify } from '@affine/component';
|
|
||||||
import {
|
|
||||||
AuthContent,
|
|
||||||
BackButton,
|
|
||||||
CountDownRender,
|
|
||||||
ModalHeader,
|
|
||||||
} from '@affine/component/auth-components';
|
|
||||||
import { Button } from '@affine/component/ui/button';
|
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
|
||||||
import { CaptchaService } from '@affine/core/modules/cloud';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../modules/cloud';
|
|
||||||
import type { AuthPanelProps } from './index';
|
|
||||||
import * as style from './style.css';
|
|
||||||
import { Captcha } from './use-captcha';
|
|
||||||
|
|
||||||
export const AfterSignUpSendEmail: FC<
|
|
||||||
AuthPanelProps<'afterSignUpSendEmail'>
|
|
||||||
> = ({ setAuthData, email, redirectUrl }) => {
|
|
||||||
const [resendCountDown, setResendCountDown] = useState(60);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
setResendCountDown(c => Math.max(c - 1, 0));
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(timer);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [isSending, setIsSending] = useState(false);
|
|
||||||
const t = useI18n();
|
|
||||||
const authService = useService(AuthService);
|
|
||||||
|
|
||||||
const captchaService = useService(CaptchaService);
|
|
||||||
|
|
||||||
const verifyToken = useLiveData(captchaService.verifyToken$);
|
|
||||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
|
||||||
const challenge = useLiveData(captchaService.challenge$);
|
|
||||||
|
|
||||||
const onResendClick = useAsyncCallback(async () => {
|
|
||||||
setIsSending(true);
|
|
||||||
try {
|
|
||||||
captchaService.revalidate();
|
|
||||||
await authService.sendEmailMagicLink(
|
|
||||||
email,
|
|
||||||
verifyToken,
|
|
||||||
challenge,
|
|
||||||
redirectUrl
|
|
||||||
);
|
|
||||||
setResendCountDown(60);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
notify.error({
|
|
||||||
title: 'Failed to send email, please try again.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setIsSending(false);
|
|
||||||
}, [authService, captchaService, challenge, email, redirectUrl, verifyToken]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ModalHeader
|
|
||||||
title={t['com.affine.auth.sign.up']()}
|
|
||||||
subTitle={t['com.affine.auth.sign.up.sent.email.subtitle']()}
|
|
||||||
/>
|
|
||||||
<AuthContent style={{ height: 100 }}>
|
|
||||||
<Trans
|
|
||||||
i18nKey="com.affine.auth.sign.sent.email.message.sent-tips"
|
|
||||||
values={{ email }}
|
|
||||||
components={{ a: <a href={`mailto:${email}`} /> }}
|
|
||||||
/>
|
|
||||||
{t['com.affine.auth.sign.sent.email.message.sent-tips.sign-up']()}
|
|
||||||
</AuthContent>
|
|
||||||
|
|
||||||
<div className={style.resendWrapper}>
|
|
||||||
{resendCountDown <= 0 ? (
|
|
||||||
<>
|
|
||||||
<Captcha />
|
|
||||||
<Button
|
|
||||||
style={
|
|
||||||
!verifyToken && needCaptcha ? { cursor: 'not-allowed' } : {}
|
|
||||||
}
|
|
||||||
disabled={(!verifyToken && needCaptcha) || isSending}
|
|
||||||
variant="plain"
|
|
||||||
size="large"
|
|
||||||
onClick={onResendClick}
|
|
||||||
>
|
|
||||||
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className={style.sentRow}>
|
|
||||||
<div className={style.sentMessage}>
|
|
||||||
{t['com.affine.auth.sent']()}
|
|
||||||
</div>
|
|
||||||
<CountDownRender
|
|
||||||
className={style.resendCountdown}
|
|
||||||
timeLeft={resendCountDown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
|
||||||
{t['com.affine.auth.sign.auth.code.message']()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BackButton
|
|
||||||
onClick={useCallback(() => {
|
|
||||||
setAuthData({ state: 'signIn' });
|
|
||||||
}, [setAuthData])}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useConfirmModal } from '@affine/component';
|
import { useConfirmModal } from '@affine/component';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { atom, useAtom } from 'jotai';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export const showAILoginRequiredAtom = atom(false);
|
export const showAILoginRequiredAtom = atom(false);
|
||||||
@@ -9,12 +10,12 @@ export const showAILoginRequiredAtom = atom(false);
|
|||||||
export const AiLoginRequiredModal = () => {
|
export const AiLoginRequiredModal = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [open, setOpen] = useAtom(showAILoginRequiredAtom);
|
const [open, setOpen] = useAtom(showAILoginRequiredAtom);
|
||||||
const setAuth = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
const openSignIn = useCallback(() => {
|
const openSignIn = useCallback(() => {
|
||||||
setAuth(prev => ({ ...prev, openModal: true }));
|
globalDialogService.open('sign-in', {});
|
||||||
}, [setAuth]);
|
}, [globalDialogService]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { notify } from '@affine/component';
|
|
||||||
import { AuthModal as AuthModalBase } from '@affine/component/auth-components';
|
|
||||||
import { authAtom, type AuthAtomData } from '@affine/core/components/atoms';
|
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useAtom } from 'jotai/react';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
|
||||||
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
|
||||||
import { SendEmail } from './send-email';
|
|
||||||
import { SignIn } from './sign-in';
|
|
||||||
import { SignInWithPassword } from './sign-in-with-password';
|
|
||||||
|
|
||||||
type AuthAtomType<T extends AuthAtomData['state']> = Extract<
|
|
||||||
AuthAtomData,
|
|
||||||
{ state: T }
|
|
||||||
>;
|
|
||||||
|
|
||||||
// return field in B that is not in A
|
|
||||||
type Difference<
|
|
||||||
A extends Record<string, any>,
|
|
||||||
B extends Record<string, any>,
|
|
||||||
> = Pick<B, Exclude<keyof B, keyof A>>;
|
|
||||||
|
|
||||||
export type AuthPanelProps<State extends AuthAtomData['state']> = {
|
|
||||||
setAuthData: <T extends AuthAtomData['state']>(
|
|
||||||
updates: { state: T } & Difference<AuthAtomType<State>, AuthAtomType<T>>
|
|
||||||
) => void;
|
|
||||||
onSkip?: () => void;
|
|
||||||
redirectUrl?: string;
|
|
||||||
} & Extract<AuthAtomData, { state: State }>;
|
|
||||||
|
|
||||||
const config: {
|
|
||||||
[k in AuthAtomData['state']]: FC<AuthPanelProps<k>>;
|
|
||||||
} = {
|
|
||||||
signIn: SignIn,
|
|
||||||
afterSignUpSendEmail: AfterSignUpSendEmail,
|
|
||||||
afterSignInSendEmail: AfterSignInSendEmail,
|
|
||||||
signInWithPassword: SignInWithPassword,
|
|
||||||
sendEmail: SendEmail,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AuthModal() {
|
|
||||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
|
||||||
const setOpen = useCallback(
|
|
||||||
(open: boolean) => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, openModal: open }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthModalBase open={authAtomValue.openModal} setOpen={setOpen}>
|
|
||||||
<AuthPanel />
|
|
||||||
</AuthModalBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthPanel({
|
|
||||||
onSkip,
|
|
||||||
redirectUrl,
|
|
||||||
}: {
|
|
||||||
onSkip?: () => void;
|
|
||||||
redirectUrl?: string | null;
|
|
||||||
}) {
|
|
||||||
const t = useI18n();
|
|
||||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
|
||||||
const authService = useService(AuthService);
|
|
||||||
const loginStatus = useLiveData(authService.session.status$);
|
|
||||||
const previousLoginStatus = useRef(loginStatus);
|
|
||||||
|
|
||||||
const setAuthData = useCallback(
|
|
||||||
(updates: Partial<AuthAtomData>) => {
|
|
||||||
// @ts-expect-error checked in impls
|
|
||||||
setAuthAtom(prev => ({
|
|
||||||
...prev,
|
|
||||||
...updates,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
loginStatus === 'authenticated' &&
|
|
||||||
previousLoginStatus.current === 'unauthenticated'
|
|
||||||
) {
|
|
||||||
setAuthAtom({
|
|
||||||
openModal: false,
|
|
||||||
state: 'signIn',
|
|
||||||
});
|
|
||||||
notify.success({
|
|
||||||
title: t['com.affine.auth.toast.title.signed-in'](),
|
|
||||||
message: t['com.affine.auth.toast.message.signed-in'](),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
previousLoginStatus.current = loginStatus;
|
|
||||||
}, [loginStatus, setAuthAtom, t]);
|
|
||||||
|
|
||||||
const CurrentPanel = config[authAtomValue.state];
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
...authAtomValue,
|
|
||||||
onSkip,
|
|
||||||
redirectUrl,
|
|
||||||
setAuthData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error checked in impls
|
|
||||||
return <CurrentPanel {...props} />;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Skeleton } from '@affine/component';
|
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { ServerService } from '@affine/core/modules/cloud';
|
import { ServerService } from '@affine/core/modules/cloud';
|
||||||
import { UrlService } from '@affine/core/modules/url';
|
import { UrlService } from '@affine/core/modules/url';
|
||||||
@@ -38,7 +37,7 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
|
|||||||
const scheme = urlService.getClientScheme();
|
const scheme = urlService.getClientScheme();
|
||||||
|
|
||||||
if (!oauth) {
|
if (!oauth) {
|
||||||
return <Skeleton height={50} />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return oauthProviders?.map(provider => (
|
return oauthProviders?.map(provider => (
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
import { notify, Wrapper } from '@affine/component';
|
|
||||||
import {
|
|
||||||
AuthContent,
|
|
||||||
AuthInput,
|
|
||||||
BackButton,
|
|
||||||
ModalHeader,
|
|
||||||
} from '@affine/component/auth-components';
|
|
||||||
import { Button } from '@affine/component/ui/button';
|
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
|
||||||
import {
|
|
||||||
sendChangeEmailMutation,
|
|
||||||
sendChangePasswordEmailMutation,
|
|
||||||
sendSetPasswordEmailMutation,
|
|
||||||
sendVerifyEmailMutation,
|
|
||||||
} from '@affine/graphql';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
import { useMutation } from '../../../components/hooks/use-mutation';
|
|
||||||
import { ServerService } from '../../../modules/cloud';
|
|
||||||
import type { AuthPanelProps } from './index';
|
|
||||||
|
|
||||||
const useEmailTitle = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
switch (emailType) {
|
|
||||||
case 'setPassword':
|
|
||||||
return t['com.affine.auth.set.password']();
|
|
||||||
case 'changePassword':
|
|
||||||
return t['com.affine.auth.reset.password']();
|
|
||||||
case 'changeEmail':
|
|
||||||
return t['com.affine.settings.email.action.change']();
|
|
||||||
case 'verifyEmail':
|
|
||||||
return t['com.affine.settings.email.action.verify']();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const useNotificationHint = (
|
|
||||||
emailType: AuthPanelProps<'sendEmail'>['emailType']
|
|
||||||
) => {
|
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
switch (emailType) {
|
|
||||||
case 'setPassword':
|
|
||||||
return t['com.affine.auth.sent.set.password.hint']();
|
|
||||||
case 'changePassword':
|
|
||||||
return t['com.affine.auth.sent.change.password.hint']();
|
|
||||||
case 'changeEmail':
|
|
||||||
case 'verifyEmail':
|
|
||||||
return t['com.affine.auth.sent.verify.email.hint']();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const useButtonContent = (
|
|
||||||
emailType: AuthPanelProps<'sendEmail'>['emailType']
|
|
||||||
) => {
|
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
switch (emailType) {
|
|
||||||
case 'setPassword':
|
|
||||||
return t['com.affine.auth.send.set.password.link']();
|
|
||||||
case 'changePassword':
|
|
||||||
return t['com.affine.auth.send.reset.password.link']();
|
|
||||||
case 'changeEmail':
|
|
||||||
case 'verifyEmail':
|
|
||||||
return t['com.affine.auth.send.verify.email.hint']();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSendEmail = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
|
||||||
const {
|
|
||||||
trigger: sendChangePasswordEmail,
|
|
||||||
isMutating: isChangePasswordMutating,
|
|
||||||
} = useMutation({
|
|
||||||
mutation: sendChangePasswordEmailMutation,
|
|
||||||
});
|
|
||||||
const { trigger: sendSetPasswordEmail, isMutating: isSetPasswordMutating } =
|
|
||||||
useMutation({
|
|
||||||
mutation: sendSetPasswordEmailMutation,
|
|
||||||
});
|
|
||||||
const { trigger: sendChangeEmail, isMutating: isChangeEmailMutating } =
|
|
||||||
useMutation({
|
|
||||||
mutation: sendChangeEmailMutation,
|
|
||||||
});
|
|
||||||
const { trigger: sendVerifyEmail, isMutating: isVerifyEmailMutation } =
|
|
||||||
useMutation({
|
|
||||||
mutation: sendVerifyEmailMutation,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading:
|
|
||||||
isChangePasswordMutating ||
|
|
||||||
isSetPasswordMutating ||
|
|
||||||
isChangeEmailMutating ||
|
|
||||||
isVerifyEmailMutation,
|
|
||||||
sendEmail: useCallback(
|
|
||||||
(email: string) => {
|
|
||||||
let trigger: (args: {
|
|
||||||
email: string;
|
|
||||||
callbackUrl: string;
|
|
||||||
}) => Promise<unknown>;
|
|
||||||
let callbackUrl;
|
|
||||||
switch (emailType) {
|
|
||||||
case 'setPassword':
|
|
||||||
trigger = sendSetPasswordEmail;
|
|
||||||
callbackUrl = 'setPassword';
|
|
||||||
break;
|
|
||||||
case 'changePassword':
|
|
||||||
trigger = sendChangePasswordEmail;
|
|
||||||
callbackUrl = 'changePassword';
|
|
||||||
break;
|
|
||||||
case 'changeEmail':
|
|
||||||
trigger = sendChangeEmail;
|
|
||||||
callbackUrl = 'changeEmail';
|
|
||||||
break;
|
|
||||||
case 'verifyEmail':
|
|
||||||
trigger = sendVerifyEmail;
|
|
||||||
callbackUrl = 'verify-email';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO(@eyhn): add error handler
|
|
||||||
return trigger({
|
|
||||||
email,
|
|
||||||
callbackUrl: `/auth/${callbackUrl}`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[
|
|
||||||
emailType,
|
|
||||||
sendChangeEmail,
|
|
||||||
sendChangePasswordEmail,
|
|
||||||
sendSetPasswordEmail,
|
|
||||||
sendVerifyEmail,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SendEmail = ({
|
|
||||||
setAuthData,
|
|
||||||
email,
|
|
||||||
emailType,
|
|
||||||
// todo(@pengx17): impl redirectUrl for sendEmail?
|
|
||||||
}: AuthPanelProps<'sendEmail'>) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const serverService = useService(ServerService);
|
|
||||||
|
|
||||||
const passwordLimits = useLiveData(
|
|
||||||
serverService.server.credentialsRequirement$.map(r => r?.password)
|
|
||||||
);
|
|
||||||
const [hasSentEmail, setHasSentEmail] = useState(false);
|
|
||||||
|
|
||||||
const title = useEmailTitle(emailType);
|
|
||||||
const hint = useNotificationHint(emailType);
|
|
||||||
const buttonContent = useButtonContent(emailType);
|
|
||||||
const { loading, sendEmail } = useSendEmail(emailType);
|
|
||||||
|
|
||||||
const onSendEmail = useAsyncCallback(async () => {
|
|
||||||
// TODO(@eyhn): add error handler
|
|
||||||
await sendEmail(email);
|
|
||||||
|
|
||||||
notify.success({ title: hint });
|
|
||||||
setHasSentEmail(true);
|
|
||||||
}, [email, hint, sendEmail]);
|
|
||||||
|
|
||||||
const onBack = useCallback(() => {
|
|
||||||
setAuthData({ state: 'signIn' });
|
|
||||||
}, [setAuthData]);
|
|
||||||
|
|
||||||
if (!passwordLimits) {
|
|
||||||
// TODO(@eyhn): loading & error UI
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content =
|
|
||||||
emailType === 'setPassword'
|
|
||||||
? t['com.affine.auth.set.password.message']({
|
|
||||||
min: String(passwordLimits.minLength),
|
|
||||||
max: String(passwordLimits.maxLength),
|
|
||||||
})
|
|
||||||
: emailType === 'changePassword'
|
|
||||||
? t['com.affine.auth.reset.password.message']()
|
|
||||||
: emailType === 'changeEmail' || emailType === 'verifyEmail'
|
|
||||||
? t['com.affine.auth.verify.email.message']({ email })
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ModalHeader
|
|
||||||
title={t['com.affine.brand.affineCloud']()}
|
|
||||||
subTitle={title}
|
|
||||||
/>
|
|
||||||
<AuthContent>{content}</AuthContent>
|
|
||||||
|
|
||||||
<Wrapper
|
|
||||||
marginTop={30}
|
|
||||||
marginBottom={50}
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AuthInput
|
|
||||||
label={t['com.affine.settings.email']()}
|
|
||||||
disabled={true}
|
|
||||||
value={email}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="extraLarge"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
disabled={hasSentEmail}
|
|
||||||
loading={loading}
|
|
||||||
onClick={onSendEmail}
|
|
||||||
>
|
|
||||||
{hasSentEmail ? t['com.affine.auth.sent']() : buttonContent}
|
|
||||||
</Button>
|
|
||||||
<BackButton onClick={onBack} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import { notify } from '@affine/component';
|
|
||||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
|
||||||
import { Button } from '@affine/component/ui/button';
|
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
|
||||||
import { CaptchaService } from '@affine/core/modules/cloud';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
|
||||||
import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../modules/cloud';
|
|
||||||
import { emailRegex } from '../../../utils/email-regex';
|
|
||||||
import type { AuthPanelProps } from './index';
|
|
||||||
import { OAuth } from './oauth';
|
|
||||||
import * as style from './style.css';
|
|
||||||
import { Captcha } from './use-captcha';
|
|
||||||
|
|
||||||
function validateEmail(email: string) {
|
|
||||||
return emailRegex.test(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SignIn: FC<AuthPanelProps<'signIn'>> = ({
|
|
||||||
setAuthData: setAuthState,
|
|
||||||
onSkip,
|
|
||||||
redirectUrl,
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const authService = useService(AuthService);
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const [isMutating, setIsMutating] = useState(false);
|
|
||||||
const captchaService = useService(CaptchaService);
|
|
||||||
|
|
||||||
const verifyToken = useLiveData(captchaService.verifyToken$);
|
|
||||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
|
||||||
const challenge = useLiveData(captchaService.challenge$);
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
|
|
||||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
|
||||||
const errorMsg = searchParams.get('error');
|
|
||||||
|
|
||||||
const onContinue = useAsyncCallback(async () => {
|
|
||||||
if (!validateEmail(email)) {
|
|
||||||
setIsValidEmail(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsValidEmail(true);
|
|
||||||
setIsMutating(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { hasPassword, registered } =
|
|
||||||
await authService.checkUserByEmail(email);
|
|
||||||
|
|
||||||
if (registered) {
|
|
||||||
// provider password sign-in if user has by default
|
|
||||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
|
||||||
if (hasPassword) {
|
|
||||||
setAuthState({
|
|
||||||
state: 'signInWithPassword',
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
captchaService.revalidate();
|
|
||||||
await authService.sendEmailMagicLink(
|
|
||||||
email,
|
|
||||||
verifyToken,
|
|
||||||
challenge,
|
|
||||||
redirectUrl
|
|
||||||
);
|
|
||||||
setAuthState({
|
|
||||||
state: 'afterSignInSendEmail',
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
captchaService.revalidate();
|
|
||||||
await authService.sendEmailMagicLink(
|
|
||||||
email,
|
|
||||||
verifyToken,
|
|
||||||
challenge,
|
|
||||||
redirectUrl
|
|
||||||
);
|
|
||||||
setAuthState({
|
|
||||||
state: 'afterSignUpSendEmail',
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
// TODO(@eyhn): better error handling
|
|
||||||
notify.error({
|
|
||||||
title: 'Failed to send email. Please try again.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsMutating(false);
|
|
||||||
}, [
|
|
||||||
authService,
|
|
||||||
captchaService,
|
|
||||||
challenge,
|
|
||||||
email,
|
|
||||||
redirectUrl,
|
|
||||||
setAuthState,
|
|
||||||
verifyToken,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ModalHeader
|
|
||||||
title={t['com.affine.auth.sign.in']()}
|
|
||||||
subTitle={t['com.affine.brand.affineCloud']()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OAuth redirectUrl={redirectUrl} />
|
|
||||||
|
|
||||||
<div className={style.authModalContent}>
|
|
||||||
<AuthInput
|
|
||||||
label={t['com.affine.settings.email']()}
|
|
||||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
|
||||||
onChange={setEmail}
|
|
||||||
error={!isValidEmail}
|
|
||||||
errorHint={
|
|
||||||
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
|
||||||
}
|
|
||||||
onEnter={onContinue}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{verifyToken || !needCaptcha ? (
|
|
||||||
<Button
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
size="extraLarge"
|
|
||||||
data-testid="continue-login-button"
|
|
||||||
block
|
|
||||||
loading={isMutating}
|
|
||||||
suffix={<ArrowRightBigIcon />}
|
|
||||||
suffixStyle={{ width: 20, height: 20, color: cssVar('blue') }}
|
|
||||||
onClick={onContinue}
|
|
||||||
>
|
|
||||||
{t['com.affine.auth.sign.email.continue']()}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Captcha />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{errorMsg && <div className={style.errorMessage}>{errorMsg}</div>}
|
|
||||||
|
|
||||||
<div className={style.authMessage}>
|
|
||||||
{/*prettier-ignore*/}
|
|
||||||
<Trans i18nKey="com.affine.auth.sign.message">
|
|
||||||
By clicking "Continue with Google/Email" above, you acknowledge that
|
|
||||||
you agree to AFFiNE's <a href="https://affine.pro/terms" target="_blank" rel="noreferrer">Terms of Conditions</a> and <a href="https://affine.pro/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>.
|
|
||||||
</Trans>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{onSkip ? (
|
|
||||||
<>
|
|
||||||
<div className={style.skipDivider}>
|
|
||||||
<div className={style.skipDividerLine} />
|
|
||||||
<span className={style.skipDividerText}>or</span>
|
|
||||||
<div className={style.skipDividerLine} />
|
|
||||||
</div>
|
|
||||||
<div className={style.skipSection}>
|
|
||||||
<div className={style.skipText}>
|
|
||||||
{t['com.affine.mobile.sign-in.skip.hint']()}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="plain"
|
|
||||||
onClick={onSkip}
|
|
||||||
className={style.skipLink}
|
|
||||||
suffix={<ArrowRightBigIcon className={style.skipLinkIcon} />}
|
|
||||||
>
|
|
||||||
{t['com.affine.mobile.sign-in.skip.link']()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,99 +1,6 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { style } from '@vanilla-extract/css';
|
||||||
import { globalStyle, style } from '@vanilla-extract/css';
|
|
||||||
export const authModalContent = style({
|
|
||||||
marginTop: '30px',
|
|
||||||
});
|
|
||||||
export const captchaWrapper = style({
|
|
||||||
margin: 'auto',
|
|
||||||
marginBottom: '4px',
|
|
||||||
textAlign: 'center',
|
|
||||||
});
|
|
||||||
export const authMessage = style({
|
|
||||||
marginTop: '30px',
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
lineHeight: 1.5,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const errorMessage = style({
|
|
||||||
marginTop: '30px',
|
|
||||||
color: cssVar('textHighlightForegroundRed'),
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
lineHeight: 1.5,
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle(`${authMessage} a`, {
|
|
||||||
color: cssVar('linkColor'),
|
|
||||||
});
|
|
||||||
globalStyle(`${authMessage} .link`, {
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: cssVar('linkColor'),
|
|
||||||
});
|
|
||||||
export const forgetPasswordButtonRow = style({
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
marginTop: '-26px', // Let this button be a tail of password input.
|
|
||||||
});
|
|
||||||
export const sendMagicLinkButtonRow = style({
|
|
||||||
marginBottom: '30px',
|
|
||||||
});
|
|
||||||
export const linkButton = style({
|
|
||||||
color: cssVar('linkColor'),
|
|
||||||
background: 'transparent',
|
|
||||||
borderColor: 'transparent',
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
lineHeight: '22px',
|
|
||||||
userSelect: 'none',
|
|
||||||
});
|
|
||||||
export const forgetPasswordButton = style({
|
|
||||||
fontSize: cssVar('fontSm'),
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
});
|
|
||||||
export const resendWrapper = style({
|
|
||||||
height: 77,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 30,
|
|
||||||
});
|
|
||||||
export const sentRow = style({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
lineHeight: '22px',
|
|
||||||
fontSize: cssVar('fontSm'),
|
|
||||||
});
|
|
||||||
export const sentMessage = style({
|
|
||||||
color: cssVar('textPrimaryColor'),
|
|
||||||
fontWeight: 600,
|
|
||||||
});
|
|
||||||
export const resendCountdown = style({
|
|
||||||
width: 45,
|
|
||||||
textAlign: 'center',
|
|
||||||
});
|
|
||||||
export const resendCountdownInButton = style({
|
|
||||||
width: 40,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: cssVar('fontSm'),
|
|
||||||
marginLeft: 16,
|
|
||||||
color: cssVar('blue'),
|
|
||||||
fontWeight: 400,
|
|
||||||
});
|
|
||||||
export const accessMessage = style({
|
|
||||||
textAlign: 'center',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
fontWeight: 500,
|
|
||||||
marginTop: 65,
|
|
||||||
marginBottom: 40,
|
|
||||||
});
|
|
||||||
export const userPlanButton = style({
|
export const userPlanButton = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
@@ -114,44 +21,3 @@ export const userPlanButton = style({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const skipDivider = style({
|
|
||||||
display: 'flex',
|
|
||||||
gap: 12,
|
|
||||||
alignItems: 'center',
|
|
||||||
height: 20,
|
|
||||||
marginTop: 12,
|
|
||||||
marginBottom: 12,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const skipDividerLine = style({
|
|
||||||
flex: 1,
|
|
||||||
height: 0,
|
|
||||||
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const skipDividerText = style({
|
|
||||||
color: cssVarV2('text/secondary'),
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const skipText = style({
|
|
||||||
color: cssVarV2('text/primary'),
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
fontWeight: 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const skipLink = style({
|
|
||||||
color: cssVarV2('text/link'),
|
|
||||||
fontSize: cssVar('fontXs'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const skipLinkIcon = style({
|
|
||||||
color: cssVarV2('text/link'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const skipSection = style({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
export const loadingContainer = style({
|
|
||||||
display: 'flex',
|
|
||||||
width: '100vw',
|
|
||||||
height: '60vh',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
});
|
|
||||||
export const subscriptionLayout = style({
|
|
||||||
margin: '10% auto',
|
|
||||||
maxWidth: '536px',
|
|
||||||
});
|
|
||||||
export const subscriptionBox = style({
|
|
||||||
padding: '48px 52px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
});
|
|
||||||
export const subscriptionTips = style({
|
|
||||||
margin: '20px 0',
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
fontSize: '12px',
|
|
||||||
fontStyle: 'normal',
|
|
||||||
fontWeight: '400',
|
|
||||||
lineHeight: '20px',
|
|
||||||
});
|
|
||||||
@@ -2,7 +2,6 @@ import { Tabs, Tooltip } from '@affine/component';
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { Doc } from '@blocksuite/affine/store';
|
import type { Doc } from '@blocksuite/affine/store';
|
||||||
import { LockIcon, PublishIcon } from '@blocksuite/icons/rc';
|
import { LockIcon, PublishIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -121,7 +120,7 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
|||||||
export const ShareMenu = (props: ShareMenuProps) => {
|
export const ShareMenu = (props: ShareMenuProps) => {
|
||||||
const { workspaceMetadata } = props;
|
const { workspaceMetadata } = props;
|
||||||
|
|
||||||
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
if (workspaceMetadata.flavour === 'local') {
|
||||||
return <LocalShareMenu {...props} />;
|
return <LocalShareMenu {...props} />;
|
||||||
}
|
}
|
||||||
return <CloudShareMenu {...props} />;
|
return <CloudShareMenu {...props} />;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
|||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
@@ -316,11 +315,9 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SharePage = (props: ShareMenuProps) => {
|
export const SharePage = (props: ShareMenuProps) => {
|
||||||
if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
if (props.workspaceMetadata.flavour === 'local') {
|
||||||
return <LocalSharePage {...props} />;
|
return <LocalSharePage {...props} />;
|
||||||
} else if (
|
} else {
|
||||||
props.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
// TODO(@eyhn): refactor this part
|
// TODO(@eyhn): refactor this part
|
||||||
<ErrorBoundary fallback={null}>
|
<ErrorBoundary fallback={null}>
|
||||||
@@ -330,5 +327,4 @@ export const SharePage = (props: ShareMenuProps) => {
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error('Unreachable');
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,48 +1,11 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
// modal atoms
|
|
||||||
export const openWorkspacesModalAtom = atom(false);
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use `useSignOut` hook instated
|
* @deprecated use `useSignOut` hook instated
|
||||||
*/
|
*/
|
||||||
export const openQuotaModalAtom = atom(false);
|
export const openQuotaModalAtom = atom(false);
|
||||||
export const rightSidebarWidthAtom = atom(320);
|
export const rightSidebarWidthAtom = atom(320);
|
||||||
|
|
||||||
export const openImportModalAtom = atom(false);
|
|
||||||
|
|
||||||
export type AuthAtomData =
|
|
||||||
| { state: 'signIn' }
|
|
||||||
| {
|
|
||||||
state: 'afterSignUpSendEmail';
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
state: 'afterSignInSendEmail';
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
state: 'signInWithPassword';
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
state: 'sendEmail';
|
|
||||||
email: string;
|
|
||||||
emailType:
|
|
||||||
| 'setPassword'
|
|
||||||
| 'changePassword'
|
|
||||||
| 'changeEmail'
|
|
||||||
| 'verifyEmail';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const authAtom = atom<
|
|
||||||
AuthAtomData & {
|
|
||||||
openModal: boolean;
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
openModal: false,
|
|
||||||
state: 'signIn',
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
||||||
export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs');
|
export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs');
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||||
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
|
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import {
|
import {
|
||||||
type getCopilotHistoriesQuery,
|
type getCopilotHistoriesQuery,
|
||||||
type RequestOptions,
|
type RequestOptions,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { UnauthorizedError } from '@blocksuite/affine/blocks';
|
import { UnauthorizedError } from '@blocksuite/affine/blocks';
|
||||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||||
import { getCurrentStore } from '@toeverything/infra';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { CopilotClient } from './copilot-client';
|
import type { CopilotClient } from './copilot-client';
|
||||||
@@ -42,7 +41,10 @@ const processTypeToPromptName = new Map(
|
|||||||
// user-id:workspace-id:doc-id -> chat session id
|
// user-id:workspace-id:doc-id -> chat session id
|
||||||
const chatSessions = new Map<string, Promise<string>>();
|
const chatSessions = new Map<string, Promise<string>>();
|
||||||
|
|
||||||
export function setupAIProvider(client: CopilotClient) {
|
export function setupAIProvider(
|
||||||
|
client: CopilotClient,
|
||||||
|
globalDialogService: GlobalDialogService
|
||||||
|
) {
|
||||||
async function getChatSessionId(workspaceId: string, docId: string) {
|
async function getChatSessionId(workspaceId: string, docId: string) {
|
||||||
const userId = (await AIProvider.userInfo)?.id;
|
const userId = (await AIProvider.userInfo)?.id;
|
||||||
|
|
||||||
@@ -499,10 +501,7 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
});
|
});
|
||||||
|
|
||||||
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.on(() => {
|
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.on(() => {
|
||||||
getCurrentStore().set(authAtom, s => ({
|
globalDialogService.open('sign-in', {});
|
||||||
...s,
|
|
||||||
openModal: true,
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setupTracker();
|
setupTracker();
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { EditorService } from '@affine/core/modules/editor';
|
|||||||
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import type { Doc } from '@blocksuite/affine/store';
|
import type { Doc } from '@blocksuite/affine/store';
|
||||||
@@ -111,7 +110,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
|
|
||||||
const openHistoryModal = useCallback(() => {
|
const openHistoryModal = useCallback(() => {
|
||||||
track.$.header.history.open();
|
track.$.header.history.open();
|
||||||
if (workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
if (workspace.flavour === 'affine-cloud') {
|
||||||
return setHistoryModalOpen(true);
|
return setHistoryModalOpen(true);
|
||||||
}
|
}
|
||||||
return setOpenHistoryTipsModal(true);
|
return setOpenHistoryTipsModal(true);
|
||||||
@@ -398,8 +397,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
data-testid="editor-option-menu-delete"
|
data-testid="editor-option-menu-delete"
|
||||||
onSelect={handleOpenTrashModal}
|
onSelect={handleOpenTrashModal}
|
||||||
/>
|
/>
|
||||||
{BUILD_CONFIG.isWeb &&
|
{BUILD_CONFIG.isWeb && workspace.flavour === 'affine-cloud' ? (
|
||||||
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
prefixIcon={<LocalWorkspaceIcon />}
|
prefixIcon={<LocalWorkspaceIcon />}
|
||||||
data-testid="editor-option-menu-link"
|
data-testid="editor-option-menu-link"
|
||||||
@@ -426,7 +424,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
>
|
>
|
||||||
<HeaderDropDownButton />
|
<HeaderDropDownButton />
|
||||||
</Menu>
|
</Menu>
|
||||||
{workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
{workspace.flavour !== 'local' ? (
|
||||||
<PageHistoryModal
|
<PageHistoryModal
|
||||||
docCollection={workspace.docCollection}
|
docCollection={workspace.docCollection}
|
||||||
open={historyModalOpen}
|
open={historyModalOpen}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const SignIn = () => {
|
export const SignIn = () => {
|
||||||
const setOpen = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const onClickSignIn = useCallback(() => {
|
const onClickSignIn = useCallback(() => {
|
||||||
setOpen(state => ({
|
globalDialogService.open('sign-in', {});
|
||||||
...state,
|
}, [globalDialogService]);
|
||||||
openModal: true,
|
|
||||||
}));
|
|
||||||
}, [setOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Avatar, PropertyValue } from '@affine/component';
|
import { Avatar, PropertyValue } from '@affine/component';
|
||||||
import { CloudDocMetaService } from '@affine/core/modules/cloud/services/cloud-doc-meta';
|
import { CloudDocMetaService } from '@affine/core/modules/cloud/services/cloud-doc-meta';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
@@ -73,8 +72,7 @@ const LocalUserValue = () => {
|
|||||||
|
|
||||||
export const CreatedByValue = () => {
|
export const CreatedByValue = () => {
|
||||||
const workspaceService = useService(WorkspaceService);
|
const workspaceService = useService(WorkspaceService);
|
||||||
const isCloud =
|
const isCloud = workspaceService.workspace.flavour !== 'local';
|
||||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
|
||||||
|
|
||||||
if (!isCloud) {
|
if (!isCloud) {
|
||||||
return (
|
return (
|
||||||
@@ -93,8 +91,7 @@ export const CreatedByValue = () => {
|
|||||||
|
|
||||||
export const UpdatedByValue = () => {
|
export const UpdatedByValue = () => {
|
||||||
const workspaceService = useService(WorkspaceService);
|
const workspaceService = useService(WorkspaceService);
|
||||||
const isCloud =
|
const isCloud = workspaceService.workspace.flavour !== 'local';
|
||||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
|
||||||
|
|
||||||
if (!isCloud) {
|
if (!isCloud) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notify, useConfirmModal } from '@affine/component';
|
import { notify, useConfirmModal } from '@affine/component';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { Workspace } from '@toeverything/infra';
|
import type { Workspace } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
useService,
|
useService,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useNavigateHelper } from '../use-navigate-helper';
|
import { useNavigateHelper } from '../use-navigate-helper';
|
||||||
@@ -31,7 +30,7 @@ export const useEnableCloud = () => {
|
|||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
const account = useLiveData(authService.session.account$);
|
const account = useLiveData(authService.session.account$);
|
||||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||||
const setAuthAtom = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
const { jumpToPage } = useNavigateHelper();
|
const { jumpToPage } = useNavigateHelper();
|
||||||
@@ -58,8 +57,8 @@ export const useEnableCloud = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const openSignIn = useCallback(() => {
|
const openSignIn = useCallback(() => {
|
||||||
setAuthAtom(prev => ({ ...prev, openModal: true }));
|
globalDialogService.open('sign-in', {});
|
||||||
}, [setAuthAtom]);
|
}, [globalDialogService]);
|
||||||
|
|
||||||
const signInOrEnableCloud = useCallback(
|
const signInOrEnableCloud = useCallback(
|
||||||
async (...args: ConfirmEnableArgs) => {
|
async (...args: ConfirmEnableArgs) => {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type { Editor } from '@affine/core/modules/editor';
|
|||||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||||
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import {
|
import {
|
||||||
@@ -78,7 +77,7 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
|||||||
});
|
});
|
||||||
}, [doc, openConfirmModal, t]);
|
}, [doc, openConfirmModal, t]);
|
||||||
|
|
||||||
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
const isCloudWorkspace = workspace.flavour !== 'local';
|
||||||
|
|
||||||
const openInAppService = useServiceOptional(OpenInAppService);
|
const openInAppService = useServiceOptional(OpenInAppService);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
} from '@affine/core/commands';
|
} from '@affine/core/commands';
|
||||||
import { useSharingUrl } from '@affine/core/components/hooks/affine/use-share-url';
|
import { useSharingUrl } from '@affine/core/components/hooks/affine/use-share-url';
|
||||||
import { useIsActiveView } from '@affine/core/modules/workbench';
|
import { useIsActiveView } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { type WorkspaceMetadata } from '@toeverything/infra';
|
import { type WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -18,7 +17,7 @@ export function useRegisterCopyLinkCommands({
|
|||||||
}) {
|
}) {
|
||||||
const isActiveView = useIsActiveView();
|
const isActiveView = useIsActiveView();
|
||||||
const workspaceId = workspaceMeta.id;
|
const workspaceId = workspaceMeta.id;
|
||||||
const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
const isCloud = workspaceMeta.flavour !== 'local';
|
||||||
|
|
||||||
const { onClickCopyLink } = useSharingUrl({
|
const { onClickCopyLink } = useSharingUrl({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import {
|
|||||||
notify,
|
notify,
|
||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
@@ -32,18 +31,14 @@ export const useSignOut = ({
|
|||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
const serverService = useService(ServerService);
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
const globalContextService = useService(GlobalContextService);
|
const globalContextService = useService(GlobalContextService);
|
||||||
|
|
||||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
const currentWorkspaceId = useLiveData(
|
const currentWorkspaceFlavour = useLiveData(
|
||||||
globalContextService.globalContext.workspaceId.$
|
globalContextService.globalContext.workspaceFlavour.$
|
||||||
);
|
|
||||||
const currentWorkspaceMetadata = useLiveData(
|
|
||||||
currentWorkspaceId
|
|
||||||
? workspacesService.list.workspace$(currentWorkspaceId)
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const signOut = useCallback(async () => {
|
const signOut = useCallback(async () => {
|
||||||
@@ -58,10 +53,10 @@ export const useSignOut = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// if current workspace is affine cloud, switch to local workspace
|
// if current workspace is sign out, switch to other workspace
|
||||||
if (currentWorkspaceMetadata?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
if (currentWorkspaceFlavour === serverService.server.id) {
|
||||||
const localWorkspace = workspaces.find(
|
const localWorkspace = workspaces.find(
|
||||||
w => w.flavour === WorkspaceFlavour.LOCAL
|
w => w.flavour !== serverService.server.id
|
||||||
);
|
);
|
||||||
if (localWorkspace) {
|
if (localWorkspace) {
|
||||||
openPage(localWorkspace.id, 'all');
|
openPage(localWorkspace.id, 'all');
|
||||||
@@ -69,9 +64,10 @@ export const useSignOut = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
authService,
|
authService,
|
||||||
currentWorkspaceMetadata?.flavour,
|
currentWorkspaceFlavour,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
openPage,
|
openPage,
|
||||||
|
serverService.server.id,
|
||||||
workspaces,
|
workspaces,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
timeout,
|
timeout,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { Map as YMap } from 'yjs';
|
|
||||||
|
|
||||||
import { CopilotClient } from '../blocksuite/block-suite-editor/ai/copilot-client';
|
import { CopilotClient } from '../blocksuite/block-suite-editor/ai/copilot-client';
|
||||||
import { setupAIProvider } from '../blocksuite/block-suite-editor/ai/setup-provider';
|
import { setupAIProvider } from '../blocksuite/block-suite-editor/ai/setup-provider';
|
||||||
@@ -147,38 +146,18 @@ export const WorkspaceSideEffects = () => {
|
|||||||
graphqlService.gql,
|
graphqlService.gql,
|
||||||
fetchService.fetch,
|
fetchService.fetch,
|
||||||
eventSourceService.eventSource
|
eventSourceService.eventSource
|
||||||
)
|
),
|
||||||
|
globalDialogService
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
dispose();
|
dispose();
|
||||||
};
|
};
|
||||||
}, [eventSourceService, fetchService, graphqlService]);
|
}, [eventSourceService, fetchService, globalDialogService, graphqlService]);
|
||||||
|
|
||||||
useRegisterWorkspaceCommands();
|
useRegisterWorkspaceCommands();
|
||||||
useRegisterNavigationCommands();
|
useRegisterNavigationCommands();
|
||||||
useRegisterFindInPageCommands();
|
useRegisterFindInPageCommands();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// hotfix for blockVersions
|
|
||||||
// this is a mistake in the
|
|
||||||
// 0.8.0 ~ 0.8.1
|
|
||||||
// 0.8.0-beta.0 ~ 0.8.0-beta.3
|
|
||||||
// 0.8.0-canary.17 ~ 0.9.0-canary.3
|
|
||||||
const meta = currentWorkspace.docCollection.doc.getMap('meta');
|
|
||||||
const blockVersions = meta.get('blockVersions');
|
|
||||||
if (
|
|
||||||
!(blockVersions instanceof YMap) &&
|
|
||||||
blockVersions !== null &&
|
|
||||||
blockVersions !== undefined &&
|
|
||||||
typeof blockVersions === 'object'
|
|
||||||
) {
|
|
||||||
meta.set(
|
|
||||||
'blockVersions',
|
|
||||||
new YMap(Object.entries(blockVersions as Record<string, number>))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [currentWorkspace.docCollection.doc]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<QuickSearchContainer />
|
<QuickSearchContainer />
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
type MenuProps,
|
type MenuProps,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
@@ -17,7 +16,6 @@ import { useLiveData, useService } from '@toeverything/infra';
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -58,11 +56,11 @@ const AuthorizedUserInfo = ({ account }: { account: AuthAccountInfo }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UnauthorizedUserInfo = () => {
|
const UnauthorizedUserInfo = () => {
|
||||||
const setOpen = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
const openSignInModal = useCallback(() => {
|
const openSignInModal = useCallback(() => {
|
||||||
setOpen(state => ({ ...state, openModal: true }));
|
globalDialogService.open('sign-in', {});
|
||||||
}, [setOpen]);
|
}, [globalDialogService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
127
packages/frontend/core/src/components/sign-in/add-selfhosted.tsx
Normal file
127
packages/frontend/core/src/components/sign-in/add-selfhosted.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { Button } from '@affine/component';
|
||||||
|
import {
|
||||||
|
AuthContent,
|
||||||
|
AuthInput,
|
||||||
|
ModalHeader,
|
||||||
|
} from '@affine/component/auth-components';
|
||||||
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import { ServersService } from '@affine/core/modules/cloud';
|
||||||
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import type { SignInState } from '.';
|
||||||
|
import * as styles from './style.css';
|
||||||
|
|
||||||
|
function normalizeURL(url: string) {
|
||||||
|
const normalized = new URL(url).toString();
|
||||||
|
return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddSelfhostedStep = ({
|
||||||
|
state,
|
||||||
|
changeState,
|
||||||
|
}: {
|
||||||
|
state: SignInState;
|
||||||
|
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||||
|
}) => {
|
||||||
|
const serversService = useService(ServersService);
|
||||||
|
const [baseURL, setBaseURL] = useState(state.initialServerBaseUrl ?? '');
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const urlValid = useMemo(() => {
|
||||||
|
try {
|
||||||
|
normalizeURL(baseURL);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [baseURL]);
|
||||||
|
|
||||||
|
const onBaseURLChange = useCallback((value: string) => {
|
||||||
|
setBaseURL(value);
|
||||||
|
setError(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onConnect = useAsyncCallback(async () => {
|
||||||
|
setIsConnecting(true);
|
||||||
|
try {
|
||||||
|
const server = await serversService.addOrGetServerByBaseUrl(
|
||||||
|
normalizeURL(baseURL)
|
||||||
|
);
|
||||||
|
changeState(prev => ({
|
||||||
|
...prev,
|
||||||
|
step: 'signIn',
|
||||||
|
server,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConnecting(false);
|
||||||
|
}, [baseURL, changeState, serversService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.initialServerBaseUrl) {
|
||||||
|
changeState(prev => ({
|
||||||
|
...prev,
|
||||||
|
initialServerBaseUrl: undefined,
|
||||||
|
}));
|
||||||
|
onConnect();
|
||||||
|
}
|
||||||
|
}, [changeState, onConnect, state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ModalHeader title={t['com.affine.auth.sign.add-selfhosted']()} />
|
||||||
|
<AuthContent>
|
||||||
|
<AuthInput
|
||||||
|
label={t['com.affine.auth.sign.add-selfhosted.baseurl']()}
|
||||||
|
value={baseURL}
|
||||||
|
onChange={onBaseURLChange}
|
||||||
|
placeholder="https://your-server.com"
|
||||||
|
error={!!error}
|
||||||
|
disabled={isConnecting}
|
||||||
|
errorHint={t['com.affine.auth.sign.add-selfhosted.error']()}
|
||||||
|
onEnter={onConnect}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
data-testid="connect-selfhosted-button"
|
||||||
|
variant="primary"
|
||||||
|
size="extraLarge"
|
||||||
|
style={{ width: '100%', marginTop: '16px' }}
|
||||||
|
disabled={!urlValid || isConnecting}
|
||||||
|
loading={isConnecting}
|
||||||
|
onClick={onConnect}
|
||||||
|
>
|
||||||
|
{t['com.affine.auth.sign.add-selfhosted.connect-button']()}
|
||||||
|
</Button>
|
||||||
|
<div className={styles.authMessage}>
|
||||||
|
<Trans
|
||||||
|
i18nKey="com.affine.auth.sign.add-selfhosted.description"
|
||||||
|
components={{
|
||||||
|
1: (
|
||||||
|
<a
|
||||||
|
href="https://docs.affine.pro/docs/self-host-affine"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AuthContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
62
packages/frontend/core/src/components/sign-in/index.tsx
Normal file
62
packages/frontend/core/src/components/sign-in/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { DefaultServerService, type Server } from '@affine/core/modules/cloud';
|
||||||
|
import { FrameworkScope, useService } from '@toeverything/infra';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { AddSelfhostedStep } from './add-selfhosted';
|
||||||
|
import { SignInStep } from './sign-in';
|
||||||
|
import { SignInWithEmailStep } from './sign-in-with-email';
|
||||||
|
import { SignInWithPasswordStep } from './sign-in-with-password';
|
||||||
|
|
||||||
|
export type SignInStep =
|
||||||
|
| 'signIn'
|
||||||
|
| 'signInWithPassword'
|
||||||
|
| 'signInWithEmail'
|
||||||
|
| 'addSelfhosted';
|
||||||
|
|
||||||
|
export interface SignInState {
|
||||||
|
step: SignInStep;
|
||||||
|
server?: Server;
|
||||||
|
initialServerBaseUrl?: string;
|
||||||
|
email?: string;
|
||||||
|
redirectUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignInPanel = ({
|
||||||
|
onClose,
|
||||||
|
server: initialServerBaseUrl,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
server?: string;
|
||||||
|
}) => {
|
||||||
|
const [state, setState] = useState<SignInState>({
|
||||||
|
step: initialServerBaseUrl ? 'addSelfhosted' : 'signIn',
|
||||||
|
initialServerBaseUrl: initialServerBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultServerService = useService(DefaultServerService);
|
||||||
|
|
||||||
|
const step = state.step;
|
||||||
|
const server = state.server ?? defaultServerService.server;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FrameworkScope scope={server.scope}>
|
||||||
|
{step === 'signIn' ? (
|
||||||
|
<SignInStep state={state} changeState={setState} close={onClose} />
|
||||||
|
) : step === 'signInWithEmail' ? (
|
||||||
|
<SignInWithEmailStep
|
||||||
|
state={state}
|
||||||
|
changeState={setState}
|
||||||
|
close={onClose}
|
||||||
|
/>
|
||||||
|
) : step === 'signInWithPassword' ? (
|
||||||
|
<SignInWithPasswordStep
|
||||||
|
state={state}
|
||||||
|
changeState={setState}
|
||||||
|
close={onClose}
|
||||||
|
/>
|
||||||
|
) : step === 'addSelfhosted' ? (
|
||||||
|
<AddSelfhostedStep state={state} changeState={setState} />
|
||||||
|
) : null}
|
||||||
|
</FrameworkScope>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,21 +8,38 @@ import {
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { AuthService, CaptchaService } from '@affine/core/modules/cloud';
|
import { AuthService, CaptchaService } from '@affine/core/modules/cloud';
|
||||||
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import type { AuthPanelProps } from './index';
|
import type { SignInState } from '.';
|
||||||
|
import { Captcha } from './captcha';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import { Captcha } from './use-captcha';
|
|
||||||
|
|
||||||
export const AfterSignInSendEmail = ({
|
export const SignInWithEmailStep = ({
|
||||||
setAuthData: setAuth,
|
state,
|
||||||
email,
|
changeState,
|
||||||
redirectUrl,
|
close,
|
||||||
}: AuthPanelProps<'afterSignInSendEmail'>) => {
|
}: {
|
||||||
|
state: SignInState;
|
||||||
|
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||||
|
close: () => void;
|
||||||
|
}) => {
|
||||||
const [resendCountDown, setResendCountDown] = useState(60);
|
const [resendCountDown, setResendCountDown] = useState(60);
|
||||||
|
|
||||||
|
const email = state.email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new Unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setResendCountDown(c => Math.max(c - 1, 0));
|
setResendCountDown(c => Math.max(c - 1, 0));
|
||||||
@@ -43,7 +60,20 @@ export const AfterSignInSendEmail = ({
|
|||||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
||||||
const challenge = useLiveData(captchaService.challenge$);
|
const challenge = useLiveData(captchaService.challenge$);
|
||||||
|
|
||||||
|
const loginStatus = useLiveData(authService.session.status$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginStatus === 'authenticated') {
|
||||||
|
close();
|
||||||
|
notify.success({
|
||||||
|
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||||
|
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [close, loginStatus, t]);
|
||||||
|
|
||||||
const onResendClick = useAsyncCallback(async () => {
|
const onResendClick = useAsyncCallback(async () => {
|
||||||
|
if (isSending || (!verifyToken && needCaptcha)) return;
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
try {
|
try {
|
||||||
setResendCountDown(60);
|
setResendCountDown(60);
|
||||||
@@ -52,7 +82,7 @@ export const AfterSignInSendEmail = ({
|
|||||||
email,
|
email,
|
||||||
verifyToken,
|
verifyToken,
|
||||||
challenge,
|
challenge,
|
||||||
redirectUrl
|
state.redirectUrl
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -61,17 +91,34 @@ export const AfterSignInSendEmail = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsSending(false);
|
setIsSending(false);
|
||||||
}, [authService, captchaService, challenge, email, redirectUrl, verifyToken]);
|
}, [
|
||||||
|
authService,
|
||||||
|
captchaService,
|
||||||
|
challenge,
|
||||||
|
email,
|
||||||
|
isSending,
|
||||||
|
needCaptcha,
|
||||||
|
state.redirectUrl,
|
||||||
|
verifyToken,
|
||||||
|
]);
|
||||||
|
|
||||||
const onSignInWithPasswordClick = useCallback(() => {
|
const onSignInWithPasswordClick = useCallback(() => {
|
||||||
setAuth({ state: 'signInWithPassword' });
|
changeState(prev => ({ ...prev, step: 'signInWithPassword' }));
|
||||||
}, [setAuth]);
|
}, [changeState]);
|
||||||
|
|
||||||
const onBackBottomClick = useCallback(() => {
|
const onBackBottomClick = useCallback(() => {
|
||||||
setAuth({ state: 'signIn' });
|
changeState(prev => ({ ...prev, step: 'signIn' }));
|
||||||
}, [setAuth]);
|
}, [changeState]);
|
||||||
|
|
||||||
return (
|
return !verifyToken && needCaptcha ? (
|
||||||
|
<>
|
||||||
|
<ModalHeader title={t['com.affine.auth.sign.in']()} />
|
||||||
|
<AuthContent style={{ height: 100 }}>
|
||||||
|
<Captcha />
|
||||||
|
</AuthContent>
|
||||||
|
<BackButton onClick={onBackBottomClick} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
title={t['com.affine.auth.sign.in']()}
|
title={t['com.affine.auth.sign.in']()}
|
||||||
@@ -88,20 +135,14 @@ export const AfterSignInSendEmail = ({
|
|||||||
|
|
||||||
<div className={style.resendWrapper}>
|
<div className={style.resendWrapper}>
|
||||||
{resendCountDown <= 0 ? (
|
{resendCountDown <= 0 ? (
|
||||||
<>
|
|
||||||
<Captcha />
|
|
||||||
<Button
|
<Button
|
||||||
style={
|
disabled={isSending}
|
||||||
!verifyToken && needCaptcha ? { cursor: 'not-allowed' } : {}
|
|
||||||
}
|
|
||||||
disabled={(!verifyToken && needCaptcha) || isSending}
|
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={onResendClick}
|
onClick={onResendClick}
|
||||||
>
|
>
|
||||||
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
{t['com.affine.auth.sign.auth.code.resend.hint']()}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={style.sentRow}>
|
<div className={style.sentRow}>
|
||||||
<div className={style.sentMessage}>
|
<div className={style.sentMessage}>
|
||||||
@@ -6,27 +6,52 @@ import {
|
|||||||
} from '@affine/component/auth-components';
|
} from '@affine/component/auth-components';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { AuthService, CaptchaService } from '@affine/core/modules/cloud';
|
import {
|
||||||
|
AuthService,
|
||||||
|
CaptchaService,
|
||||||
|
ServerService,
|
||||||
|
} from '@affine/core/modules/cloud';
|
||||||
|
import { Unreachable } from '@affine/env/constant';
|
||||||
|
import { ServerDeploymentType } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import type { FC } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { AuthPanelProps } from './index';
|
import type { SignInState } from '.';
|
||||||
|
import { Captcha } from './captcha';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
import { Captcha } from './use-captcha';
|
|
||||||
|
|
||||||
export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
export const SignInWithPasswordStep = ({
|
||||||
setAuthData,
|
state,
|
||||||
email,
|
changeState,
|
||||||
redirectUrl,
|
close,
|
||||||
|
}: {
|
||||||
|
state: SignInState;
|
||||||
|
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||||
|
close: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
|
const email = state.email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new Unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [passwordError, setPasswordError] = useState(false);
|
const [passwordError, setPasswordError] = useState(false);
|
||||||
const captchaService = useService(CaptchaService);
|
const captchaService = useService(CaptchaService);
|
||||||
|
const serverService = useService(ServerService);
|
||||||
|
const isSelfhosted = useLiveData(
|
||||||
|
serverService.server.config$.selector(
|
||||||
|
c => c.type === ServerDeploymentType.Selfhosted
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const serverName = useLiveData(
|
||||||
|
serverService.server.config$.selector(c => c.serverName)
|
||||||
|
);
|
||||||
|
|
||||||
const verifyToken = useLiveData(captchaService.verifyToken$);
|
const verifyToken = useLiveData(captchaService.verifyToken$);
|
||||||
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
const needCaptcha = useLiveData(captchaService.needCaptcha$);
|
||||||
@@ -34,8 +59,20 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [sendingEmail, setSendingEmail] = useState(false);
|
const [sendingEmail, setSendingEmail] = useState(false);
|
||||||
|
|
||||||
|
const loginStatus = useLiveData(authService.session.status$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginStatus === 'authenticated') {
|
||||||
|
close();
|
||||||
|
notify.success({
|
||||||
|
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||||
|
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [close, loginStatus, t]);
|
||||||
|
|
||||||
const onSignIn = useAsyncCallback(async () => {
|
const onSignIn = useAsyncCallback(async () => {
|
||||||
if (isLoading) return;
|
if (isLoading || (!verifyToken && needCaptcha)) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -55,6 +92,7 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
|||||||
}, [
|
}, [
|
||||||
isLoading,
|
isLoading,
|
||||||
verifyToken,
|
verifyToken,
|
||||||
|
needCaptcha,
|
||||||
captchaService,
|
captchaService,
|
||||||
authService,
|
authService,
|
||||||
email,
|
email,
|
||||||
@@ -66,14 +104,7 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
|||||||
if (sendingEmail) return;
|
if (sendingEmail) return;
|
||||||
setSendingEmail(true);
|
setSendingEmail(true);
|
||||||
try {
|
try {
|
||||||
captchaService.revalidate();
|
changeState(prev => ({ ...prev, step: 'signInWithEmail' }));
|
||||||
await authService.sendEmailMagicLink(
|
|
||||||
email,
|
|
||||||
verifyToken,
|
|
||||||
challenge,
|
|
||||||
redirectUrl
|
|
||||||
);
|
|
||||||
setAuthData({ state: 'afterSignInSendEmail' });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
notify.error({
|
notify.error({
|
||||||
@@ -82,26 +113,13 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
|||||||
// TODO(@eyhn): handle error better
|
// TODO(@eyhn): handle error better
|
||||||
}
|
}
|
||||||
setSendingEmail(false);
|
setSendingEmail(false);
|
||||||
}, [
|
}, [sendingEmail, changeState]);
|
||||||
sendingEmail,
|
|
||||||
verifyToken,
|
|
||||||
captchaService,
|
|
||||||
authService,
|
|
||||||
email,
|
|
||||||
challenge,
|
|
||||||
redirectUrl,
|
|
||||||
setAuthData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const sendChangePasswordEmail = useCallback(() => {
|
|
||||||
setAuthData({ state: 'sendEmail', emailType: 'changePassword' });
|
|
||||||
}, [setAuthData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
title={t['com.affine.auth.sign.in']()}
|
title={t['com.affine.auth.sign.in']()}
|
||||||
subTitle={t['com.affine.brand.affineCloud']()}
|
subTitle={serverName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Wrapper
|
<Wrapper
|
||||||
@@ -128,23 +146,8 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
|||||||
errorHint={t['com.affine.auth.password.error']()}
|
errorHint={t['com.affine.auth.password.error']()}
|
||||||
onEnter={onSignIn}
|
onEnter={onSignIn}
|
||||||
/>
|
/>
|
||||||
<div
|
{!isSelfhosted && (
|
||||||
className={styles.forgetPasswordButtonRow}
|
<div className={styles.passwordButtonRow}>
|
||||||
style={{ display: 'none' }} // Not implemented yet.
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className={styles.linkButton}
|
|
||||||
onClick={sendChangePasswordEmail}
|
|
||||||
style={{
|
|
||||||
color: 'var(--affine-text-secondary-color)',
|
|
||||||
fontSize: 'var(--affine-font-sm)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t['com.affine.auth.forget']()}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{(verifyToken || !needCaptcha) && (
|
|
||||||
<div className={styles.sendMagicLinkButtonRow}>
|
|
||||||
<a
|
<a
|
||||||
data-testid="send-magic-link-button"
|
data-testid="send-magic-link-button"
|
||||||
className={styles.linkButton}
|
className={styles.linkButton}
|
||||||
@@ -168,8 +171,8 @@ export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
|||||||
</Wrapper>
|
</Wrapper>
|
||||||
<BackButton
|
<BackButton
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
setAuthData({ state: 'signIn' });
|
changeState(prev => ({ ...prev, step: 'signIn' }));
|
||||||
}, [setAuthData])}
|
}, [changeState])}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
221
packages/frontend/core/src/components/sign-in/sign-in.tsx
Normal file
221
packages/frontend/core/src/components/sign-in/sign-in.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import { Button, notify } from '@affine/component';
|
||||||
|
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||||
|
import { OAuth } from '@affine/core/components/affine/auth/oauth';
|
||||||
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import { AuthService, ServerService } from '@affine/core/modules/cloud';
|
||||||
|
import { ServerDeploymentType } from '@affine/graphql';
|
||||||
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
|
import { ArrowRightBigIcon, PublishIcon } from '@blocksuite/icons/rc';
|
||||||
|
import {
|
||||||
|
FeatureFlagService,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import type { SignInState } from '.';
|
||||||
|
import * as style from './style.css';
|
||||||
|
|
||||||
|
const emailRegex =
|
||||||
|
/^(?:(?:[^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|((?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
|
function validateEmail(email: string) {
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignInStep = ({
|
||||||
|
state,
|
||||||
|
changeState,
|
||||||
|
close,
|
||||||
|
}: {
|
||||||
|
state: SignInState;
|
||||||
|
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||||
|
close: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const serverService = useService(ServerService);
|
||||||
|
const serverName = useLiveData(
|
||||||
|
serverService.server.config$.selector(c => c.serverName)
|
||||||
|
);
|
||||||
|
const isSelfhosted = useLiveData(
|
||||||
|
serverService.server.config$.selector(
|
||||||
|
c => c.type === ServerDeploymentType.Selfhosted
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
const featureFlagService = useService(FeatureFlagService);
|
||||||
|
const enableMultipleCloudServers = useLiveData(
|
||||||
|
featureFlagService.flags.enable_multiple_cloud_servers.$
|
||||||
|
);
|
||||||
|
const [isMutating, setIsMutating] = useState(false);
|
||||||
|
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
|
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||||
|
|
||||||
|
const loginStatus = useLiveData(authService.session.status$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginStatus === 'authenticated') {
|
||||||
|
close();
|
||||||
|
notify.success({
|
||||||
|
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||||
|
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [close, loginStatus, t]);
|
||||||
|
|
||||||
|
const onContinue = useAsyncCallback(async () => {
|
||||||
|
if (!validateEmail(email)) {
|
||||||
|
setIsValidEmail(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsValidEmail(true);
|
||||||
|
setIsMutating(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { hasPassword, registered, magicLink } =
|
||||||
|
await authService.checkUserByEmail(email);
|
||||||
|
|
||||||
|
if (registered) {
|
||||||
|
// provider password sign-in if user has by default
|
||||||
|
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||||
|
if (hasPassword) {
|
||||||
|
changeState(prev => ({
|
||||||
|
...prev,
|
||||||
|
email,
|
||||||
|
step: 'signInWithPassword',
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
if (magicLink) {
|
||||||
|
changeState(prev => ({
|
||||||
|
...prev,
|
||||||
|
email,
|
||||||
|
step: 'signInWithEmail',
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
notify.error({
|
||||||
|
title: 'Failed to send email. Please contact the administrator.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (magicLink) {
|
||||||
|
changeState(prev => ({
|
||||||
|
...prev,
|
||||||
|
email,
|
||||||
|
step: 'signInWithEmail',
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
notify.error({
|
||||||
|
title: 'Failed to send email. Please contact the administrator.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
// TODO(@eyhn): better error handling
|
||||||
|
notify.error({
|
||||||
|
title: 'Failed to send email. Please try again.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsMutating(false);
|
||||||
|
}, [authService, changeState, email]);
|
||||||
|
|
||||||
|
const onAddSelfhosted = useCallback(() => {
|
||||||
|
changeState(prev => ({
|
||||||
|
...prev,
|
||||||
|
step: 'addSelfhosted',
|
||||||
|
}));
|
||||||
|
}, [changeState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ModalHeader
|
||||||
|
title={t['com.affine.auth.sign.in']()}
|
||||||
|
subTitle={serverName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OAuth redirectUrl={state.redirectUrl} />
|
||||||
|
|
||||||
|
<div className={style.authModalContent}>
|
||||||
|
<AuthInput
|
||||||
|
label={t['com.affine.settings.email']()}
|
||||||
|
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||||
|
onChange={setEmail}
|
||||||
|
error={!isValidEmail}
|
||||||
|
errorHint={
|
||||||
|
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
||||||
|
}
|
||||||
|
onEnter={onContinue}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
size="extraLarge"
|
||||||
|
data-testid="continue-login-button"
|
||||||
|
block
|
||||||
|
loading={isMutating}
|
||||||
|
suffix={<ArrowRightBigIcon />}
|
||||||
|
suffixStyle={{ width: 20, height: 20, color: cssVar('blue') }}
|
||||||
|
onClick={onContinue}
|
||||||
|
>
|
||||||
|
{t['com.affine.auth.sign.email.continue']()}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!isSelfhosted && (
|
||||||
|
<div className={style.authMessage}>
|
||||||
|
{/*prettier-ignore*/}
|
||||||
|
<Trans i18nKey="com.affine.auth.sign.message">
|
||||||
|
By clicking "Continue with Google/Email" above, you acknowledge that
|
||||||
|
you agree to AFFiNE's <a href="https://affine.pro/terms" target="_blank" rel="noreferrer">Terms of Conditions</a> and <a href="https://affine.pro/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>.
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={style.skipDivider}>
|
||||||
|
<div className={style.skipDividerLine} />
|
||||||
|
<span className={style.skipDividerText}>or</span>
|
||||||
|
<div className={style.skipDividerLine} />
|
||||||
|
</div>
|
||||||
|
<div className={style.skipSection}>
|
||||||
|
{!isSelfhosted &&
|
||||||
|
BUILD_CONFIG.isElectron &&
|
||||||
|
enableMultipleCloudServers && (
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
className={style.addSelfhostedButton}
|
||||||
|
prefix={
|
||||||
|
<PublishIcon className={style.addSelfhostedButtonPrefix} />
|
||||||
|
}
|
||||||
|
onClick={onAddSelfhosted}
|
||||||
|
>
|
||||||
|
{t['com.affine.auth.sign.add-selfhosted']()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className={style.skipText}>
|
||||||
|
{t['com.affine.mobile.sign-in.skip.hint']()}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => close()}
|
||||||
|
className={style.skipLink}
|
||||||
|
suffix={<ArrowRightBigIcon className={style.skipLinkIcon} />}
|
||||||
|
>
|
||||||
|
{t['com.affine.mobile.sign-in.skip.link']()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
121
packages/frontend/core/src/components/sign-in/style.css.ts
Normal file
121
packages/frontend/core/src/components/sign-in/style.css.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const authModalContent = style({
|
||||||
|
marginTop: '30px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const authMessage = style({
|
||||||
|
marginTop: '30px',
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
lineHeight: 1.5,
|
||||||
|
});
|
||||||
|
globalStyle(`${authMessage} a`, {
|
||||||
|
color: cssVar('linkColor'),
|
||||||
|
});
|
||||||
|
globalStyle(`${authMessage} .link`, {
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: cssVar('linkColor'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const captchaWrapper = style({
|
||||||
|
margin: 'auto',
|
||||||
|
marginBottom: '4px',
|
||||||
|
textAlign: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const passwordButtonRow = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '30px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const linkButton = style({
|
||||||
|
color: cssVar('linkColor'),
|
||||||
|
background: 'transparent',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
lineHeight: '22px',
|
||||||
|
userSelect: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resendWrapper = style({
|
||||||
|
height: 77,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sentRow = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
lineHeight: '22px',
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sentMessage = style({
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
fontWeight: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resendCountdown = style({
|
||||||
|
width: 45,
|
||||||
|
textAlign: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addSelfhostedButton = style({
|
||||||
|
marginTop: 10,
|
||||||
|
marginLeft: -5,
|
||||||
|
marginBottom: 16,
|
||||||
|
color: cssVarV2('text/link'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addSelfhostedButtonPrefix = style({
|
||||||
|
color: cssVarV2('text/link'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipDivider = style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 20,
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipDividerLine = style({
|
||||||
|
flex: 1,
|
||||||
|
height: 0,
|
||||||
|
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipDividerText = style({
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipText = style({
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
fontWeight: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipLink = style({
|
||||||
|
color: cssVarV2('text/link'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipLinkIcon = style({
|
||||||
|
color: cssVarV2('text/link'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipSection = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { BrowserWarning, LocalDemoTips } from '@affine/component/affine-banner';
|
import { BrowserWarning, LocalDemoTips } from '@affine/component/affine-banner';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useEnableCloud } from '../components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '../components/hooks/affine/use-enable-cloud';
|
||||||
import { AuthService } from '../modules/cloud';
|
import { AuthService } from '../modules/cloud';
|
||||||
import { authAtom } from './atoms';
|
import { GlobalDialogService } from '../modules/dialogs';
|
||||||
|
|
||||||
const minimumChromeVersion = 106;
|
const minimumChromeVersion = 106;
|
||||||
|
|
||||||
@@ -69,15 +67,15 @@ export const TopTip = ({
|
|||||||
const [showLocalDemoTips, setShowLocalDemoTips] = useState(true);
|
const [showLocalDemoTips, setShowLocalDemoTips] = useState(true);
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
const setAuthModal = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
const onLogin = useCallback(() => {
|
const onLogin = useCallback(() => {
|
||||||
setAuthModal({ openModal: true, state: 'signIn' });
|
globalDialogService.open('sign-in', {});
|
||||||
}, [setAuthModal]);
|
}, [globalDialogService]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!BUILD_CONFIG.isElectron &&
|
!BUILD_CONFIG.isElectron &&
|
||||||
showLocalDemoTips &&
|
showLocalDemoTips &&
|
||||||
workspace.flavour === WorkspaceFlavour.LOCAL
|
workspace.flavour === 'local'
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<LocalDemoTips
|
<LocalDemoTips
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { MenuItem } from '@affine/component/ui/menu';
|
import { MenuItem } from '@affine/component/ui/menu';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -13,7 +12,6 @@ import {
|
|||||||
type WorkspaceMetadata,
|
type WorkspaceMetadata,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useCatchEventCallback } from '../../hooks/use-catch-event-hook';
|
import { useCatchEventCallback } from '../../hooks/use-catch-event-hook';
|
||||||
@@ -23,17 +21,14 @@ import { UserAccountItem } from './user-account';
|
|||||||
import { AFFiNEWorkspaceList } from './workspace-list';
|
import { AFFiNEWorkspaceList } from './workspace-list';
|
||||||
|
|
||||||
export const SignInItem = () => {
|
export const SignInItem = () => {
|
||||||
const setOpen = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const onClickSignIn = useCallback(() => {
|
const onClickSignIn = useCallback(() => {
|
||||||
track.$.navigationPanel.workspaceList.requestSignIn();
|
track.$.navigationPanel.workspaceList.requestSignIn();
|
||||||
setOpen(state => ({
|
globalDialogService.open('sign-in', {});
|
||||||
...state,
|
}, [globalDialogService]);
|
||||||
openModal: true,
|
|
||||||
}));
|
|
||||||
}, [setOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -83,14 +78,9 @@ const UserWithWorkspaceListInner = ({
|
|||||||
|
|
||||||
const isAuthenticated = session.status === 'authenticated';
|
const isAuthenticated = session.status === 'authenticated';
|
||||||
|
|
||||||
const setOpenSignIn = useSetAtom(authAtom);
|
|
||||||
|
|
||||||
const openSignInModal = useCallback(() => {
|
const openSignInModal = useCallback(() => {
|
||||||
setOpenSignIn(state => ({
|
globalDialogService.open('sign-in', {});
|
||||||
...state,
|
}, [globalDialogService]);
|
||||||
openModal: true,
|
|
||||||
}));
|
|
||||||
}, [setOpenSignIn]);
|
|
||||||
|
|
||||||
const onNewWorkspace = useCallback(() => {
|
const onNewWorkspace = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ export const workspaceListWrapper = style({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
});
|
});
|
||||||
export const workspaceType = style({
|
export const workspaceServer = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
padding: '0px 12px',
|
padding: '0px 12px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workspaceServerName = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import { ScrollableContainer } from '@affine/component';
|
import {
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
ScrollableContainer,
|
||||||
|
} from '@affine/component';
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||||
|
import type { Server } from '@affine/core/modules/cloud';
|
||||||
|
import { AuthService, ServersService } from '@affine/core/modules/cloud';
|
||||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons/rc';
|
import {
|
||||||
|
CloudWorkspaceIcon,
|
||||||
|
LocalWorkspaceIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from '@blocksuite/icons/rc';
|
||||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
|
FrameworkScope,
|
||||||
|
GlobalContextService,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
useServiceOptional,
|
useServiceOptional,
|
||||||
@@ -29,28 +42,97 @@ interface WorkspaceModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CloudWorkSpaceList = ({
|
const CloudWorkSpaceList = ({
|
||||||
|
server,
|
||||||
workspaces,
|
workspaces,
|
||||||
onClickWorkspace,
|
onClickWorkspace,
|
||||||
onClickWorkspaceSetting,
|
onClickWorkspaceSetting,
|
||||||
}: Omit<WorkspaceModalProps, 'onNewWorkspace' | 'onAddWorkspace'>) => {
|
onClickEnableCloud,
|
||||||
const t = useI18n();
|
}: {
|
||||||
if (workspaces.length === 0) {
|
server: Server;
|
||||||
return null;
|
workspaces: WorkspaceMetadata[];
|
||||||
|
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
|
onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
|
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
||||||
|
}) => {
|
||||||
|
const globalContextService = useService(GlobalContextService);
|
||||||
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
const serversService = useService(ServersService);
|
||||||
|
const account = useLiveData(authService.session.account$);
|
||||||
|
const accountStatus = useLiveData(authService.session.status$);
|
||||||
|
const navigateHelper = useNavigateHelper();
|
||||||
|
|
||||||
|
const currentWorkspaceFlavour = useLiveData(
|
||||||
|
globalContextService.globalContext.workspaceFlavour.$
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteServer = useCallback(() => {
|
||||||
|
serversService.removeServer(server.id);
|
||||||
|
|
||||||
|
if (currentWorkspaceFlavour === server.id) {
|
||||||
|
const otherWorkspace = workspaces.find(w => w.flavour !== server.id);
|
||||||
|
if (otherWorkspace) {
|
||||||
|
navigateHelper.openPage(otherWorkspace.id, 'all');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentWorkspaceFlavour,
|
||||||
|
navigateHelper,
|
||||||
|
server.id,
|
||||||
|
serversService,
|
||||||
|
workspaces,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleSignOut = useAsyncCallback(async () => {
|
||||||
|
await authService.signOut();
|
||||||
|
}, [authService]);
|
||||||
|
|
||||||
|
const handleSignIn = useAsyncCallback(async () => {
|
||||||
|
globalDialogService.open('sign-in', {
|
||||||
|
server: server.baseUrl,
|
||||||
|
});
|
||||||
|
}, [globalDialogService, server.baseUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.workspaceListWrapper}>
|
<div className={styles.workspaceListWrapper}>
|
||||||
<div className={styles.workspaceType}>
|
<div className={styles.workspaceServer}>
|
||||||
|
<div className={styles.workspaceServerName}>
|
||||||
<CloudWorkspaceIcon
|
<CloudWorkspaceIcon
|
||||||
width={14}
|
width={14}
|
||||||
height={14}
|
height={14}
|
||||||
className={styles.workspaceTypeIcon}
|
className={styles.workspaceTypeIcon}
|
||||||
/>
|
/>
|
||||||
{t['com.affine.workspaceList.workspaceListType.cloud']()}
|
{serverName} -
|
||||||
|
{account ? account.email : 'Not signed in'}
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
items={[
|
||||||
|
server.id !== 'affine-cloud' && (
|
||||||
|
<MenuItem key="delete-server" onClick={handleDeleteServer}>
|
||||||
|
Delete Server
|
||||||
|
</MenuItem>
|
||||||
|
),
|
||||||
|
accountStatus === 'authenticated' && (
|
||||||
|
<MenuItem key="sign-out" onClick={handleSignOut}>
|
||||||
|
Sign Out
|
||||||
|
</MenuItem>
|
||||||
|
),
|
||||||
|
accountStatus === 'unauthenticated' && (
|
||||||
|
<MenuItem key="sign-in" onClick={handleSignIn}>
|
||||||
|
Sign In
|
||||||
|
</MenuItem>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<IconButton icon={<MoreHorizontalIcon />} />
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
items={workspaces}
|
items={workspaces}
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
onSettingClick={onClickWorkspaceSetting}
|
onSettingClick={onClickWorkspaceSetting}
|
||||||
|
onEnableCloudClick={onClickEnableCloud}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -68,7 +150,8 @@ const LocalWorkspaces = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={styles.workspaceListWrapper}>
|
<div className={styles.workspaceListWrapper}>
|
||||||
<div className={styles.workspaceType}>
|
<div className={styles.workspaceServer}>
|
||||||
|
<div className={styles.workspaceServerName}>
|
||||||
<LocalWorkspaceIcon
|
<LocalWorkspaceIcon
|
||||||
width={14}
|
width={14}
|
||||||
height={14}
|
height={14}
|
||||||
@@ -76,6 +159,7 @@ const LocalWorkspaces = ({
|
|||||||
/>
|
/>
|
||||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
items={workspaces}
|
items={workspaces}
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
@@ -103,15 +187,13 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
|
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
const session = useService(AuthService).session;
|
const serversService = useService(ServersService);
|
||||||
const status = useLiveData(session.status$);
|
const servers = useLiveData(serversService.servers$);
|
||||||
|
|
||||||
const isAuthenticated = status === 'authenticated';
|
|
||||||
|
|
||||||
const cloudWorkspaces = useMemo(
|
const cloudWorkspaces = useMemo(
|
||||||
() =>
|
() =>
|
||||||
workspaces.filter(
|
workspaces.filter(
|
||||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
({ flavour }) => flavour !== 'local'
|
||||||
) as WorkspaceMetadata[],
|
) as WorkspaceMetadata[],
|
||||||
[workspaces]
|
[workspaces]
|
||||||
);
|
);
|
||||||
@@ -119,7 +201,7 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
const localWorkspaces = useMemo(
|
const localWorkspaces = useMemo(
|
||||||
() =>
|
() =>
|
||||||
workspaces.filter(
|
workspaces.filter(
|
||||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
({ flavour }) => flavour === 'local'
|
||||||
) as WorkspaceMetadata[],
|
) as WorkspaceMetadata[],
|
||||||
[workspaces]
|
[workspaces]
|
||||||
);
|
);
|
||||||
@@ -160,20 +242,23 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
className={styles.workspaceListsWrapper}
|
className={styles.workspaceListsWrapper}
|
||||||
scrollBarClassName={styles.scrollbar}
|
scrollBarClassName={styles.scrollbar}
|
||||||
>
|
>
|
||||||
{isAuthenticated ? (
|
|
||||||
<div>
|
<div>
|
||||||
|
{servers.map(server => (
|
||||||
|
<FrameworkScope key={server.id} scope={server.scope}>
|
||||||
<CloudWorkSpaceList
|
<CloudWorkSpaceList
|
||||||
workspaces={cloudWorkspaces}
|
server={server}
|
||||||
|
workspaces={cloudWorkspaces.filter(
|
||||||
|
({ flavour }) => flavour === server.id
|
||||||
|
)}
|
||||||
onClickWorkspace={handleClickWorkspace}
|
onClickWorkspace={handleClickWorkspace}
|
||||||
onClickWorkspaceSetting={
|
onClickWorkspaceSetting={
|
||||||
showSettingsButton ? onClickWorkspaceSetting : undefined
|
showSettingsButton ? onClickWorkspaceSetting : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
) : null}
|
</FrameworkScope>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
<LocalWorkspaces
|
<LocalWorkspaces
|
||||||
workspaces={localWorkspaces}
|
workspaces={localWorkspaces}
|
||||||
onClickWorkspace={handleClickWorkspace}
|
onClickWorkspace={handleClickWorkspace}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useSystemOnline } from '@affine/core/components/hooks/use-system-online
|
|||||||
import { useWorkspace } from '@affine/core/components/hooks/use-workspace';
|
import { useWorkspace } from '@affine/core/components/hooks/use-workspace';
|
||||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import {
|
import {
|
||||||
ArrowDownSmallIcon,
|
ArrowDownSmallIcon,
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
@@ -97,7 +96,7 @@ const useSyncEngineSyncProgress = (meta: WorkspaceMetadata) => {
|
|||||||
|
|
||||||
let content;
|
let content;
|
||||||
// TODO(@eyhn): add i18n
|
// TODO(@eyhn): add i18n
|
||||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour === 'local') {
|
||||||
if (!BUILD_CONFIG.isElectron) {
|
if (!BUILD_CONFIG.isElectron) {
|
||||||
content = 'This is a local demo workspace.';
|
content = 'This is a local demo workspace.';
|
||||||
} else {
|
} else {
|
||||||
@@ -132,7 +131,7 @@ const useSyncEngineSyncProgress = (meta: WorkspaceMetadata) => {
|
|||||||
return {
|
return {
|
||||||
message: content,
|
message: content,
|
||||||
icon:
|
icon:
|
||||||
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
workspace.flavour !== 'local' ? (
|
||||||
!isOnline ? (
|
!isOnline ? (
|
||||||
<OfflineStatus />
|
<OfflineStatus />
|
||||||
) : (
|
) : (
|
||||||
@@ -143,7 +142,7 @@ const useSyncEngineSyncProgress = (meta: WorkspaceMetadata) => {
|
|||||||
),
|
),
|
||||||
progress,
|
progress,
|
||||||
active:
|
active:
|
||||||
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
|
workspace.flavour !== 'local' &&
|
||||||
((syncing && progress !== undefined) || engineState.retrying), // active if syncing or retrying,
|
((syncing && progress !== undefined) || engineState.retrying), // active if syncing or retrying,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -173,7 +172,7 @@ const WorkspaceSyncInfo = ({
|
|||||||
workspaceProfile: WorkspaceProfileInfo;
|
workspaceProfile: WorkspaceProfileInfo;
|
||||||
}) => {
|
}) => {
|
||||||
const syncStatus = useSyncEngineSyncProgress(workspaceMetadata);
|
const syncStatus = useSyncEngineSyncProgress(workspaceMetadata);
|
||||||
const isCloud = workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
const isCloud = workspaceMetadata.flavour !== 'local';
|
||||||
const { paused, pause } = usePauseAnimation();
|
const { paused, pause } = usePauseAnimation();
|
||||||
|
|
||||||
// to make sure that animation will play first time
|
// to make sure that animation will play first time
|
||||||
@@ -315,8 +314,7 @@ export const WorkspaceCard = forwardRef<
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.showOnCardHover}>
|
<div className={styles.showOnCardHover}>
|
||||||
{onClickEnableCloud &&
|
{onClickEnableCloud && workspaceMetadata.flavour === 'local' ? (
|
||||||
workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
|
|
||||||
<Button
|
<Button
|
||||||
className={styles.enableCloudButton}
|
className={styles.enableCloudButton}
|
||||||
onClick={onEnableCloud}
|
onClick={onEnableCloud}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { Button, Modal, notify, Wrapper } from '@affine/component';
|
||||||
|
import {
|
||||||
|
AuthContent,
|
||||||
|
AuthInput,
|
||||||
|
ModalHeader,
|
||||||
|
} from '@affine/component/auth-components';
|
||||||
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import {
|
||||||
|
AuthService,
|
||||||
|
DefaultServerService,
|
||||||
|
ServersService,
|
||||||
|
} from '@affine/core/modules/cloud';
|
||||||
|
import type {
|
||||||
|
DialogComponentProps,
|
||||||
|
GLOBAL_DIALOG_SCHEMA,
|
||||||
|
} from '@affine/core/modules/dialogs';
|
||||||
|
import { Unreachable } from '@affine/env/constant';
|
||||||
|
import {
|
||||||
|
sendChangePasswordEmailMutation,
|
||||||
|
sendSetPasswordEmailMutation,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const ChangePasswordDialog = ({
|
||||||
|
close,
|
||||||
|
server: serverBaseUrl,
|
||||||
|
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['change-password']>) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const defaultServerService = useService(DefaultServerService);
|
||||||
|
const serversService = useService(ServersService);
|
||||||
|
let server;
|
||||||
|
|
||||||
|
if (serverBaseUrl) {
|
||||||
|
server = serversService.getServerByBaseUrl(serverBaseUrl);
|
||||||
|
if (!server) {
|
||||||
|
throw new Unreachable('Server not found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
server = defaultServerService.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authService = server.scope.get(AuthService);
|
||||||
|
const account = useLiveData(authService.session.account$);
|
||||||
|
const email = account?.email;
|
||||||
|
const hasPassword = account?.info?.hasPassword;
|
||||||
|
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const passwordLimits = useLiveData(
|
||||||
|
server.credentialsRequirement$.map(r => r?.password)
|
||||||
|
);
|
||||||
|
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
// should not happen
|
||||||
|
throw new Unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSendEmail = useAsyncCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (hasPassword) {
|
||||||
|
await server.gql({
|
||||||
|
query: sendChangePasswordEmailMutation,
|
||||||
|
variables: {
|
||||||
|
callbackUrl: `/auth/changePassword`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await server.gql({
|
||||||
|
query: sendSetPasswordEmailMutation,
|
||||||
|
variables: {
|
||||||
|
callbackUrl: `/auth/setPassword`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
notify.success({
|
||||||
|
title: hasPassword
|
||||||
|
? t['com.affine.auth.sent.change.password.hint']()
|
||||||
|
: t['com.affine.auth.sent.set.password.hint'](),
|
||||||
|
});
|
||||||
|
setHasSentEmail(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
notify.error({
|
||||||
|
title: t['com.affine.auth.sent.change.email.fail'](),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [hasPassword, server, t]);
|
||||||
|
|
||||||
|
if (!passwordLimits) {
|
||||||
|
// TODO(@eyhn): loading & error UI
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open
|
||||||
|
onOpenChange={() => close()}
|
||||||
|
width={400}
|
||||||
|
minHeight={500}
|
||||||
|
contentOptions={{
|
||||||
|
['data-testid' as string]: 'change-password-modal',
|
||||||
|
style: { padding: '44px 40px 20px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={serverName}
|
||||||
|
subTitle={
|
||||||
|
hasPassword
|
||||||
|
? t['com.affine.auth.reset.password']()
|
||||||
|
: t['com.affine.auth.set.password']()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AuthContent>
|
||||||
|
{hasPassword
|
||||||
|
? t['com.affine.auth.reset.password.message']()
|
||||||
|
: t['com.affine.auth.set.password.message']({
|
||||||
|
min: String(passwordLimits.minLength),
|
||||||
|
max: String(passwordLimits.maxLength),
|
||||||
|
})}
|
||||||
|
</AuthContent>
|
||||||
|
|
||||||
|
<Wrapper
|
||||||
|
marginTop={30}
|
||||||
|
marginBottom={50}
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AuthInput
|
||||||
|
label={t['com.affine.settings.email']()}
|
||||||
|
disabled={true}
|
||||||
|
value={email}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="extraLarge"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
disabled={hasSentEmail}
|
||||||
|
loading={loading}
|
||||||
|
onClick={onSendEmail}
|
||||||
|
>
|
||||||
|
{hasSentEmail
|
||||||
|
? t['com.affine.auth.sent']()
|
||||||
|
: hasPassword
|
||||||
|
? t['com.affine.auth.send.reset.password.link']()
|
||||||
|
: t['com.affine.auth.send.set.password.link']()}
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Avatar, ConfirmModal, Input, Switch } from '@affine/component';
|
import { Avatar, ConfirmModal, Input, Switch } from '@affine/component';
|
||||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||||
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
|
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import {
|
import {
|
||||||
type DialogComponentProps,
|
type DialogComponentProps,
|
||||||
type GLOBAL_DIALOG_SCHEMA,
|
type GLOBAL_DIALOG_SCHEMA,
|
||||||
|
GlobalDialogService,
|
||||||
} from '@affine/core/modules/dialogs';
|
} from '@affine/core/modules/dialogs';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +16,6 @@ import {
|
|||||||
useService,
|
useService,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
||||||
@@ -27,7 +25,7 @@ interface NameWorkspaceContentProps extends ConfirmModalProps {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
onConfirmName: (
|
onConfirmName: (
|
||||||
name: string,
|
name: string,
|
||||||
workspaceFlavour: WorkspaceFlavour,
|
workspaceFlavour: string,
|
||||||
avatar?: File
|
avatar?: File
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
@@ -47,14 +45,11 @@ const NameWorkspaceContent = ({
|
|||||||
const session = useService(AuthService).session;
|
const session = useService(AuthService).session;
|
||||||
const loginStatus = useLiveData(session.status$);
|
const loginStatus = useLiveData(session.status$);
|
||||||
|
|
||||||
const setOpenSignIn = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
const openSignInModal = useCallback(() => {
|
const openSignInModal = useCallback(() => {
|
||||||
setOpenSignIn(state => ({
|
globalDialogService.open('sign-in', {});
|
||||||
...state,
|
}, [globalDialogService]);
|
||||||
openModal: true,
|
|
||||||
}));
|
|
||||||
}, [setOpenSignIn]);
|
|
||||||
|
|
||||||
const onSwitchChange = useCallback(
|
const onSwitchChange = useCallback(
|
||||||
(checked: boolean) => {
|
(checked: boolean) => {
|
||||||
@@ -67,10 +62,7 @@ const NameWorkspaceContent = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateWorkspace = useCallback(() => {
|
const handleCreateWorkspace = useCallback(() => {
|
||||||
onConfirmName(
|
onConfirmName(workspaceName, enable ? 'affine-cloud' : 'local');
|
||||||
workspaceName,
|
|
||||||
enable ? WorkspaceFlavour.AFFINE_CLOUD : WorkspaceFlavour.LOCAL
|
|
||||||
);
|
|
||||||
}, [enable, onConfirmName, workspaceName]);
|
}, [enable, onConfirmName, workspaceName]);
|
||||||
|
|
||||||
const onEnter = useCallback(() => {
|
const onEnter = useCallback(() => {
|
||||||
@@ -161,7 +153,7 @@ export const CreateWorkspaceDialog = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const onConfirmName = useAsyncCallback(
|
const onConfirmName = useAsyncCallback(
|
||||||
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
|
async (name: string, workspaceFlavour: string) => {
|
||||||
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
|
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
ImportTemplateService,
|
ImportTemplateService,
|
||||||
TemplateDownloaderService,
|
TemplateDownloaderService,
|
||||||
} from '@affine/core/modules/import-template';
|
} from '@affine/core/modules/import-template';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||||
import { AllDocsIcon } from '@blocksuite/icons/rc';
|
import { AllDocsIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -56,7 +55,7 @@ const Dialog = ({
|
|||||||
useState<WorkspaceMetadata | null>(null);
|
useState<WorkspaceMetadata | null>(null);
|
||||||
const selectedWorkspace =
|
const selectedWorkspace =
|
||||||
rawSelectedWorkspace ??
|
rawSelectedWorkspace ??
|
||||||
workspaces.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
|
workspaces.find(w => w.flavour !== 'local') ??
|
||||||
workspaces.at(0);
|
workspaces.at(0);
|
||||||
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
|
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
|
||||||
const { openPage, jumpToSignIn } = useNavigateHelper();
|
const { openPage, jumpToSignIn } = useNavigateHelper();
|
||||||
@@ -146,7 +145,8 @@ const Dialog = ({
|
|||||||
try {
|
try {
|
||||||
const { workspaceId, docId } =
|
const { workspaceId, docId } =
|
||||||
await importTemplateService.importToNewWorkspace(
|
await importTemplateService.importToNewWorkspace(
|
||||||
WorkspaceFlavour.AFFINE_CLOUD,
|
// TODO: support selfhosted
|
||||||
|
'affine-cloud',
|
||||||
'Workspace',
|
'Workspace',
|
||||||
templateDownloader.data$.value
|
templateDownloader.data$.value
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
import { _addLocalWorkspace } from '@affine/core/modules/workspace-engine';
|
import { _addLocalWorkspace } from '@affine/core/modules/workspace-engine';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { apis } from '@affine/electron-api';
|
import { apis } from '@affine/electron-api';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
@@ -37,7 +36,7 @@ export const ImportWorkspaceDialog = ({
|
|||||||
workspacesService.list.revalidate();
|
workspacesService.list.revalidate();
|
||||||
close({
|
close({
|
||||||
workspace: {
|
workspace: {
|
||||||
flavour: WorkspaceFlavour.LOCAL,
|
flavour: 'local',
|
||||||
id: result.workspaceId,
|
id: result.workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AuthModal } from '@affine/core/components/affine/auth';
|
|
||||||
import {
|
import {
|
||||||
type DialogComponentProps,
|
type DialogComponentProps,
|
||||||
type GLOBAL_DIALOG_SCHEMA,
|
type GLOBAL_DIALOG_SCHEMA,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { ChangePasswordDialog } from './change-password';
|
||||||
import { CollectionEditorDialog } from './collection-editor';
|
import { CollectionEditorDialog } from './collection-editor';
|
||||||
import { CreateWorkspaceDialog } from './create-workspace';
|
import { CreateWorkspaceDialog } from './create-workspace';
|
||||||
import { DocInfoDialog } from './doc-info';
|
import { DocInfoDialog } from './doc-info';
|
||||||
@@ -19,12 +19,17 @@ import { DateSelectorDialog } from './selectors/date';
|
|||||||
import { DocSelectorDialog } from './selectors/doc';
|
import { DocSelectorDialog } from './selectors/doc';
|
||||||
import { TagSelectorDialog } from './selectors/tag';
|
import { TagSelectorDialog } from './selectors/tag';
|
||||||
import { SettingDialog } from './setting';
|
import { SettingDialog } from './setting';
|
||||||
|
import { SignInDialog } from './sign-in';
|
||||||
|
import { VerifyEmailDialog } from './verify-email';
|
||||||
|
|
||||||
const GLOBAL_DIALOGS = {
|
const GLOBAL_DIALOGS = {
|
||||||
'create-workspace': CreateWorkspaceDialog,
|
'create-workspace': CreateWorkspaceDialog,
|
||||||
'import-workspace': ImportWorkspaceDialog,
|
'import-workspace': ImportWorkspaceDialog,
|
||||||
'import-template': ImportTemplateDialog,
|
'import-template': ImportTemplateDialog,
|
||||||
setting: SettingDialog,
|
setting: SettingDialog,
|
||||||
|
'sign-in': SignInDialog,
|
||||||
|
'change-password': ChangePasswordDialog,
|
||||||
|
'verify-email': VerifyEmailDialog,
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
||||||
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
||||||
@@ -66,8 +71,6 @@ export const GlobalDialogs = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<AuthModal />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import { Avatar } from '@affine/component/ui/avatar';
|
import { Avatar } from '@affine/component/ui/avatar';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
|
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||||
import { Upload } from '@affine/core/components/pure/file-upload';
|
import { Upload } from '@affine/core/components/pure/file-upload';
|
||||||
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { SubscriptionPlan } from '@affine/graphql';
|
import { SubscriptionPlan } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
useService,
|
useService,
|
||||||
useServices,
|
useServices,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { AuthService, ServerService } from '../../../../modules/cloud';
|
import { AuthService, ServerService } from '../../../../modules/cloud';
|
||||||
@@ -178,9 +177,10 @@ export const AccountSetting = ({
|
|||||||
}: {
|
}: {
|
||||||
onChangeSettingState?: (settingState: SettingState) => void;
|
onChangeSettingState?: (settingState: SettingState) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { authService, serverService } = useServices({
|
const { authService, serverService, globalDialogService } = useServices({
|
||||||
AuthService,
|
AuthService,
|
||||||
ServerService,
|
ServerService,
|
||||||
|
GlobalDialogService,
|
||||||
});
|
});
|
||||||
const serverFeatures = useLiveData(serverService.server.features$);
|
const serverFeatures = useLiveData(serverService.server.features$);
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -189,28 +189,20 @@ export const AccountSetting = ({
|
|||||||
session.revalidate();
|
session.revalidate();
|
||||||
}, [session]);
|
}, [session]);
|
||||||
const account = useEnsureLiveData(session.account$);
|
const account = useEnsureLiveData(session.account$);
|
||||||
const setAuthModal = useSetAtom(authAtom);
|
|
||||||
const openSignOutModal = useSignOut();
|
const openSignOutModal = useSignOut();
|
||||||
|
|
||||||
const onChangeEmail = useCallback(() => {
|
const onChangeEmail = useCallback(() => {
|
||||||
setAuthModal({
|
globalDialogService.open('verify-email', {
|
||||||
openModal: true,
|
server: serverService.server.baseUrl,
|
||||||
state: 'sendEmail',
|
changeEmail: !!account.info?.emailVerified,
|
||||||
// @ts-expect-error accont email is always defined
|
|
||||||
email: account.email,
|
|
||||||
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
|
|
||||||
});
|
});
|
||||||
}, [account.email, account.info?.emailVerified, setAuthModal]);
|
}, [account, globalDialogService, serverService.server.baseUrl]);
|
||||||
|
|
||||||
const onPasswordButtonClick = useCallback(() => {
|
const onPasswordButtonClick = useCallback(() => {
|
||||||
setAuthModal({
|
globalDialogService.open('change-password', {
|
||||||
openModal: true,
|
server: serverService.server.baseUrl,
|
||||||
state: 'sendEmail',
|
|
||||||
// @ts-expect-error accont email is always defined
|
|
||||||
email: account.email,
|
|
||||||
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
|
|
||||||
});
|
});
|
||||||
}, [account.email, account.info?.hasPassword, setAuthModal]);
|
}, [globalDialogService, serverService.server.baseUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { Button, type ButtonProps } from '@affine/component';
|
import { Button, type ButtonProps } from '@affine/component';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export const AILogin = (btnProps: ButtonProps) => {
|
export const AILogin = (btnProps: ButtonProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const setOpen = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
const onClickSignIn = useCallback(() => {
|
const onClickSignIn = useCallback(() => {
|
||||||
setOpen(state => ({
|
globalDialogService.open('sign-in', {});
|
||||||
...state,
|
}, [globalDialogService]);
|
||||||
openModal: true,
|
|
||||||
}));
|
|
||||||
}, [setOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={onClickSignIn} variant="primary" {...btnProps}>
|
<Button onClick={onClickSignIn} variant="primary" {...btnProps}>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Button, type ButtonProps } from '@affine/component/ui/button';
|
import { Button, type ButtonProps } from '@affine/component/ui/button';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import {
|
import {
|
||||||
type CreateCheckoutSessionInput,
|
type CreateCheckoutSessionInput,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
@@ -18,7 +18,6 @@ import { track } from '@affine/track';
|
|||||||
import { DoneIcon } from '@blocksuite/icons/rc';
|
import { DoneIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@@ -387,14 +386,11 @@ export const SignUpAction = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: PropsWithChildren<{ className?: string }>) => {
|
}: PropsWithChildren<{ className?: string }>) => {
|
||||||
const setOpen = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
const onClickSignIn = useCallback(() => {
|
const onClickSignIn = useCallback(() => {
|
||||||
setOpen(state => ({
|
globalDialogService.open('sign-in', {});
|
||||||
...state,
|
}, [globalDialogService]);
|
||||||
openModal: true,
|
|
||||||
}));
|
|
||||||
}, [setOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { Avatar } from '@affine/component/ui/avatar';
|
|||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||||
import { UserPlanButton } from '@affine/core/components/affine/auth/user-plan-button';
|
import { UserPlanButton } from '@affine/core/components/affine/auth/user-plan-button';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
|
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
|
||||||
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useSetAtom } from 'jotai/react';
|
|
||||||
import {
|
import {
|
||||||
type MouseEvent,
|
type MouseEvent,
|
||||||
Suspense,
|
Suspense,
|
||||||
@@ -95,14 +94,14 @@ export const UserInfo = ({
|
|||||||
|
|
||||||
export const SignInButton = () => {
|
export const SignInButton = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const setAuthModal = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={style.accountButton}
|
className={style.accountButton}
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
setAuthModal({ openModal: true, state: 'signIn' });
|
globalDialogService.open('sign-in', {});
|
||||||
}, [setAuthModal])}
|
}, [globalDialogService])}
|
||||||
>
|
>
|
||||||
<div className="avatar not-sign">
|
<div className="avatar not-sign">
|
||||||
<Logo1Icon />
|
<Logo1Icon />
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
|||||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
@@ -44,7 +43,7 @@ export const WorkspaceDeleteModal = ({
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
|
{workspaceMetadata.flavour === 'local' ? (
|
||||||
<Trans i18nKey="com.affine.workspaceDelete.description">
|
<Trans i18nKey="com.affine.workspaceDelete.description">
|
||||||
Deleting (
|
Deleting (
|
||||||
<span className={styles.workspaceName}>
|
<span className={styles.workspaceName}>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { SettingRow } from '@affine/component/setting-components';
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
useLiveData,
|
useLiveData,
|
||||||
@@ -37,7 +36,7 @@ export const EnableCloudPanel = ({
|
|||||||
});
|
});
|
||||||
}, [confirmEnableCloud, onCloseSetting, workspace]);
|
}, [confirmEnableCloud, onCloseSetting, workspace]);
|
||||||
|
|
||||||
if (flavour !== WorkspaceFlavour.LOCAL) {
|
if (flavour !== 'local') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
WorkspacePermissionService,
|
WorkspacePermissionService,
|
||||||
} from '@affine/core/modules/permissions';
|
} from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { Permission, UserFriendlyError } from '@affine/graphql';
|
import { Permission, UserFriendlyError } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
@@ -410,7 +409,7 @@ export const MembersPanel = ({
|
|||||||
onChangeSettingState: (settingState: SettingState) => void;
|
onChangeSettingState: (settingState: SettingState) => void;
|
||||||
}): ReactElement | null => {
|
}): ReactElement | null => {
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour === 'local') {
|
||||||
return <MembersPanelLocal />;
|
return <MembersPanelLocal />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import {
|
|||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting';
|
import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
|
||||||
export const SharingPanel = () => {
|
export const SharingPanel = () => {
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour === 'local') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <Sharing />;
|
return <Sharing />;
|
||||||
|
|||||||
25
packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx
Normal file
25
packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Modal } from '@affine/component';
|
||||||
|
import { SignInPanel } from '@affine/core/components/sign-in';
|
||||||
|
import type {
|
||||||
|
DialogComponentProps,
|
||||||
|
GLOBAL_DIALOG_SCHEMA,
|
||||||
|
} from '@affine/core/modules/dialogs';
|
||||||
|
export const SignInDialog = ({
|
||||||
|
close,
|
||||||
|
server: initialServerBaseUrl,
|
||||||
|
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open
|
||||||
|
onOpenChange={() => close()}
|
||||||
|
width={400}
|
||||||
|
minHeight={500}
|
||||||
|
contentOptions={{
|
||||||
|
['data-testid' as string]: 'auth-modal',
|
||||||
|
style: { padding: '44px 40px 20px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SignInPanel onClose={close} server={initialServerBaseUrl} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { Button, Modal, notify, Wrapper } from '@affine/component';
|
||||||
|
import {
|
||||||
|
AuthContent,
|
||||||
|
AuthInput,
|
||||||
|
ModalHeader,
|
||||||
|
} from '@affine/component/auth-components';
|
||||||
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
import {
|
||||||
|
AuthService,
|
||||||
|
DefaultServerService,
|
||||||
|
ServersService,
|
||||||
|
} from '@affine/core/modules/cloud';
|
||||||
|
import type {
|
||||||
|
DialogComponentProps,
|
||||||
|
GLOBAL_DIALOG_SCHEMA,
|
||||||
|
} from '@affine/core/modules/dialogs';
|
||||||
|
import { Unreachable } from '@affine/env/constant';
|
||||||
|
import {
|
||||||
|
sendChangeEmailMutation,
|
||||||
|
sendVerifyEmailMutation,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const VerifyEmailDialog = ({
|
||||||
|
close,
|
||||||
|
server: serverBaseUrl,
|
||||||
|
changeEmail,
|
||||||
|
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['verify-email']>) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const defaultServerService = useService(DefaultServerService);
|
||||||
|
const serversService = useService(ServersService);
|
||||||
|
let server;
|
||||||
|
|
||||||
|
if (serverBaseUrl) {
|
||||||
|
server = serversService.getServerByBaseUrl(serverBaseUrl);
|
||||||
|
if (!server) {
|
||||||
|
throw new Unreachable('Server not found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
server = defaultServerService.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authService = server.scope.get(AuthService);
|
||||||
|
const account = useLiveData(authService.session.account$);
|
||||||
|
const email = account?.email;
|
||||||
|
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const passwordLimits = useLiveData(
|
||||||
|
server.credentialsRequirement$.map(r => r?.password)
|
||||||
|
);
|
||||||
|
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
// should not happen
|
||||||
|
throw new Unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSendEmail = useAsyncCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (changeEmail) {
|
||||||
|
await server.gql({
|
||||||
|
query: sendChangeEmailMutation,
|
||||||
|
variables: {
|
||||||
|
callbackUrl: `/auth/changeEmail`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await server.gql({
|
||||||
|
query: sendVerifyEmailMutation,
|
||||||
|
variables: {
|
||||||
|
callbackUrl: `/auth/verify-email`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
notify.success({
|
||||||
|
title: t['com.affine.auth.send.verify.email.hint'](),
|
||||||
|
});
|
||||||
|
setHasSentEmail(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
notify.error({
|
||||||
|
title: t['com.affine.auth.sent.change.email.fail'](),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [changeEmail, server, t]);
|
||||||
|
|
||||||
|
if (!passwordLimits) {
|
||||||
|
// should never reach here
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open
|
||||||
|
onOpenChange={() => close()}
|
||||||
|
width={400}
|
||||||
|
minHeight={500}
|
||||||
|
contentOptions={{
|
||||||
|
['data-testid' as string]: 'verify-email-modal',
|
||||||
|
style: { padding: '44px 40px 20px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title={serverName}
|
||||||
|
subTitle={t['com.affine.settings.email.action.change']()}
|
||||||
|
/>
|
||||||
|
<AuthContent>
|
||||||
|
{t['com.affine.auth.verify.email.message']({ email })}
|
||||||
|
</AuthContent>
|
||||||
|
|
||||||
|
<Wrapper
|
||||||
|
marginTop={30}
|
||||||
|
marginBottom={50}
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AuthInput
|
||||||
|
label={t['com.affine.settings.email']()}
|
||||||
|
disabled={true}
|
||||||
|
value={email}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="extraLarge"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
disabled={hasSentEmail}
|
||||||
|
loading={loading}
|
||||||
|
onClick={onSendEmail}
|
||||||
|
>
|
||||||
|
{hasSentEmail
|
||||||
|
? t['com.affine.auth.sent']()
|
||||||
|
: t['com.affine.auth.send.verify.email.hint']()}
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
|
import { notify } from '@affine/component';
|
||||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||||
import { SignInPageContainer } from '@affine/component/auth-components';
|
import { SignInPageContainer } from '@affine/component/auth-components';
|
||||||
|
import { SignInPanel } from '@affine/core/components/sign-in';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { AuthPanel } from '../../../components/affine/auth';
|
|
||||||
import {
|
import {
|
||||||
RouteLogic,
|
RouteLogic,
|
||||||
useNavigateHelper,
|
useNavigateHelper,
|
||||||
@@ -17,18 +19,25 @@ export const SignIn = ({
|
|||||||
}: {
|
}: {
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
const session = useService(AuthService).session;
|
const session = useService(AuthService).session;
|
||||||
const status = useLiveData(session.status$);
|
|
||||||
const isRevalidating = useLiveData(session.isRevalidating$);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { jumpToIndex } = useNavigateHelper();
|
const { jumpToIndex } = useNavigateHelper();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
|
||||||
const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri');
|
const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri');
|
||||||
|
const error = searchParams.get('error');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn) {
|
if (error) {
|
||||||
if (redirectUrl) {
|
notify.error({
|
||||||
|
title: t['com.affine.auth.toast.title.failed'](),
|
||||||
|
message: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [error, t]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (session.status$.value === 'authenticated' && redirectUrl) {
|
||||||
navigate(redirectUrl, {
|
navigate(redirectUrl, {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
@@ -37,13 +46,12 @@ export const SignIn = ({
|
|||||||
search: searchParams.toString(),
|
search: searchParams.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [jumpToIndex, navigate, isLoggedIn, redirectUrl, searchParams]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignInPageContainer>
|
<SignInPageContainer>
|
||||||
<div style={{ maxWidth: '400px', width: '100%' }}>
|
<div style={{ maxWidth: '400px', width: '100%' }}>
|
||||||
<AuthPanel onSkip={jumpToIndex} redirectUrl={redirectUrl} />
|
<SignInPanel onClose={handleClose} />
|
||||||
</div>
|
</div>
|
||||||
</SignInPageContainer>
|
</SignInPageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
buildShowcaseWorkspace,
|
buildShowcaseWorkspace,
|
||||||
createFirstAppData,
|
createFirstAppData,
|
||||||
} from '@affine/core/utils/first-app-data';
|
} from '@affine/core/utils/first-app-data';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import {
|
import {
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
@@ -59,11 +58,8 @@ export const Component = ({
|
|||||||
const createCloudWorkspace = useCallback(() => {
|
const createCloudWorkspace = useCallback(() => {
|
||||||
if (createOnceRef.current) return;
|
if (createOnceRef.current) return;
|
||||||
createOnceRef.current = true;
|
createOnceRef.current = true;
|
||||||
buildShowcaseWorkspace(
|
// TODO: support selfhosted
|
||||||
workspacesService,
|
buildShowcaseWorkspace(workspacesService, 'affine-cloud', 'AFFiNE Cloud')
|
||||||
WorkspaceFlavour.AFFINE_CLOUD,
|
|
||||||
'AFFiNE Cloud'
|
|
||||||
)
|
|
||||||
.then(({ meta, defaultDocId }) => {
|
.then(({ meta, defaultDocId }) => {
|
||||||
if (defaultDocId) {
|
if (defaultDocId) {
|
||||||
jumpToPage(meta.id, defaultDocId);
|
jumpToPage(meta.id, defaultDocId);
|
||||||
@@ -86,15 +82,14 @@ export const Component = ({
|
|||||||
// check is user logged in && has cloud workspace
|
// check is user logged in && has cloud workspace
|
||||||
if (searchParams.get('initCloud') === 'true') {
|
if (searchParams.get('initCloud') === 'true') {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
if (list.every(w => w.flavour !== WorkspaceFlavour.AFFINE_CLOUD)) {
|
if (list.every(w => w.flavour !== 'affine-cloud')) {
|
||||||
createCloudWorkspace();
|
createCloudWorkspace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// open first cloud workspace
|
// open first cloud workspace
|
||||||
const openWorkspace =
|
const openWorkspace =
|
||||||
list.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
|
list.find(w => w.flavour === 'affine-cloud') ?? list[0];
|
||||||
list[0];
|
|
||||||
openPage(openWorkspace.id, defaultIndexRoute);
|
openPage(openWorkspace.id, defaultIndexRoute);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { NotificationCenter } from '@affine/component';
|
import { NotificationCenter } from '@affine/component';
|
||||||
import { DefaultServerService } from '@affine/core/modules/cloud';
|
import { DefaultServerService } from '@affine/core/modules/cloud';
|
||||||
import { FrameworkScope, useService } from '@toeverything/infra';
|
import {
|
||||||
|
FrameworkScope,
|
||||||
|
GlobalContextService,
|
||||||
|
useService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -10,6 +14,7 @@ import { FindInPageModal } from './find-in-page/find-in-page-modal';
|
|||||||
|
|
||||||
export const RootWrapper = () => {
|
export const RootWrapper = () => {
|
||||||
const defaultServerService = useService(DefaultServerService);
|
const defaultServerService = useService(DefaultServerService);
|
||||||
|
const globalContextService = useService(GlobalContextService);
|
||||||
const [isServerReady, setIsServerReady] = useState(false);
|
const [isServerReady, setIsServerReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,6 +35,15 @@ export const RootWrapper = () => {
|
|||||||
};
|
};
|
||||||
}, [defaultServerService, isServerReady]);
|
}, [defaultServerService, isServerReady]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
globalContextService.globalContext.serverId.set(
|
||||||
|
defaultServerService.server.id
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
globalContextService.globalContext.serverId.set(null);
|
||||||
|
};
|
||||||
|
}, [defaultServerService, globalContextService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FrameworkScope scope={defaultServerService.server.scope}>
|
<FrameworkScope scope={defaultServerService.server.scope}>
|
||||||
<GlobalDialogs />
|
<GlobalDialogs />
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||||
import { workbenchRoutes } from '@affine/core/desktop/workbench-router';
|
import { workbenchRoutes } from '@affine/core/desktop/workbench-router';
|
||||||
|
import {
|
||||||
|
DefaultServerService,
|
||||||
|
WorkspaceServerService,
|
||||||
|
} from '@affine/core/modules/cloud';
|
||||||
import { ZipTransformer } from '@blocksuite/affine/blocks';
|
import { ZipTransformer } from '@blocksuite/affine/blocks';
|
||||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
@@ -125,12 +129,15 @@ export const Component = (): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
||||||
const { workspacesService, globalContextService } = useServices({
|
const { workspacesService, globalContextService, defaultServerService } =
|
||||||
|
useServices({
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
|
DefaultServerService,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||||
|
const workspaceServer = workspace?.scope.get(WorkspaceServerService).server;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const ref = workspacesService.open({ metadata: meta });
|
const ref = workspacesService.open({ metadata: meta });
|
||||||
@@ -189,13 +196,30 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
|||||||
};
|
};
|
||||||
localStorage.setItem('last_workspace_id', workspace.id);
|
localStorage.setItem('last_workspace_id', workspace.id);
|
||||||
globalContextService.globalContext.workspaceId.set(workspace.id);
|
globalContextService.globalContext.workspaceId.set(workspace.id);
|
||||||
|
if (workspaceServer) {
|
||||||
|
globalContextService.globalContext.serverId.set(workspaceServer.id);
|
||||||
|
}
|
||||||
|
globalContextService.globalContext.workspaceFlavour.set(
|
||||||
|
workspace.flavour
|
||||||
|
);
|
||||||
return () => {
|
return () => {
|
||||||
window.currentWorkspace = undefined;
|
window.currentWorkspace = undefined;
|
||||||
globalContextService.globalContext.workspaceId.set(null);
|
globalContextService.globalContext.workspaceId.set(null);
|
||||||
|
if (workspaceServer) {
|
||||||
|
globalContextService.globalContext.serverId.set(
|
||||||
|
defaultServerService.server.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
globalContextService.globalContext.workspaceFlavour.set(null);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}, [globalContextService, workspace]);
|
}, [
|
||||||
|
defaultServerService.server.id,
|
||||||
|
globalContextService,
|
||||||
|
workspace,
|
||||||
|
workspaceServer,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return null; // skip this, workspace will be set in layout effect
|
return null; // skip this, workspace will be set in layout effect
|
||||||
@@ -203,13 +227,16 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
|||||||
|
|
||||||
if (!isRootDocReady) {
|
if (!isRootDocReady) {
|
||||||
return (
|
return (
|
||||||
|
<FrameworkScope scope={workspaceServer?.scope}>
|
||||||
<FrameworkScope scope={workspace.scope}>
|
<FrameworkScope scope={workspace.scope}>
|
||||||
<AppContainer fallback />
|
<AppContainer fallback />
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
|
</FrameworkScope>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<FrameworkScope scope={workspaceServer?.scope}>
|
||||||
<FrameworkScope scope={workspace.scope}>
|
<FrameworkScope scope={workspace.scope}>
|
||||||
<AffineErrorBoundary height="100vh">
|
<AffineErrorBoundary height="100vh">
|
||||||
<WorkspaceLayout>
|
<WorkspaceLayout>
|
||||||
@@ -217,5 +244,6 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
|||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
|
</FrameworkScope>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { AppContainer } from '@affine/core/desktop/components/app-container';
|
|||||||
import { WorkspaceDialogs } from '@affine/core/desktop/dialogs';
|
import { WorkspaceDialogs } from '@affine/core/desktop/dialogs';
|
||||||
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import {
|
import {
|
||||||
LiveData,
|
LiveData,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
@@ -29,10 +28,9 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
|||||||
<WorkspaceDialogs />
|
<WorkspaceDialogs />
|
||||||
|
|
||||||
{/* ---- some side-effect components ---- */}
|
{/* ---- some side-effect components ---- */}
|
||||||
{currentWorkspace?.flavour === WorkspaceFlavour.LOCAL && (
|
{currentWorkspace?.flavour === 'local' ? (
|
||||||
<LocalQuotaModal />
|
<LocalQuotaModal />
|
||||||
)}
|
) : (
|
||||||
{currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
|
|
||||||
<CloudQuotaModal />
|
<CloudQuotaModal />
|
||||||
)}
|
)}
|
||||||
<AiLoginRequiredModal />
|
<AiLoginRequiredModal />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AuthModal } from '@affine/core/components/affine/auth';
|
|
||||||
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
||||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||||
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
||||||
@@ -30,7 +29,6 @@ export function ShareHeader({
|
|||||||
snapshotUrl={snapshotUrl}
|
snapshotUrl={snapshotUrl}
|
||||||
templateName={templateName}
|
templateName={templateName}
|
||||||
/>
|
/>
|
||||||
<AuthModal />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
|||||||
import { ShareReaderService } from '@affine/core/modules/share-doc';
|
import { ShareReaderService } from '@affine/core/modules/share-doc';
|
||||||
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
||||||
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
|
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
type DocMode,
|
type DocMode,
|
||||||
@@ -170,7 +169,7 @@ const SharePageInner = ({
|
|||||||
{
|
{
|
||||||
metadata: {
|
metadata: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
flavour: 'affine-cloud',
|
||||||
},
|
},
|
||||||
isSharedMode: true,
|
isSharedMode: true,
|
||||||
},
|
},
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
import { SignInPanel } from '@affine/core/components/sign-in';
|
||||||
|
|
||||||
|
import { MobileSignInLayout } from './layout';
|
||||||
|
|
||||||
|
export const MobileSignInPanel = ({
|
||||||
|
onClose,
|
||||||
|
server,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
server?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<MobileSignInLayout>
|
||||||
|
<SignInPanel onClose={onClose} server={server} />
|
||||||
|
</MobileSignInLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@ import { IconButton } from '@affine/component';
|
|||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { CloseIcon, CollaborationIcon } from '@blocksuite/icons/rc';
|
import { CloseIcon, CollaborationIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
useLiveData,
|
useLiveData,
|
||||||
@@ -16,10 +15,8 @@ import { type HTMLAttributes, useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import * as styles from './menu.css';
|
import * as styles from './menu.css';
|
||||||
|
|
||||||
const filterByFlavour = (
|
const filterByFlavour = (workspaces: WorkspaceMetadata[], flavour: string) =>
|
||||||
workspaces: WorkspaceMetadata[],
|
workspaces.filter(ws => flavour === ws.flavour);
|
||||||
flavour: WorkspaceFlavour
|
|
||||||
) => workspaces.filter(ws => flavour === ws.flavour);
|
|
||||||
|
|
||||||
const WorkspaceItem = ({
|
const WorkspaceItem = ({
|
||||||
workspace,
|
workspace,
|
||||||
@@ -93,13 +90,14 @@ export const SelectorMenu = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
|
|
||||||
|
// TODO: support selfhosted
|
||||||
const cloudWorkspaces = useMemo(
|
const cloudWorkspaces = useMemo(
|
||||||
() => filterByFlavour(workspaces, WorkspaceFlavour.AFFINE_CLOUD),
|
() => filterByFlavour(workspaces, 'affine-cloud'),
|
||||||
[workspaces]
|
[workspaces]
|
||||||
);
|
);
|
||||||
|
|
||||||
const localWorkspaces = useMemo(
|
const localWorkspaces = useMemo(
|
||||||
() => filterByFlavour(workspaces, WorkspaceFlavour.LOCAL),
|
() => filterByFlavour(workspaces, 'local'),
|
||||||
[workspaces]
|
[workspaces]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AuthModal } from '@affine/core/components/affine/auth';
|
|
||||||
import {
|
import {
|
||||||
type DialogComponentProps,
|
type DialogComponentProps,
|
||||||
type GLOBAL_DIALOG_SCHEMA,
|
type GLOBAL_DIALOG_SCHEMA,
|
||||||
@@ -12,6 +11,7 @@ import { CollectionSelectorDialog } from './selectors/collection-selector';
|
|||||||
import { DocSelectorDialog } from './selectors/doc-selector';
|
import { DocSelectorDialog } from './selectors/doc-selector';
|
||||||
import { TagSelectorDialog } from './selectors/tag-selector';
|
import { TagSelectorDialog } from './selectors/tag-selector';
|
||||||
import { SettingDialog } from './setting';
|
import { SettingDialog } from './setting';
|
||||||
|
import { SignInDialog } from './sign-in';
|
||||||
|
|
||||||
const GLOBAL_DIALOGS = {
|
const GLOBAL_DIALOGS = {
|
||||||
// 'create-workspace': CreateWorkspaceDialog,
|
// 'create-workspace': CreateWorkspaceDialog,
|
||||||
@@ -19,6 +19,7 @@ const GLOBAL_DIALOGS = {
|
|||||||
// 'import-template': ImportTemplateDialog,
|
// 'import-template': ImportTemplateDialog,
|
||||||
setting: SettingDialog,
|
setting: SettingDialog,
|
||||||
// import: ImportDialog,
|
// import: ImportDialog,
|
||||||
|
'sign-in': SignInDialog,
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
||||||
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
||||||
@@ -58,8 +59,6 @@ export const GlobalDialogs = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<AuthModal />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Avatar } from '@affine/component';
|
import { Avatar } from '@affine/component';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
|
||||||
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
|
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
useEnsureLiveData,
|
useEnsureLiveData,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
import { UserPlanTag } from '../../../components';
|
import { UserPlanTag } from '../../../components';
|
||||||
@@ -79,11 +78,11 @@ const AuthorizedUserProfile = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UnauthorizedUserProfile = () => {
|
const UnauthorizedUserProfile = () => {
|
||||||
const setAuthModal = useSetAtom(authAtom);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
onClick={() => setAuthModal({ openModal: true, state: 'signIn' })}
|
onClick={() => globalDialogService.open('sign-in', {})}
|
||||||
avatar={<Avatar size={48} rounded={4} />}
|
avatar={<Avatar size={48} rounded={4} />}
|
||||||
title="Sign up / Sign in"
|
title="Sign up / Sign in"
|
||||||
caption="Sync with AFFiNE Cloud"
|
caption="Sync with AFFiNE Cloud"
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
import { IconButton, Modal, SafeArea } from '@affine/component';
|
import { IconButton, Modal, SafeArea } from '@affine/component';
|
||||||
import { authAtom } from '@affine/core/components/atoms';
|
import type {
|
||||||
|
DialogComponentProps,
|
||||||
|
GLOBAL_DIALOG_SCHEMA,
|
||||||
|
} from '@affine/core/modules/dialogs';
|
||||||
import { CloseIcon } from '@blocksuite/icons/rc';
|
import { CloseIcon } from '@blocksuite/icons/rc';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { MobileSignIn } from './mobile-sign-in';
|
import { MobileSignInPanel } from '../../components/sign-in';
|
||||||
|
|
||||||
export const MobileSignInModal = () => {
|
|
||||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
|
||||||
const setOpen = useCallback(
|
|
||||||
(open: boolean) => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, openModal: open }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
|
||||||
setOpen(false);
|
|
||||||
}, [setOpen]);
|
|
||||||
|
|
||||||
|
export const SignInDialog = ({
|
||||||
|
close,
|
||||||
|
server: initialServerBaseUrl,
|
||||||
|
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
fullScreen
|
fullScreen
|
||||||
animation="slideBottom"
|
animation="slideBottom"
|
||||||
open={authAtomValue.openModal}
|
open
|
||||||
onOpenChange={setOpen}
|
onOpenChange={() => close()}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
style: {
|
style: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@@ -35,7 +27,7 @@ export const MobileSignInModal = () => {
|
|||||||
}}
|
}}
|
||||||
withoutCloseButton
|
withoutCloseButton
|
||||||
>
|
>
|
||||||
<MobileSignIn onSkip={closeModal} />
|
<MobileSignInPanel onClose={close} server={initialServerBaseUrl} />
|
||||||
<SafeArea
|
<SafeArea
|
||||||
top
|
top
|
||||||
style={{ position: 'absolute', top: 0, right: 0, paddingRight: 16 }}
|
style={{ position: 'absolute', top: 0, right: 0, paddingRight: 16 }}
|
||||||
@@ -46,7 +38,7 @@ export const MobileSignInModal = () => {
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
icon={<CloseIcon />}
|
icon={<CloseIcon />}
|
||||||
style={{ borderRadius: 8, padding: 4 }}
|
style={{ borderRadius: 8, padding: 4 }}
|
||||||
onClick={closeModal}
|
onClick={() => close()}
|
||||||
/>
|
/>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -5,7 +5,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import { GlobalDialogs } from '../../dialogs';
|
import { GlobalDialogs } from '../../dialogs';
|
||||||
import { MobileSignInModal } from '../../views/sign-in/modal';
|
|
||||||
|
|
||||||
export const RootWrapper = () => {
|
export const RootWrapper = () => {
|
||||||
const defaultServerService = useService(DefaultServerService);
|
const defaultServerService = useService(DefaultServerService);
|
||||||
@@ -33,7 +32,6 @@ export const RootWrapper = () => {
|
|||||||
<FrameworkScope scope={defaultServerService.server.scope}>
|
<FrameworkScope scope={defaultServerService.server.scope}>
|
||||||
<GlobalDialogs />
|
<GlobalDialogs />
|
||||||
<NotificationCenter />
|
<NotificationCenter />
|
||||||
<MobileSignInModal />
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,38 +1,10 @@
|
|||||||
import {
|
|
||||||
RouteLogic,
|
|
||||||
useNavigateHelper,
|
|
||||||
} from '@affine/core/components/hooks/use-navigate-helper';
|
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { MobileSignIn } from '../views/sign-in/mobile-sign-in';
|
import { MobileSignInPanel } from '../components/sign-in';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const session = useService(AuthService).session;
|
|
||||||
const status = useLiveData(session.status$);
|
|
||||||
const isRevalidating = useLiveData(session.isRevalidating$);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { jumpToIndex } = useNavigateHelper();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
|
||||||
|
|
||||||
useEffect(() => {
|
return <MobileSignInPanel onClose={() => navigate('/')} />;
|
||||||
if (isLoggedIn) {
|
|
||||||
const redirectUri = searchParams.get('redirect_uri');
|
|
||||||
if (redirectUri) {
|
|
||||||
navigate(redirectUri, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
jumpToIndex(RouteLogic.REPLACE, {
|
|
||||||
search: searchParams.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [jumpToIndex, navigate, isLoggedIn, searchParams]);
|
|
||||||
|
|
||||||
return <MobileSignIn onSkip={() => navigate('/')} />;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { IconButton, MobileMenu } from '@affine/component';
|
import { IconButton, MobileMenu } from '@affine/component';
|
||||||
import { SharePage } from '@affine/core/components/affine/share-page-modal/share-menu/share-page';
|
import { SharePage } from '@affine/core/components/affine/share-page-modal/share-menu/share-page';
|
||||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { ShareiOsIcon } from '@blocksuite/icons/rc';
|
import { ShareiOsIcon } from '@blocksuite/icons/rc';
|
||||||
import { DocService, useServices, WorkspaceService } from '@toeverything/infra';
|
import { DocService, useServices, WorkspaceService } from '@toeverything/infra';
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ export const PageHeaderShareButton = () => {
|
|||||||
const doc = docService.doc.blockSuiteDoc;
|
const doc = docService.doc.blockSuiteDoc;
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
if (workspace.meta.flavour === WorkspaceFlavour.LOCAL) {
|
if (workspace.meta.flavour === 'local') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import {
|
|||||||
} from '@affine/core/components/affine/quota-reached-modal';
|
} from '@affine/core/components/affine/quota-reached-modal';
|
||||||
import { SWRConfigProvider } from '@affine/core/components/providers/swr-config-provider';
|
import { SWRConfigProvider } from '@affine/core/components/providers/swr-config-provider';
|
||||||
import { WorkspaceSideEffects } from '@affine/core/components/providers/workspace-side-effects';
|
import { WorkspaceSideEffects } from '@affine/core/components/providers/workspace-side-effects';
|
||||||
|
import {
|
||||||
|
DefaultServerService,
|
||||||
|
WorkspaceServerService,
|
||||||
|
} from '@affine/core/modules/cloud';
|
||||||
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
FrameworkScope,
|
FrameworkScope,
|
||||||
@@ -47,12 +50,15 @@ export const WorkspaceLayout = ({
|
|||||||
children,
|
children,
|
||||||
}: PropsWithChildren<{ meta: WorkspaceMetadata }>) => {
|
}: PropsWithChildren<{ meta: WorkspaceMetadata }>) => {
|
||||||
// todo: reduce code duplication with packages\frontend\core\src\pages\workspace\index.tsx
|
// todo: reduce code duplication with packages\frontend\core\src\pages\workspace\index.tsx
|
||||||
const { workspacesService, globalContextService } = useServices({
|
const { workspacesService, globalContextService, defaultServerService } =
|
||||||
|
useServices({
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
|
DefaultServerService,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||||
|
const workspaceServer = workspace?.scope.get(WorkspaceServerService)?.server;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const ref = workspacesService.open({ metadata: meta });
|
const ref = workspacesService.open({ metadata: meta });
|
||||||
@@ -75,13 +81,30 @@ export const WorkspaceLayout = ({
|
|||||||
);
|
);
|
||||||
localStorage.setItem('last_workspace_id', workspace.id);
|
localStorage.setItem('last_workspace_id', workspace.id);
|
||||||
globalContextService.globalContext.workspaceId.set(workspace.id);
|
globalContextService.globalContext.workspaceId.set(workspace.id);
|
||||||
|
if (workspaceServer) {
|
||||||
|
globalContextService.globalContext.serverId.set(workspaceServer.id);
|
||||||
|
}
|
||||||
|
globalContextService.globalContext.workspaceFlavour.set(
|
||||||
|
workspace.flavour
|
||||||
|
);
|
||||||
return () => {
|
return () => {
|
||||||
window.currentWorkspace = undefined;
|
window.currentWorkspace = undefined;
|
||||||
globalContextService.globalContext.workspaceId.set(null);
|
globalContextService.globalContext.workspaceId.set(null);
|
||||||
|
if (workspaceServer) {
|
||||||
|
globalContextService.globalContext.serverId.set(
|
||||||
|
defaultServerService.server.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
globalContextService.globalContext.workspaceFlavour.set(null);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}, [globalContextService, workspace]);
|
}, [
|
||||||
|
defaultServerService.server.id,
|
||||||
|
globalContextService,
|
||||||
|
workspace,
|
||||||
|
workspaceServer,
|
||||||
|
]);
|
||||||
|
|
||||||
const isRootDocReady =
|
const isRootDocReady =
|
||||||
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
|
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
|
||||||
@@ -95,6 +118,7 @@ export const WorkspaceLayout = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<FrameworkScope scope={workspaceServer?.scope}>
|
||||||
<FrameworkScope scope={workspace.scope}>
|
<FrameworkScope scope={workspace.scope}>
|
||||||
<AffineErrorBoundary height="100dvh">
|
<AffineErrorBoundary height="100dvh">
|
||||||
<SWRConfigProvider>
|
<SWRConfigProvider>
|
||||||
@@ -102,8 +126,9 @@ export const WorkspaceLayout = ({
|
|||||||
|
|
||||||
{/* ---- some side-effect components ---- */}
|
{/* ---- some side-effect components ---- */}
|
||||||
<PeekViewManagerModal />
|
<PeekViewManagerModal />
|
||||||
{workspace?.flavour === WorkspaceFlavour.LOCAL && <LocalQuotaModal />}
|
{workspace?.flavour === 'local' ? (
|
||||||
{workspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
|
<LocalQuotaModal />
|
||||||
|
) : (
|
||||||
<CloudQuotaModal />
|
<CloudQuotaModal />
|
||||||
)}
|
)}
|
||||||
<AiLoginRequiredModal />
|
<AiLoginRequiredModal />
|
||||||
@@ -112,5 +137,6 @@ export const WorkspaceLayout = ({
|
|||||||
</SWRConfigProvider>
|
</SWRConfigProvider>
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
|
</FrameworkScope>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user