mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 16:44:56 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e7a4120aa | ||
|
|
a61ded3f25 | ||
|
|
f48cd0dfef | ||
|
|
486044f0fb | ||
|
|
6da566c5f6 | ||
|
|
98e218af93 | ||
|
|
b036f1b5c9 | ||
|
|
8881286025 | ||
|
|
b8333de119 | ||
|
|
0d3180fd94 | ||
|
|
5bf9351be4 | ||
|
|
419f1b34b3 | ||
|
|
1b91ffa6a5 | ||
|
|
431ed770fa | ||
|
|
dd45c80cc4 | ||
|
|
48de982a6b | ||
|
|
261d413607 | ||
|
|
b723dd8ab8 | ||
|
|
1cf0263def | ||
|
|
b557c6e6e5 | ||
|
|
df6d0a2750 | ||
|
|
644bd8c817 | ||
|
|
4ebe8f5fb4 | ||
|
|
f94306703a | ||
|
|
3e23878e0f | ||
|
|
bd1733b2a9 | ||
|
|
31f7f6c9cf | ||
|
|
8af064b663 | ||
|
|
b8a1fbd6c7 | ||
|
|
e2b057cb93 | ||
|
|
9ac8f3177e | ||
|
|
931e9968b8 | ||
|
|
c07c7c0969 | ||
|
|
f5dceda0cc | ||
|
|
203459679c |
12
.github/renovate.json
vendored
12
.github/renovate.json
vendored
@@ -13,7 +13,7 @@
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["napi", "napi-build", "napi-derive"],
|
||||
"matchDepNames": ["napi", "napi-build", "napi-derive"],
|
||||
"rangeStrategy": "replace",
|
||||
"groupName": "napi-rs"
|
||||
},
|
||||
@@ -33,11 +33,7 @@
|
||||
"groupName": "opentelemetry"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"@prisma/client",
|
||||
"@prisma/instrumentation",
|
||||
"prisma"
|
||||
],
|
||||
"matchDepNames": ["@prisma/client", "@prisma/instrumentation", "prisma"],
|
||||
"rangeStrategy": "replace",
|
||||
"groupName": "prisma"
|
||||
},
|
||||
@@ -47,7 +43,7 @@
|
||||
"groupName": "electron-forge"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["oxlint"],
|
||||
"matchDepNames": ["oxlint"],
|
||||
"rangeStrategy": "replace",
|
||||
"groupName": "oxlint"
|
||||
},
|
||||
@@ -73,7 +69,7 @@
|
||||
{
|
||||
"groupName": "rust toolchain",
|
||||
"matchManagers": ["custom.regex"],
|
||||
"matchPackageNames": ["rustc"]
|
||||
"matchDepNames": ["rustc"]
|
||||
}
|
||||
],
|
||||
"commitMessagePrefix": "chore: ",
|
||||
|
||||
1
.github/workflows/build-test.yml
vendored
1
.github/workflows/build-test.yml
vendored
@@ -351,6 +351,7 @@ jobs:
|
||||
env:
|
||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
|
||||
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
|
||||
2
.github/workflows/release-desktop.yml
vendored
2
.github/workflows/release-desktop.yml
vendored
@@ -123,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Signing By Apple Developer ID
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
uses: apple-actions/import-codesign-certs@v2
|
||||
uses: apple-actions/import-codesign-certs@v3
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"rules": {
|
||||
// allow
|
||||
"import/named": "allow",
|
||||
"no-await-in-loop": "allow",
|
||||
// deny
|
||||
"unicorn/prefer-array-some": "error",
|
||||
"unicorn/no-useless-promise-resolve-reject": "error",
|
||||
"import/no-cycle": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"lint:eslint:fix": "yarn lint:eslint --fix",
|
||||
"lint:prettier": "prettier --ignore-unknown --cache --check .",
|
||||
"lint:prettier:fix": "prettier --ignore-unknown --cache --write .",
|
||||
"lint:ox": "oxlint -c oxlint.json --import-plugin --deny-warnings -D correctness -D nursery -D prefer-array-some -D no-useless-promise-resolve-reject -D perf -A no-undef -A consistent-type-exports -A default -A named -A ban-ts-comment -A export -A no-unresolved -A no-default-export -A no-duplicates -A no-side-effects-in-initialization -A no-named-as-default -A getter-return -A no-barrel-file -A no-await-in-loop",
|
||||
"lint:ox": "oxlint -c oxlint.json --deny-warnings --import-plugin -D correctness -D perf",
|
||||
"lint": "yarn lint:eslint && yarn lint:prettier",
|
||||
"lint:fix": "yarn lint:eslint:fix && yarn lint:prettier:fix",
|
||||
"test": "vitest --run",
|
||||
@@ -59,7 +59,7 @@
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@magic-works/i18n-codegen": "^0.6.0",
|
||||
"@nx/vite": "19.0.1",
|
||||
"@nx/vite": "19.0.3",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@taplo/cli": "^0.7.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.54",
|
||||
"@napi-rs/cli": "3.0.0-alpha.55",
|
||||
"lib0": "^0.2.93",
|
||||
"nx": "^19.0.0",
|
||||
"nx-cloud": "^18.0.0",
|
||||
"nx-cloud": "^19.0.0",
|
||||
"yjs": "^13.6.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.10.2",
|
||||
"@aws-sdk/client-s3": "^3.552.0",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.17.0",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.18.0",
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0",
|
||||
"@google-cloud/opentelemetry-resource-util": "^2.1.0",
|
||||
"@keyv/redis": "^2.8.4",
|
||||
|
||||
@@ -102,7 +102,9 @@ export class DocHistoryManager {
|
||||
description: 'How many times the snapshot history created',
|
||||
})
|
||||
.add(1);
|
||||
this.logger.log(`History created for ${id} in workspace ${workspaceId}.`);
|
||||
this.logger.debug(
|
||||
`History created for ${id} in workspace ${workspaceId}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,10 +72,12 @@ export class QuotaManagementService {
|
||||
const total = usedSize + recvSize;
|
||||
// only skip total storage check if workspace has unlimited feature
|
||||
if (total > quota && !unlimited) {
|
||||
this.logger.log(`storage size limit exceeded: ${total} > ${quota}`);
|
||||
this.logger.warn(`storage size limit exceeded: ${total} > ${quota}`);
|
||||
return true;
|
||||
} else if (recvSize > blobLimit) {
|
||||
this.logger.log(`blob size limit exceeded: ${recvSize} > ${blobLimit}`);
|
||||
this.logger.warn(
|
||||
`blob size limit exceeded: ${recvSize} > ${blobLimit}`
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { refreshPrompts } from './utils/prompts';
|
||||
|
||||
export class UpdatePrompts1715672224087 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
await refreshPrompts(db);
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(db: PrismaClient) {
|
||||
await db.aiPrompt.updateMany({
|
||||
where: {
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
data: {
|
||||
name: 'gpt-4-vision-preview',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type Prompt = {
|
||||
export const prompts: Prompt[] = [
|
||||
{
|
||||
name: 'debug:chat:gpt4',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -27,7 +27,7 @@ export const prompts: Prompt[] = [
|
||||
},
|
||||
{
|
||||
name: 'chat:gpt4',
|
||||
model: 'gpt-4-vision-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -39,13 +39,13 @@ export const prompts: Prompt[] = [
|
||||
{
|
||||
name: 'debug:action:gpt4',
|
||||
action: 'text',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
name: 'debug:action:vision4',
|
||||
action: 'text',
|
||||
model: 'gpt-4-vision-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
@@ -69,7 +69,7 @@ export const prompts: Prompt[] = [
|
||||
{
|
||||
name: 'Summary',
|
||||
action: 'Summary',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -81,7 +81,7 @@ export const prompts: Prompt[] = [
|
||||
{
|
||||
name: 'Summary the webpage',
|
||||
action: 'Summary the webpage',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -93,7 +93,7 @@ export const prompts: Prompt[] = [
|
||||
{
|
||||
name: 'Explain this',
|
||||
action: 'Explain this',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -128,7 +128,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Explain this code',
|
||||
action: 'Explain this code',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -140,7 +140,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Translate to',
|
||||
action: 'Translate',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -166,7 +166,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Write an article about this',
|
||||
action: 'Write an article about this',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -191,7 +191,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Write a twitter about this',
|
||||
action: 'Write a twitter about this',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -203,7 +203,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Write a poem about this',
|
||||
action: 'Write a poem about this',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -215,7 +215,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Write a blog post about this',
|
||||
action: 'Write a blog post about this',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -229,7 +229,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Write outline',
|
||||
action: 'Write outline',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -241,7 +241,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Change tone to',
|
||||
action: 'Change tone',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -262,7 +262,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Brainstorm ideas about this',
|
||||
action: 'Brainstorm ideas about this',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -286,7 +286,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Brainstorm mindmap',
|
||||
action: 'Brainstorm mindmap',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -298,7 +298,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Expand mind map',
|
||||
action: 'Expand mind map',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -316,7 +316,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Improve writing for it',
|
||||
action: 'Improve writing for it',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -328,7 +328,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Improve grammar for it',
|
||||
action: 'Improve grammar for it',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -340,7 +340,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Fix spelling for it',
|
||||
action: 'Fix spelling for it',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -352,7 +352,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Find action items from it',
|
||||
action: 'Find action items from it',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -374,7 +374,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Check code error',
|
||||
action: 'Check code error',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -386,7 +386,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Create a presentation',
|
||||
action: 'Create a presentation',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -398,7 +398,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Create headings',
|
||||
action: 'Create headings',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -487,7 +487,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Make it longer',
|
||||
action: 'Make it longer',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -512,7 +512,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Make it shorter',
|
||||
action: 'Make it shorter',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -536,7 +536,7 @@ content: {{content}}`,
|
||||
{
|
||||
name: 'Continue writing',
|
||||
action: 'Continue writing',
|
||||
model: 'gpt-4-turbo-preview',
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
|
||||
@@ -42,7 +42,7 @@ export class CacheInterceptor implements NestInterceptor {
|
||||
if (preventKey) {
|
||||
const key = await this.getCacheKey(ctx, preventKey);
|
||||
if (key) {
|
||||
this.logger.debug(`cache ${key} staled`);
|
||||
this.logger.verbose(`cache ${key} staled`);
|
||||
await this.cache.delete(key);
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ export class CacheInterceptor implements NestInterceptor {
|
||||
const cachedData = await this.cache.get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
this.logger.debug(`cache ${cacheKey} hit`);
|
||||
this.logger.verbose(`cache ${cacheKey} hit`);
|
||||
return of(cachedData);
|
||||
} else {
|
||||
this.logger.debug(`cache ${cacheKey} miss`);
|
||||
this.logger.verbose(`cache ${cacheKey} miss`);
|
||||
return next.handle().pipe(
|
||||
mergeMap(async result => {
|
||||
await this.cache.set(cacheKey, result);
|
||||
|
||||
@@ -133,7 +133,7 @@ export class CopilotController {
|
||||
@Query() params: Record<string, string | string[]>
|
||||
): Promise<string> {
|
||||
const { model } = await this.checkRequest(user.id, sessionId);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
const provider = await this.provider.getProviderByCapability(
|
||||
CopilotCapability.TextToText,
|
||||
model
|
||||
);
|
||||
@@ -179,7 +179,7 @@ export class CopilotController {
|
||||
): Promise<Observable<ChatEvent>> {
|
||||
try {
|
||||
const { model } = await this.checkRequest(user.id, sessionId);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
const provider = await this.provider.getProviderByCapability(
|
||||
CopilotCapability.TextToText,
|
||||
model
|
||||
);
|
||||
@@ -246,7 +246,7 @@ export class CopilotController {
|
||||
sessionId,
|
||||
messageId
|
||||
);
|
||||
const provider = this.provider.getProviderByCapability(
|
||||
const provider = await this.provider.getProviderByCapability(
|
||||
hasAttachment
|
||||
? CopilotCapability.ImageToImage
|
||||
: CopilotCapability.TextToImage,
|
||||
|
||||
@@ -50,7 +50,7 @@ export class FalProvider
|
||||
return FalProvider.capabilities;
|
||||
}
|
||||
|
||||
isModelAvailable(model: string): boolean {
|
||||
async isModelAvailable(model: string): Promise<boolean> {
|
||||
return this.availableModels.includes(model);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,11 @@ export function registerCopilotProvider<
|
||||
const providerConfig = config.plugins.copilot?.[type];
|
||||
if (!provider.assetsConfig(providerConfig as C)) {
|
||||
throw new Error(
|
||||
`Invalid configuration for copilot provider ${type}: ${providerConfig}`
|
||||
`Invalid configuration for copilot provider ${type}: ${JSON.stringify(providerConfig)}`
|
||||
);
|
||||
}
|
||||
const instance = new provider(providerConfig as C);
|
||||
logger.log(
|
||||
logger.debug(
|
||||
`Copilot provider ${type} registered, capabilities: ${provider.capabilities.join(', ')}`
|
||||
);
|
||||
|
||||
@@ -116,11 +116,11 @@ export class CopilotProviderService {
|
||||
return this.cachedProviders.get(provider)!;
|
||||
}
|
||||
|
||||
getProviderByCapability<C extends CopilotCapability>(
|
||||
async getProviderByCapability<C extends CopilotCapability>(
|
||||
capability: C,
|
||||
model?: string,
|
||||
prefer?: CopilotProviderType
|
||||
): CapabilityToCopilotProvider[C] | null {
|
||||
): Promise<CapabilityToCopilotProvider[C] | null> {
|
||||
const providers = PROVIDER_CAPABILITY_MAP.get(capability);
|
||||
if (Array.isArray(providers) && providers.length) {
|
||||
let selectedProvider: CopilotProviderType | undefined = prefer;
|
||||
@@ -137,7 +137,7 @@ export class CopilotProviderService {
|
||||
const provider = this.getProvider(selectedProvider);
|
||||
if (provider.getCapabilities().includes(capability)) {
|
||||
if (model) {
|
||||
if (provider.isModelAvailable(model)) {
|
||||
if (await provider.isModelAvailable(model)) {
|
||||
return provider as CapabilityToCopilotProvider[C];
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ClientOptions, OpenAI } from 'openai';
|
||||
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ export class OpenAIProvider
|
||||
|
||||
readonly availableModels = [
|
||||
// text to text
|
||||
'gpt-4o',
|
||||
'gpt-4-vision-preview',
|
||||
'gpt-4-turbo-preview',
|
||||
'gpt-3.5-turbo',
|
||||
@@ -51,7 +53,9 @@ export class OpenAIProvider
|
||||
'dall-e-3',
|
||||
];
|
||||
|
||||
private readonly logger = new Logger(OpenAIProvider.type);
|
||||
private readonly instance: OpenAI;
|
||||
private existsModels: string[] | undefined;
|
||||
|
||||
constructor(config: ClientOptions) {
|
||||
assert(OpenAIProvider.assetsConfig(config));
|
||||
@@ -70,8 +74,20 @@ export class OpenAIProvider
|
||||
return OpenAIProvider.capabilities;
|
||||
}
|
||||
|
||||
isModelAvailable(model: string): boolean {
|
||||
return this.availableModels.includes(model);
|
||||
async isModelAvailable(model: string): Promise<boolean> {
|
||||
const knownModels = this.availableModels.includes(model);
|
||||
if (knownModels) return true;
|
||||
|
||||
if (!this.existsModels) {
|
||||
try {
|
||||
this.existsModels = await this.instance.models
|
||||
.list()
|
||||
.then(({ data }) => data.map(m => m.id));
|
||||
} catch (e) {
|
||||
this.logger.error('Failed to fetch online model list', e);
|
||||
}
|
||||
}
|
||||
return !!this.existsModels?.includes(model);
|
||||
}
|
||||
|
||||
protected chatToGPTMessage(
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface CopilotConfig {
|
||||
|
||||
export enum AvailableModels {
|
||||
// text to text
|
||||
Gpt4Omni = 'gpt-4o',
|
||||
Gpt4VisionPreview = 'gpt-4-vision-preview',
|
||||
Gpt4TurboPreview = 'gpt-4-turbo-preview',
|
||||
Gpt35Turbo = 'gpt-3.5-turbo',
|
||||
@@ -172,7 +173,7 @@ export type CopilotImageOptions = z.infer<typeof CopilotImageOptionsSchema>;
|
||||
export interface CopilotProvider {
|
||||
readonly type: CopilotProviderType;
|
||||
getCapabilities(): CopilotCapability[];
|
||||
isModelAvailable(model: string): boolean;
|
||||
isModelAvailable(model: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface CopilotTextToTextProvider extends CopilotProvider {
|
||||
|
||||
@@ -42,7 +42,7 @@ export class RedisMutexLocker implements ILocker {
|
||||
|
||||
async lock(owner: string, key: string): Promise<Lock> {
|
||||
const lockKey = `MutexLock:${key}`;
|
||||
this.logger.debug(`Client ${owner} is trying to lock resource ${key}`);
|
||||
this.logger.verbose(`Client ${owner} is trying to lock resource ${key}`);
|
||||
|
||||
const success = await this.redis.sendCommand(
|
||||
new Command('EVAL', [lockScript, '1', lockKey, owner])
|
||||
|
||||
@@ -36,7 +36,7 @@ test.beforeEach(async t => {
|
||||
plugins: {
|
||||
copilot: {
|
||||
openai: {
|
||||
apiKey: '1',
|
||||
apiKey: process.env.COPILOT_OPENAI_API_KEY ?? '1',
|
||||
},
|
||||
fal: {
|
||||
apiKey: '1',
|
||||
@@ -368,7 +368,9 @@ test('should be able to get provider', async t => {
|
||||
const { provider } = t.context;
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.TextToText);
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.TextToText
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
@@ -377,7 +379,7 @@ test('should be able to get provider', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.TextToEmbedding
|
||||
);
|
||||
t.is(
|
||||
@@ -388,7 +390,9 @@ test('should be able to get provider', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.TextToImage);
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.TextToImage
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'fal',
|
||||
@@ -397,7 +401,9 @@ test('should be able to get provider', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.ImageToImage);
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.ImageToImage
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'fal',
|
||||
@@ -406,7 +412,9 @@ test('should be able to get provider', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
const p = provider.getProviderByCapability(CopilotCapability.ImageToText);
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.ImageToText
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
@@ -417,7 +425,7 @@ test('should be able to get provider', async t => {
|
||||
// text-to-image use fal by default, but this case can use
|
||||
// model dall-e-3 to select openai provider
|
||||
{
|
||||
const p = provider.getProviderByCapability(
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.TextToImage,
|
||||
'dall-e-3'
|
||||
);
|
||||
@@ -427,14 +435,38 @@ test('should be able to get provider', async t => {
|
||||
'should get provider support text-to-image and model'
|
||||
);
|
||||
}
|
||||
|
||||
// gpt4o is not defined now, but it already published by openai
|
||||
// we should check from online api if it is available
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.ImageToText,
|
||||
'gpt-4o'
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'should get provider support text-to-image and model'
|
||||
);
|
||||
}
|
||||
|
||||
// if a model is not defined and not available in online api
|
||||
// it should return null
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.ImageToText,
|
||||
'gpt-4-not-exist'
|
||||
);
|
||||
t.falsy(p, 'should not get provider');
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to register test provider', async t => {
|
||||
const { provider } = t.context;
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
|
||||
const assertProvider = (cap: CopilotCapability) => {
|
||||
const p = provider.getProviderByCapability(cap, 'test');
|
||||
const assertProvider = async (cap: CopilotCapability) => {
|
||||
const p = await provider.getProviderByCapability(cap, 'test');
|
||||
t.is(
|
||||
p?.type,
|
||||
CopilotProviderType.Test,
|
||||
@@ -442,9 +474,9 @@ test('should be able to register test provider', async t => {
|
||||
);
|
||||
};
|
||||
|
||||
assertProvider(CopilotCapability.TextToText);
|
||||
assertProvider(CopilotCapability.TextToEmbedding);
|
||||
assertProvider(CopilotCapability.TextToImage);
|
||||
assertProvider(CopilotCapability.ImageToImage);
|
||||
assertProvider(CopilotCapability.ImageToText);
|
||||
await assertProvider(CopilotCapability.TextToText);
|
||||
await assertProvider(CopilotCapability.TextToEmbedding);
|
||||
await assertProvider(CopilotCapability.TextToImage);
|
||||
await assertProvider(CopilotCapability.ImageToImage);
|
||||
await assertProvider(CopilotCapability.ImageToText);
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export class MockCopilotTestProvider
|
||||
return MockCopilotTestProvider.capabilities;
|
||||
}
|
||||
|
||||
override isModelAvailable(model: string): boolean {
|
||||
override async isModelAvailable(model: string): Promise<boolean> {
|
||||
return this.availableModels.includes(model);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ test('should be able calc quota after switch plan', async t => {
|
||||
);
|
||||
t.is(size1, 0, 'failed to check free plan blob size');
|
||||
|
||||
quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||
|
||||
const size2 = await checkBlobSize(
|
||||
app,
|
||||
|
||||
4
packages/common/env/package.json
vendored
4
packages/common/env/package.json
vendored
@@ -3,8 +3,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@blocksuite/global": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/store": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/global": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/store": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"vitest": "1.6.0"
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/global": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/store": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/blocks": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/global": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/store": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@datastructures-js/binary-search-tree": "^5.3.2",
|
||||
"foxact": "^0.2.33",
|
||||
"jotai": "^2.8.0",
|
||||
@@ -28,8 +28,8 @@
|
||||
"devDependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/presets": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/block-std": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/presets": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"async-call-rpc": "^6.4.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -37,6 +37,10 @@ export class EventBus {
|
||||
}
|
||||
}
|
||||
|
||||
get root(): EventBus {
|
||||
return this.parent?.root ?? this;
|
||||
}
|
||||
|
||||
on<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
|
||||
if (!this.listeners[id]) {
|
||||
this.listeners[id] = [];
|
||||
|
||||
@@ -75,12 +75,12 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/block-std": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/global": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/block-std": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/blocks": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/global": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/icons": "2.1.50",
|
||||
"@blocksuite/presets": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/store": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/presets": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/store": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@storybook/addon-actions": "^7.6.17",
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-interactions": "^7.6.17",
|
||||
|
||||
@@ -27,7 +27,7 @@ export const scrollableViewport = style({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
globalStyle(`${scrollableViewport} > div`, {
|
||||
globalStyle(`${scrollableViewport} >:first-child`, {
|
||||
display: 'contents !important',
|
||||
});
|
||||
export const scrollableContainer = style({
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/global": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/block-std": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/blocks": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/global": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/icons": "2.1.50",
|
||||
"@blocksuite/inline": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/presets": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/store": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/inline": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/presets": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/store": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { createStore } from 'jotai';
|
||||
|
||||
import { openSettingModalAtom, openWorkspaceListModalAtom } from '../atoms';
|
||||
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { mixpanel } from '../utils/mixpanel';
|
||||
|
||||
export function registerAffineNavigationCommands({
|
||||
t,
|
||||
@@ -76,6 +77,10 @@ export function registerAffineNavigationCommands({
|
||||
label: t['com.affine.cmdk.affine.navigation.open-settings'](),
|
||||
keyBinding: '$mod+,',
|
||||
run() {
|
||||
mixpanel.track('SettingsViewed', {
|
||||
// page:
|
||||
segment: 'cmdk',
|
||||
});
|
||||
store.set(openSettingModalAtom, s => ({
|
||||
activeTab: 'appearance',
|
||||
open: !s.open,
|
||||
@@ -84,6 +89,25 @@ export function registerAffineNavigationCommands({
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:open-account',
|
||||
category: 'affine:navigation',
|
||||
icon: <ArrowRightBigIcon />,
|
||||
label: t['com.affine.cmdk.affine.navigation.open-account-settings'](),
|
||||
run() {
|
||||
mixpanel.track('AccountSettingsViewed', {
|
||||
// page:
|
||||
segment: 'cmdk',
|
||||
});
|
||||
store.set(openSettingModalAtom, s => ({
|
||||
activeTab: 'account',
|
||||
open: !s.open,
|
||||
}));
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:goto-trash',
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { useTheme } from 'next-themes';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../atoms';
|
||||
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
|
||||
import { mixpanel } from '../utils';
|
||||
|
||||
export function registerAffineSettingsCommands({
|
||||
t,
|
||||
@@ -38,6 +39,9 @@ export function registerAffineSettingsCommands({
|
||||
label: '',
|
||||
icon: <SettingsIcon />,
|
||||
run() {
|
||||
mixpanel.track('QuickSearchOpened', {
|
||||
control: 'shortcut',
|
||||
});
|
||||
const quickSearchModalState = store.get(openQuickSearchModalAtom);
|
||||
|
||||
if (!editor) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, FlexWrapper, notify } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { AiIcon } from '@blocksuite/icons';
|
||||
@@ -69,6 +70,11 @@ export const AIOnboardingEdgeless = ({
|
||||
const mode = useLiveData(doc.mode$);
|
||||
|
||||
const goToPricingPlans = useCallback(() => {
|
||||
mixpanel.track('PlansViewed', {
|
||||
page: 'whiteboard editor',
|
||||
segment: 'ai onboarding',
|
||||
module: 'whiteboard dialog',
|
||||
});
|
||||
setSettingModal({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button, IconButton, Modal } from '@affine/component';
|
||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { useBlurRoot } from '@affine/core/hooks/use-blur-root';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -122,6 +123,11 @@ export const AIOnboardingGeneral = ({
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
mixpanel.track('PlansViewed', {
|
||||
page: 'whiteboard-editor',
|
||||
segment: 'ai onboarding',
|
||||
module: 'general',
|
||||
});
|
||||
closeAndDismiss();
|
||||
}, [closeAndDismiss, setSettingModal]);
|
||||
const onPrev = useCallback(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -40,6 +41,10 @@ export const UserPlanButton = () => {
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
});
|
||||
mixpanel.track('PlansViewed', {
|
||||
segment: 'settings panel',
|
||||
module: 'profile and badge',
|
||||
});
|
||||
},
|
||||
[setSettingModalAtom]
|
||||
);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
||||
import { timestampToLocalDate } from '@affine/core/utils';
|
||||
import {
|
||||
type CalendarTranslation,
|
||||
timestampToCalendarDate,
|
||||
} from '@affine/core/utils';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { ListHistoryQuery } from '@affine/graphql';
|
||||
import { listHistoryQuery, recoverDocMutation } from '@affine/graphql';
|
||||
@@ -174,10 +177,13 @@ export const useSnapshotPage = (
|
||||
return page;
|
||||
};
|
||||
|
||||
export const historyListGroupByDay = (histories: DocHistory[]) => {
|
||||
export const historyListGroupByDay = (
|
||||
histories: DocHistory[],
|
||||
translation: CalendarTranslation
|
||||
) => {
|
||||
const map = new Map<string, DocHistory[]>();
|
||||
for (const history of histories) {
|
||||
const day = timestampToLocalDate(history.timestamp);
|
||||
const day = timestampToCalendarDate(history.timestamp, translation);
|
||||
const list = map.get(day) ?? [];
|
||||
list.push(history);
|
||||
map.set(day, list);
|
||||
|
||||
@@ -33,7 +33,11 @@ import {
|
||||
import { encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
||||
import { mixpanel, timestampToLocalTime } from '../../../utils';
|
||||
import {
|
||||
type CalendarTranslation,
|
||||
mixpanel,
|
||||
timestampToLocalTime,
|
||||
} from '../../../utils';
|
||||
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
|
||||
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
||||
import {
|
||||
@@ -225,6 +229,9 @@ const PlanPrompt = () => {
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
});
|
||||
mixpanel.track('PlansViewed', {
|
||||
segment: 'doc history',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -233,7 +240,7 @@ const PlanPrompt = () => {
|
||||
return (
|
||||
<div className={styles.planPromptTitle}>
|
||||
{
|
||||
isProWorkspace === null
|
||||
isProWorkspace !== null
|
||||
? !isProWorkspace
|
||||
? t[
|
||||
'com.affine.history.confirm-restore-modal.plan-prompt.limited-title'
|
||||
@@ -308,14 +315,19 @@ const PageHistoryList = ({
|
||||
onLoadMore: (() => void) | false;
|
||||
loadingMore: boolean;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const historyListByDay = useMemo(() => {
|
||||
return historyListGroupByDay(historyList);
|
||||
}, [historyList]);
|
||||
const translation: CalendarTranslation = {
|
||||
yesterday: t['com.affine.yesterday'],
|
||||
today: t['com.affine.today'],
|
||||
tomorrow: t['com.affine.tomorrow'],
|
||||
nextWeek: t['com.affine.nextWeek'],
|
||||
};
|
||||
return historyListGroupByDay(historyList, translation);
|
||||
}, [historyList, t]);
|
||||
|
||||
const [collapsedMap, setCollapsedMap] = useState<Record<number, boolean>>({});
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (historyList.length > 0 && !activeVersion) {
|
||||
onVersionChange(historyList[0].timestamp);
|
||||
|
||||
@@ -67,7 +67,7 @@ export const previewContainer = style({
|
||||
},
|
||||
])
|
||||
),
|
||||
'&[data-distance="> 20"]': {
|
||||
'&[data-distance="20"],&[data-distance="> 20"]': {
|
||||
transform: `scale(0) translateY(calc(${-8 * 20}px + ${previewTopOffset}))`,
|
||||
opacity: 0,
|
||||
zIndex: -20,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { openQuotaModalAtom, openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { UserQuotaService } from '@affine/core/modules/cloud';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import bytes from 'bytes';
|
||||
@@ -48,6 +49,11 @@ export const CloudQuotaModal = () => {
|
||||
activeTab: 'plans',
|
||||
});
|
||||
|
||||
mixpanel.track('PlansViewed', {
|
||||
segment: 'payment wall',
|
||||
category: 'payment wall storage',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
}, [setOpen, setSettingModalAtom]);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SubscriptionService,
|
||||
UserCopilotQuotaService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
@@ -46,6 +47,12 @@ export const AIUsagePanel = () => {
|
||||
open: true,
|
||||
activeTab: 'billing',
|
||||
});
|
||||
mixpanel.track('BillingViewed', {
|
||||
segment: 'settings panel',
|
||||
module: 'account usage list',
|
||||
control: 'change plan button',
|
||||
type: 'ai subscription',
|
||||
});
|
||||
}, [setOpenSettingModal]);
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -162,8 +162,11 @@ const StoragePanel = () => {
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const onUpgrade = useCallback(() => {
|
||||
mixpanel.track('Button', {
|
||||
resolve: 'UpgradeStorage',
|
||||
mixpanel.track('PlansViewed', {
|
||||
segment: 'settings panel',
|
||||
module: 'account usage list',
|
||||
control: 'cloud storage upgrade button',
|
||||
type: 'cloud subscription',
|
||||
});
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
|
||||
@@ -108,17 +108,22 @@ const SubscriptionSettings = () => {
|
||||
|
||||
const openPlans = useCallback(
|
||||
(scrollAnchor?: string) => {
|
||||
mixpanel.track('Button', {
|
||||
resolve: 'ChangePlan',
|
||||
currentPlan: proSubscription?.plan,
|
||||
mixpanel.track('PlansViewed', {
|
||||
type: proSubscription?.plan,
|
||||
category: proSubscription?.recurring,
|
||||
// page:
|
||||
segment: 'settings panel',
|
||||
module: 'billing subscription list',
|
||||
control: 'change plan button',
|
||||
});
|
||||
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: scrollAnchor,
|
||||
});
|
||||
},
|
||||
[proSubscription?.plan, setOpenSettingModalAtom]
|
||||
[proSubscription?.plan, proSubscription?.recurring, setOpenSettingModalAtom]
|
||||
);
|
||||
const gotoCloudPlansSetting = useCallback(() => openPlans(), [openPlans]);
|
||||
const gotoAiPlanSetting = useCallback(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, type ButtonProps } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { popupWindow } from '@affine/core/utils';
|
||||
import { mixpanel, popupWindow } from '@affine/core/utils';
|
||||
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -42,6 +42,10 @@ export const AISubscribe = ({ ...btnProps }: AISubscribeProps) => {
|
||||
|
||||
const subscribe = useAsyncCallback(async () => {
|
||||
setMutating(true);
|
||||
mixpanel.track('plan upgrade started', {
|
||||
category: SubscriptionRecurring.Yearly,
|
||||
type: SubscriptionPlan.AI,
|
||||
});
|
||||
try {
|
||||
const session = await subscriptionService.createCheckoutSession({
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
|
||||
@@ -115,15 +115,22 @@ export const SettingSidebar = ({
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
const generalList = useGeneralSettingList();
|
||||
const onAccountSettingClick = useCallback(() => {
|
||||
mixpanel.track('Button', {
|
||||
resolve: 'AccountSetting',
|
||||
mixpanel.track('AccountSettingsViewed', {
|
||||
// page:
|
||||
segment: 'settings panel',
|
||||
module: 'settings menu',
|
||||
control: 'menu item',
|
||||
});
|
||||
onTabChange('account', null);
|
||||
}, [onTabChange]);
|
||||
const onWorkspaceSettingClick = useCallback(
|
||||
(subTab: WorkspaceSubTab, workspaceMetadata: WorkspaceMetadata) => {
|
||||
mixpanel.track('Button', {
|
||||
resolve: 'WorkspaceSetting',
|
||||
mixpanel.track(`view workspace setting`, {
|
||||
// page:
|
||||
segment: 'settings panel',
|
||||
module: 'settings menu',
|
||||
control: 'menu item',
|
||||
type: subTab,
|
||||
workspaceId: workspaceMetadata.id,
|
||||
});
|
||||
onTabChange(`workspace:${subTab}`, workspaceMetadata);
|
||||
@@ -148,9 +155,21 @@ export const SettingSidebar = ({
|
||||
key={key}
|
||||
title={title}
|
||||
onClick={() => {
|
||||
mixpanel.track('Button', {
|
||||
resolve: key,
|
||||
});
|
||||
if (key === 'billing') {
|
||||
mixpanel.track('BillingViewed', {
|
||||
// page:
|
||||
segment: 'settings panel',
|
||||
module: 'settings menu',
|
||||
control: 'menu item',
|
||||
});
|
||||
} else if (key === 'plans') {
|
||||
mixpanel.track('PlansViewed', {
|
||||
// page:
|
||||
segment: 'settings panel',
|
||||
module: 'settings menu',
|
||||
control: 'menu item',
|
||||
});
|
||||
}
|
||||
onTabChange(key, null);
|
||||
}}
|
||||
data-testid={testId}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useMembers } from '@affine/core/hooks/affine/use-members';
|
||||
import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Permission } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -144,6 +145,12 @@ export const CloudWorkspaceMembersPanel = () => {
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
});
|
||||
mixpanel.track('PlansViewed', {
|
||||
// page:
|
||||
segment: 'settings panel',
|
||||
module: 'workspace setting',
|
||||
control: 'invite member',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Button } from '@affine/component/ui/button';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { ShareService } from '@affine/core/modules/share-doc';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -101,6 +102,12 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
await shareService.share.enableShare(
|
||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
||||
);
|
||||
mixpanel.track('ShareCreated', {
|
||||
segment: 'sharing panel',
|
||||
module: 'public share',
|
||||
control: 'share panel',
|
||||
type: mode,
|
||||
});
|
||||
notify.success({
|
||||
title:
|
||||
t[
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -52,10 +53,14 @@ export const useSharingUrl = ({
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
mixpanel.track('ShareLinkCopied', {
|
||||
module: urlType === 'share' ? 'public share' : 'private share',
|
||||
type: 'link',
|
||||
});
|
||||
} else {
|
||||
toast('Network not available');
|
||||
}
|
||||
}, [sharingUrl, t]);
|
||||
}, [sharingUrl, t, urlType]);
|
||||
|
||||
return {
|
||||
sharingUrl,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { authAtom, openSettingModalAtom } from '@affine/core/atoms';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { getBaseUrl } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { UnauthorizedError } from '@blocksuite/blocks';
|
||||
@@ -345,6 +346,11 @@ Could you make a new website based on these notes and send back just the html fi
|
||||
getCurrentStore().set(openSettingModalAtom, {
|
||||
activeTab: 'billing',
|
||||
open: true,
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
mixpanel.track('PlansViewed', {
|
||||
segment: 'payment wall',
|
||||
category: 'payment wall ai action count',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.docEditorRoot}>
|
||||
<div className={styles.affineDocViewport}>
|
||||
<>
|
||||
<div className={styles.affineDocViewport} style={{ height: '100%' }}>
|
||||
{!isJournal ? (
|
||||
<adapted.DocTitle doc={page} ref={titleRef} />
|
||||
) : (
|
||||
@@ -133,7 +133,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
{portals.map(p => (
|
||||
<Fragment key={p.id}>{p.portal}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { ElementOrFactory } from '@affine/component';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { BlockSpec } from '@blocksuite/block-std';
|
||||
import type { ParagraphService, RootService } from '@blocksuite/blocks';
|
||||
import {
|
||||
AffineLinkedDocWidget,
|
||||
AffineSlashMenuWidget,
|
||||
AttachmentService,
|
||||
CanvasTextFonts,
|
||||
EdgelessRootService,
|
||||
PageRootService,
|
||||
} from '@blocksuite/blocks';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import bytes from 'bytes';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
@@ -48,6 +52,37 @@ class CustomEdgelessPageService extends EdgelessRootService {
|
||||
override loadFonts(): void {
|
||||
customLoadFonts(this);
|
||||
}
|
||||
|
||||
override addElement<T = Record<string, unknown>>(type: string, props: T) {
|
||||
const res = super.addElement(type, props);
|
||||
mixpanel.track('WhiteboardObjectCreated', {
|
||||
page: 'whiteboard editor',
|
||||
module: 'whiteboard',
|
||||
segment: 'canvas',
|
||||
// control:
|
||||
type: 'whiteboard object',
|
||||
category: type,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
override addBlock(
|
||||
flavour: string,
|
||||
props: Record<string, unknown>,
|
||||
parent?: string | BlockModel,
|
||||
parentIndex?: number
|
||||
) {
|
||||
const res = super.addBlock(flavour, props, parent, parentIndex);
|
||||
mixpanel.track('WhiteboardObjectCreated', {
|
||||
page: 'whiteboard editor',
|
||||
module: 'whiteboard',
|
||||
segment: 'canvas',
|
||||
// control:
|
||||
type: 'whiteboard object',
|
||||
category: flavour.split(':')[1], // affine:paragraph -> paragraph
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
type AffineReference = HTMLElementTagNameMap['affine-reference'];
|
||||
@@ -85,6 +120,63 @@ function patchSpecsWithReferenceRenderer(
|
||||
});
|
||||
}
|
||||
|
||||
function patchSlashMenuWidget() {
|
||||
const menuGroup = AffineSlashMenuWidget.DEFAULT_OPTIONS.menus.find(group => {
|
||||
return group.name === 'Docs';
|
||||
});
|
||||
|
||||
if (Array.isArray(menuGroup?.items)) {
|
||||
const newDocItem = menuGroup.items.find(item => {
|
||||
return item.name === 'New Doc';
|
||||
});
|
||||
|
||||
if (newDocItem) {
|
||||
const oldAction = newDocItem.action;
|
||||
newDocItem.action = async (...props) => {
|
||||
await oldAction(...props);
|
||||
mixpanel.track('DocCreated', {
|
||||
segment: 'doc',
|
||||
module: 'command menu',
|
||||
control: 'new doc command',
|
||||
type: 'doc',
|
||||
category: 'doc',
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchLinkedDocPopover() {
|
||||
const oldGetMenus = AffineLinkedDocWidget.DEFAULT_OPTIONS.getMenus;
|
||||
|
||||
AffineLinkedDocWidget.DEFAULT_OPTIONS.getMenus = ctx => {
|
||||
const menus = oldGetMenus(ctx);
|
||||
const newDocGroup = menus.find(group => group.name === 'New Doc');
|
||||
const newDocItem = newDocGroup?.items.find(item => item.key === 'create');
|
||||
// todo: patch import doc/workspace action
|
||||
// const importItem = newDocGroup?.items.find(item => item.key === 'import');
|
||||
|
||||
if (newDocItem) {
|
||||
const oldAction = newDocItem.action;
|
||||
newDocItem.action = async () => {
|
||||
await oldAction();
|
||||
mixpanel.track('DocCreated', {
|
||||
segment: 'doc',
|
||||
module: 'linked doc popover',
|
||||
control: 'new doc command',
|
||||
type: 'doc',
|
||||
category: 'doc',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return menus;
|
||||
};
|
||||
}
|
||||
|
||||
patchSlashMenuWidget();
|
||||
patchLinkedDocPopover();
|
||||
|
||||
/**
|
||||
* Patch the block specs with custom renderers.
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,7 @@ export const docEditorRoot = style({
|
||||
export const affineDocViewport = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingBottom: '150px',
|
||||
paddingBottom: '100px',
|
||||
});
|
||||
|
||||
export const docContainer = style({
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
@@ -97,8 +99,34 @@ export const PageHeaderMenuButton = ({
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
duplicate(pageId);
|
||||
mixpanel.track('DocCreated', {
|
||||
segment: 'editor header',
|
||||
module: 'header menu',
|
||||
control: 'copy doc',
|
||||
type: 'doc duplicate',
|
||||
category: 'doc',
|
||||
});
|
||||
}, [duplicate, pageId]);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
if (options.isWorkspaceFile) {
|
||||
mixpanel.track('WorkspaceCreated', {
|
||||
segment: 'editor header',
|
||||
module: 'header menu',
|
||||
control: 'import button',
|
||||
type: 'imported workspace',
|
||||
});
|
||||
} else {
|
||||
mixpanel.track('DocCreated', {
|
||||
segment: 'editor header',
|
||||
module: 'header menu',
|
||||
control: 'import button',
|
||||
type: 'imported doc',
|
||||
});
|
||||
}
|
||||
}, [importFile]);
|
||||
|
||||
const EditMenu = (
|
||||
<>
|
||||
{!isJournal && (
|
||||
@@ -179,7 +207,7 @@ export const PageHeaderMenuButton = ({
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-import"
|
||||
onSelect={importFile}
|
||||
onSelect={onImportFile}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Import']()}
|
||||
|
||||
@@ -36,30 +36,47 @@ export const usePageHelper = (docCollection: DocCollection) => {
|
||||
return createPageAndOpen('edgeless');
|
||||
}, [createPageAndOpen]);
|
||||
|
||||
const importFileAndOpen = useAsyncCallback(async () => {
|
||||
const { showImportModal } = await import('@blocksuite/blocks');
|
||||
const onSuccess = (
|
||||
pageIds: string[],
|
||||
options: { isWorkspaceFile: boolean; importedCount: number }
|
||||
) => {
|
||||
toast(
|
||||
`Successfully imported ${options.importedCount} Page${
|
||||
options.importedCount > 1 ? 's' : ''
|
||||
}.`
|
||||
);
|
||||
if (options.isWorkspaceFile) {
|
||||
jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL);
|
||||
return;
|
||||
}
|
||||
const importFileAndOpen = useMemo(
|
||||
() => async () => {
|
||||
const { showImportModal } = await import('@blocksuite/blocks');
|
||||
const { promise, resolve, reject } =
|
||||
Promise.withResolvers<
|
||||
Parameters<
|
||||
NonNullable<Parameters<typeof showImportModal>[0]['onSuccess']>
|
||||
>[1]
|
||||
>();
|
||||
const onSuccess = (
|
||||
pageIds: string[],
|
||||
options: { isWorkspaceFile: boolean; importedCount: number }
|
||||
) => {
|
||||
resolve(options);
|
||||
toast(
|
||||
`Successfully imported ${options.importedCount} Page${
|
||||
options.importedCount > 1 ? 's' : ''
|
||||
}.`
|
||||
);
|
||||
if (options.isWorkspaceFile) {
|
||||
jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const pageId = pageIds[0];
|
||||
openPage(docCollection.id, pageId);
|
||||
};
|
||||
showImportModal({ collection: docCollection, onSuccess });
|
||||
}, [docCollection, openPage, jumpToSubPath]);
|
||||
if (pageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const pageId = pageIds[0];
|
||||
openPage(docCollection.id, pageId);
|
||||
};
|
||||
showImportModal({
|
||||
collection: docCollection,
|
||||
onSuccess,
|
||||
onFail: message => {
|
||||
reject(new Error(message));
|
||||
},
|
||||
});
|
||||
return await promise;
|
||||
},
|
||||
[docCollection, openPage, jumpToSubPath]
|
||||
);
|
||||
|
||||
const createLinkedPageAndOpen = useAsyncCallback(
|
||||
async (pageId: string) => {
|
||||
|
||||
@@ -25,6 +25,6 @@ export const editor = style({
|
||||
globalStyle(
|
||||
`${editor} .affine-page-viewport:not(.affine-embed-synced-doc-editor)`,
|
||||
{
|
||||
paddingBottom: '150px',
|
||||
paddingBottom: '100px',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DropdownButton, Menu } from '@affine/component';
|
||||
import { BlockCard } from '@affine/component/card/block-card';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
@@ -69,11 +70,27 @@ export const NewPageButton = ({
|
||||
const handleCreateNewPage = useCallback(() => {
|
||||
createNewPage();
|
||||
setOpen(false);
|
||||
mixpanel.track('DocCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'all doc',
|
||||
module: 'doc list header',
|
||||
control: 'new doc button',
|
||||
type: 'doc',
|
||||
category: 'page',
|
||||
});
|
||||
}, [createNewPage]);
|
||||
|
||||
const handleCreateNewEdgeless = useCallback(() => {
|
||||
createNewEdgeless();
|
||||
setOpen(false);
|
||||
mixpanel.track('DocCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'all doc',
|
||||
module: 'doc list header',
|
||||
control: 'new whiteboard button',
|
||||
type: 'doc',
|
||||
category: 'whiteboard',
|
||||
});
|
||||
}, [createNewEdgeless]);
|
||||
|
||||
const handleImportFile = useCallback(() => {
|
||||
@@ -104,10 +121,7 @@ export const NewPageButton = ({
|
||||
>
|
||||
<DropdownButton
|
||||
size={size}
|
||||
onClick={useCallback(() => {
|
||||
createNewPage();
|
||||
setOpen(false);
|
||||
}, [createNewPage])}
|
||||
onClick={handleCreateNewPage}
|
||||
onClickDropDown={useCallback(() => setOpen(open => !open), [])}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
@@ -46,6 +47,28 @@ export const PageListHeader = () => {
|
||||
return t['com.affine.all-pages.header']();
|
||||
}, [t]);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
if (options.isWorkspaceFile) {
|
||||
mixpanel.track('WorkspaceCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'all doc',
|
||||
module: 'doc list header',
|
||||
control: 'import button',
|
||||
type: 'imported workspace',
|
||||
});
|
||||
} else {
|
||||
mixpanel.track('DocCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'all doc',
|
||||
module: 'doc list header',
|
||||
control: 'import button',
|
||||
type: 'imported doc',
|
||||
// category
|
||||
});
|
||||
}
|
||||
}, [importFile]);
|
||||
|
||||
return (
|
||||
<div className={styles.docListHeader}>
|
||||
<div className={styles.docListHeaderTitle}>{title}</div>
|
||||
@@ -54,7 +77,7 @@ export const PageListHeader = () => {
|
||||
testId="new-page-button-trigger"
|
||||
onCreateEdgeless={createEdgeless}
|
||||
onCreatePage={createPage}
|
||||
onImportFile={importFile}
|
||||
onImportFile={onImportFile}
|
||||
>
|
||||
<div className={styles.buttonText}>{t['New Page']()}</div>
|
||||
</PageListNewPageButton>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TagService } from '@affine/core/modules/tag';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
|
||||
import * as styles from './group-definitions.css';
|
||||
@@ -128,7 +128,17 @@ const GroupTagLabel = ({ tag, count }: { tag: Tag; count: number }) => {
|
||||
};
|
||||
export const useTagGroupDefinitions = (): ItemGroupDefinition<ListItem>[] => {
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tags = useLiveData(tagList.tags$);
|
||||
const sortedTagsLiveData$ = useMemo(
|
||||
() =>
|
||||
LiveData.computed(get =>
|
||||
get(tagList.tags$).sort((a, b) =>
|
||||
get(a.value$).localeCompare(get(b.value$))
|
||||
)
|
||||
),
|
||||
[tagList.tags$]
|
||||
);
|
||||
const tags = useLiveData(sortedTagsLiveData$);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const untagged = useMemo(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-sui
|
||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
@@ -96,6 +97,13 @@ export const PageOperationCell = ({
|
||||
|
||||
const onDuplicate = useCallback(() => {
|
||||
duplicate(page.id, false);
|
||||
mixpanel.track('DocCreated', {
|
||||
segment: 'all doc',
|
||||
module: 'doc item menu',
|
||||
control: 'copy doc',
|
||||
type: 'doc duplicate',
|
||||
category: 'doc',
|
||||
});
|
||||
}, [duplicate, page.id]);
|
||||
|
||||
const OperationMenu = (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite
|
||||
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
@@ -235,6 +236,9 @@ export const usePageCommands = () => {
|
||||
page.id,
|
||||
blockId
|
||||
);
|
||||
mixpanel.track('AppendToJournal', {
|
||||
control: 'cmdk',
|
||||
});
|
||||
},
|
||||
icon: <TodayIcon />,
|
||||
});
|
||||
@@ -250,6 +254,10 @@ export const usePageCommands = () => {
|
||||
const page = pageHelper.createPage();
|
||||
page.load();
|
||||
pageMetaHelper.setDocTitle(page.id, query);
|
||||
mixpanel.track('DocCreated', {
|
||||
control: 'cmdk',
|
||||
type: 'doc',
|
||||
});
|
||||
},
|
||||
icon: <PageIcon />,
|
||||
});
|
||||
@@ -265,6 +273,10 @@ export const usePageCommands = () => {
|
||||
const page = pageHelper.createEdgeless();
|
||||
page.load();
|
||||
pageMetaHelper.setDocTitle(page.id, query);
|
||||
mixpanel.track('DocCreated', {
|
||||
control: 'cmdk',
|
||||
type: 'whiteboard',
|
||||
});
|
||||
},
|
||||
icon: <EdgelessIcon />,
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ export const searchInput = style({
|
||||
export const pageTitleWrapper = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '18px 24px 0 24px',
|
||||
padding: '18px 16px 0',
|
||||
width: '100%',
|
||||
});
|
||||
export const pageTitle = style({
|
||||
@@ -113,9 +113,11 @@ globalStyle(`${root} [cmdk-list]`, {
|
||||
overflow: 'auto',
|
||||
overscrollBehavior: 'contain',
|
||||
height: 'min(330px, calc(var(--cmdk-list-height) + 8px))',
|
||||
padding: '0 0 8px 6px',
|
||||
margin: '8px 6px',
|
||||
scrollbarGutter: 'stable',
|
||||
scrollPaddingBlock: '12px',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${cssVar('iconColor')} transparent`,
|
||||
});
|
||||
globalStyle(`${root} [cmdk-list]:not([data-opening])`, {
|
||||
transition: 'height .1s ease',
|
||||
|
||||
@@ -8,7 +8,14 @@ import type { CommandCategory } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { Command } from 'cmdk';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Suspense, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Suspense,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
cmdkQueryAtom,
|
||||
@@ -164,6 +171,8 @@ export const CMDKContainer = ({
|
||||
const isInEditor = pageMeta !== undefined;
|
||||
const [opening, setOpening] = useState(open);
|
||||
const { syncing, progress } = useDocEngineStatus();
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// fix list height animation on opening
|
||||
@@ -182,6 +191,25 @@ export const CMDKContainer = ({
|
||||
}
|
||||
return;
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
if (syncing && !showLoading) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setShowLoading(true);
|
||||
}, 500);
|
||||
} else if (!syncing && showLoading) {
|
||||
setShowLoading(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [syncing, showLoading]);
|
||||
|
||||
return (
|
||||
<Command
|
||||
{...rest}
|
||||
@@ -205,7 +233,7 @@ export const CMDKContainer = ({
|
||||
inEditor: isInEditor,
|
||||
})}
|
||||
>
|
||||
{syncing ? (
|
||||
{showLoading ? (
|
||||
<Loading
|
||||
size={24}
|
||||
progress={progress ? Math.max(progress, 0.2) : undefined}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IconButton } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
@@ -24,10 +25,26 @@ export const AddFavouriteButton = ({
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
createLinkedPage(pageId);
|
||||
mixpanel.track('DocCreated', {
|
||||
// page:
|
||||
segment: 'all doc',
|
||||
module: 'favorite',
|
||||
control: 'new fav sub doc',
|
||||
type: 'doc',
|
||||
category: 'page',
|
||||
});
|
||||
} else {
|
||||
const page = createPage();
|
||||
page.load();
|
||||
favAdapter.set(page.id, 'doc', true);
|
||||
mixpanel.track('DocCreated', {
|
||||
// page:
|
||||
segment: 'all doc',
|
||||
module: 'favorite',
|
||||
control: 'new fav doc',
|
||||
type: 'doc',
|
||||
category: 'page',
|
||||
});
|
||||
}
|
||||
},
|
||||
[pageId, createLinkedPage, createPage, favAdapter]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ImportIcon } from '@blocksuite/icons';
|
||||
|
||||
@@ -8,8 +10,31 @@ import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||
const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { importFile } = usePageHelper(docCollection);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
if (options.isWorkspaceFile) {
|
||||
mixpanel.track('WorkspaceCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'navigation panel',
|
||||
module: 'doc list header',
|
||||
control: 'import button',
|
||||
type: 'imported workspace',
|
||||
});
|
||||
} else {
|
||||
mixpanel.track('DocCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'navigation panel',
|
||||
module: 'doc list header',
|
||||
control: 'import button',
|
||||
type: 'imported doc',
|
||||
// category
|
||||
});
|
||||
}
|
||||
}, [importFile]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<ImportIcon />} onClick={importFile}>
|
||||
<MenuItem icon={<ImportIcon />} onClick={onImportFile}>
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AnimatedDeleteIcon } from '@affine/component';
|
||||
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
||||
@@ -106,11 +107,23 @@ export const RootAppSidebar = ({
|
||||
)
|
||||
);
|
||||
|
||||
const allPageActive = currentPath === '/all';
|
||||
|
||||
const trashActive = currentPath === '/trash';
|
||||
|
||||
const onClickNewPage = useAsyncCallback(async () => {
|
||||
const page = createPage();
|
||||
page.load();
|
||||
openPage(page.id);
|
||||
}, [createPage, openPage]);
|
||||
mixpanel.track('DocCreated', {
|
||||
page: allPageActive ? 'all' : trashActive ? 'trash' : 'other',
|
||||
segment: 'navigation panel',
|
||||
module: 'bottom button',
|
||||
control: 'new doc button',
|
||||
category: 'page',
|
||||
type: 'doc',
|
||||
});
|
||||
}, [allPageActive, createPage, openPage, trashActive]);
|
||||
|
||||
const { trashModal, setTrashModal, handleOnConfirm } =
|
||||
useTrashModalHelper(docCollection);
|
||||
@@ -166,10 +179,6 @@ export const RootAppSidebar = ({
|
||||
});
|
||||
}, [docCollection.id, collection, navigateHelper, open]);
|
||||
|
||||
const allPageActive = currentPath === '/all';
|
||||
|
||||
const trashActive = currentPath === '/trash';
|
||||
|
||||
return (
|
||||
<AppSidebar
|
||||
clientBorder={appSettings.clientBorder}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '@affine/core/atoms';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { AccountIcon, SignOutIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -79,6 +80,12 @@ const AccountMenu = () => {
|
||||
const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom);
|
||||
|
||||
const onOpenAccountSetting = useCallback(() => {
|
||||
mixpanel.track('AccountSettingsViewed', {
|
||||
// page:
|
||||
segment: 'navigation panel',
|
||||
module: 'profile and badge',
|
||||
control: 'profile and email',
|
||||
});
|
||||
setSettingModalAtom(prev => ({
|
||||
...prev,
|
||||
open: true,
|
||||
|
||||
@@ -23,10 +23,12 @@ export const AppContainer = ({
|
||||
useNoisyBackground,
|
||||
useBlurBackground,
|
||||
children,
|
||||
...rest
|
||||
}: WorkspaceRootProps) => {
|
||||
const noisyBackground = useNoisyBackground && environment.isDesktop;
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={clsx(appStyle, {
|
||||
'noisy-background': noisyBackground,
|
||||
'blur-background': environment.isDesktop && useBlurBackground,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
pushGlobalLoadingEventAtom,
|
||||
resolveGlobalLoadingEventAtom,
|
||||
} from '@affine/component/global-loading';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { PageRootService, RootBlockModel } from '@blocksuite/blocks';
|
||||
@@ -25,6 +26,11 @@ async function exportHandler({ page, type }: ExportHandlerOptions) {
|
||||
if (editorRoot) {
|
||||
pageService = editorRoot.spec.getService<PageRootService>('affine:page');
|
||||
}
|
||||
mixpanel.track('ShareCreated', {
|
||||
type,
|
||||
segment: 'sharing panel',
|
||||
module: 'export share',
|
||||
});
|
||||
switch (type) {
|
||||
case 'html':
|
||||
await HtmlTransformer.exportDoc(page);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
@@ -141,6 +142,11 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
label: t['com.affine.header.option.duplicate'](),
|
||||
run() {
|
||||
duplicate(docId);
|
||||
mixpanel.track('DocCreated', {
|
||||
control: 'cmdk',
|
||||
type: 'doc duplicate',
|
||||
category: 'doc',
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
} from '../hooks/affine/use-global-dnd-helper';
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||
import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands';
|
||||
import { WorkbenchService } from '../modules/workbench';
|
||||
import {
|
||||
AllWorkspaceModals,
|
||||
CurrentWorkspaceModals,
|
||||
@@ -62,7 +64,6 @@ export const QuickSearch = () => {
|
||||
|
||||
const onToggleQuickSearch = useCallback(
|
||||
(open: boolean) => {
|
||||
mixpanel.track('QuickSearch', { open });
|
||||
setOpenQuickSearchModalAtom(open);
|
||||
},
|
||||
[setOpenQuickSearchModalAtom]
|
||||
@@ -113,7 +114,16 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
const upgrading = useLiveData(currentWorkspace.upgrade.upgrading$);
|
||||
const needUpgrade = useLiveData(currentWorkspace.upgrade.needUpgrade$);
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const basename = useLiveData(workbench.basename$);
|
||||
|
||||
const currentPath = useLiveData(
|
||||
workbench.location$.map(location => basename + location.pathname)
|
||||
);
|
||||
|
||||
useRegisterWorkspaceCommands();
|
||||
useRegisterNavigationCommands();
|
||||
|
||||
useEffect(() => {
|
||||
// hotfix for blockVersions
|
||||
@@ -143,6 +153,10 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
||||
const handleOpenQuickSearchModal = useCallback(() => {
|
||||
setOpenQuickSearchModalAtom(true);
|
||||
mixpanel.track('QuickSearchOpened', {
|
||||
segment: 'navigation panel',
|
||||
control: 'search button',
|
||||
});
|
||||
}, [setOpenQuickSearchModalAtom]);
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
@@ -152,6 +166,12 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
activeTab: 'appearance',
|
||||
open: true,
|
||||
});
|
||||
mixpanel.track('SettingsViewed', {
|
||||
// page:
|
||||
segment: 'navigation panel',
|
||||
module: 'general list',
|
||||
control: 'settings button',
|
||||
});
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
const resizing = useAtomValue(appSidebarResizingAtom);
|
||||
@@ -171,7 +191,7 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||
<>
|
||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
||||
<AppContainer resizing={resizing}>
|
||||
<AppContainer data-current-path={currentPath} resizing={resizing}>
|
||||
<Suspense fallback={<AppSidebarFallback />}>
|
||||
<RootAppSidebar
|
||||
isPublicWorkspace={false}
|
||||
|
||||
@@ -19,9 +19,14 @@ const EditorChatPanel = ({ editor }: SidebarTabProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
editor.host.spec.getService('affine:page').slots.docLinkClicked.on(() => {
|
||||
const pageService = editor.host.spec.getService('affine:page');
|
||||
|
||||
pageService.slots.docLinkClicked.on(() => {
|
||||
(chatPanelRef.current as ChatPanel).doc = editor.doc;
|
||||
});
|
||||
pageService.slots.editorModeSwitch.on(() => {
|
||||
(chatPanelRef.current as ChatPanel).host = editor.host;
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
if (!editor) {
|
||||
@@ -32,11 +37,9 @@ const EditorChatPanel = ({ editor }: SidebarTabProps) => {
|
||||
chatPanelRef.current = new ChatPanel();
|
||||
}
|
||||
|
||||
if (editor !== chatPanelRef.current?.editor) {
|
||||
(chatPanelRef.current as ChatPanel).editor = editor;
|
||||
(chatPanelRef.current as ChatPanel).doc = editor.doc;
|
||||
// (copilotPanelRef.current as CopilotPanel).fitPadding = [20, 20, 20, 20];
|
||||
}
|
||||
(chatPanelRef.current as ChatPanel).host = editor.host;
|
||||
(chatPanelRef.current as ChatPanel).doc = editor.doc;
|
||||
// (copilotPanelRef.current as CopilotPanel).fitPadding = [20, 20, 20, 20];
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
|
||||
@@ -7,15 +7,12 @@ import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useGeneralShortcuts } from '../../../hooks/affine/use-shortcuts';
|
||||
import { NavigatorService } from '../services/navigator';
|
||||
import * as styles from './navigation-buttons.css';
|
||||
import { useRegisterNavigationCommands } from './use-register-navigation-commands';
|
||||
|
||||
export const NavigationButtons = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const shortcuts = useGeneralShortcuts().shortcuts;
|
||||
|
||||
useRegisterNavigationCommands();
|
||||
|
||||
const shortcutsObject = useMemo(() => {
|
||||
const goBack = t['com.affine.keyboardShortcuts.goBack']();
|
||||
const goBackShortcut = shortcuts?.[goBack];
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
type AuthAccountInfo,
|
||||
type AuthService,
|
||||
} from '../../cloud';
|
||||
import { AccountLoggedOut } from '../../cloud/services/auth';
|
||||
import { UserQuotaChanged } from '../../cloud/services/user-quota';
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.onApplicationStart)
|
||||
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
||||
@OnEvent(AccountChanged, e => e.updateIdentity)
|
||||
@OnEvent(AccountLoggedOut, e => e.onAccountLoggedOut)
|
||||
@OnEvent(UserQuotaChanged, e => e.onUserQuotaChanged)
|
||||
export class TelemetryService extends Service {
|
||||
private prevQuota: NonNullable<QuotaQuery['currentUser']>['quota'] | null =
|
||||
@@ -26,23 +28,32 @@ export class TelemetryService extends Service {
|
||||
track_pageview: true,
|
||||
persistence: 'localStorage',
|
||||
});
|
||||
}
|
||||
const account = this.auth.session.account$.value;
|
||||
this.onAccountChanged(account);
|
||||
}
|
||||
|
||||
onAccountChanged(account: AuthAccountInfo | null) {
|
||||
if (account === null) {
|
||||
mixpanel.reset();
|
||||
} else {
|
||||
mixpanel.reset();
|
||||
mixpanel.identify(account.id);
|
||||
mixpanel.people.set({
|
||||
$email: account.email,
|
||||
$name: account.label,
|
||||
$avatar: account.avatar,
|
||||
mixpanel.register({
|
||||
appVersion: runtimeConfig.appVersion,
|
||||
environment: runtimeConfig.appBuildType,
|
||||
editorVersion: runtimeConfig.editorVersion,
|
||||
isSelfHosted: Boolean(runtimeConfig.isSelfHosted),
|
||||
isDesktop: environment.isDesktop,
|
||||
});
|
||||
}
|
||||
const account = this.auth.session.account$.value;
|
||||
this.updateIdentity(account);
|
||||
}
|
||||
|
||||
updateIdentity(account: AuthAccountInfo | null) {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
mixpanel.identify(account.id);
|
||||
mixpanel.people.set({
|
||||
$email: account.email,
|
||||
$name: account.label,
|
||||
$avatar: account.avatar,
|
||||
});
|
||||
}
|
||||
|
||||
onAccountLoggedOut() {
|
||||
mixpanel.reset();
|
||||
}
|
||||
|
||||
onUserQuotaChanged(quota: NonNullable<QuotaQuery['currentUser']>['quota']) {
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { createEvent, Service } from '@toeverything/infra';
|
||||
import { combineLatest, distinctUntilChanged, map, skip } from 'rxjs';
|
||||
|
||||
import { Workbench } from '../entities/workbench';
|
||||
|
||||
export const WorkbenchLocationChanged = createEvent<string>(
|
||||
'WorkbenchLocationChanged'
|
||||
);
|
||||
|
||||
export class WorkbenchService extends Service {
|
||||
constructor() {
|
||||
super();
|
||||
combineLatest([this.workbench.location$, this.workbench.basename$])
|
||||
.pipe(
|
||||
map(([location, basename]) => basename + location.pathname),
|
||||
distinctUntilChanged(),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe(newLocation => {
|
||||
this.eventBus.root.emit(WorkbenchLocationChanged, newLocation);
|
||||
mixpanel.track_pageview({
|
||||
location: newLocation,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
workbench = this.framework.createEntity(Workbench);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const trustedDomain = [
|
||||
'youtube.com',
|
||||
't.me',
|
||||
'reddit.com',
|
||||
'affine.pro',
|
||||
];
|
||||
|
||||
const logger = new DebugLogger('redirect_proxy');
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { AuthService, SubscriptionService } from '../modules/cloud';
|
||||
import { mixpanel } from '../utils';
|
||||
import { container } from './subscribe.css';
|
||||
|
||||
export const Component = () => {
|
||||
@@ -68,6 +69,16 @@ export const Component = () => {
|
||||
});
|
||||
setMessage('Redirecting...');
|
||||
location.href = checkout;
|
||||
mixpanel.track('PlanChangeSucceeded', {
|
||||
type: plan,
|
||||
category: recurring,
|
||||
});
|
||||
if (plan) {
|
||||
mixpanel.people.set({
|
||||
[SubscriptionPlan.AI === plan ? 'ai plan' : plan]: plan,
|
||||
recurring: recurring,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Something went wrong. Please try again.');
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
} from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
@@ -27,6 +29,28 @@ export const AllPageHeader = ({
|
||||
workspace.docCollection
|
||||
);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
if (options.isWorkspaceFile) {
|
||||
mixpanel.track('WorkspaceCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'all page',
|
||||
module: 'doc list header',
|
||||
control: 'import button',
|
||||
type: 'imported workspace',
|
||||
});
|
||||
} else {
|
||||
mixpanel.track('DocCreated', {
|
||||
page: 'doc library',
|
||||
segment: 'all page',
|
||||
module: 'doc list header',
|
||||
control: 'import button',
|
||||
type: 'imported doc',
|
||||
// category
|
||||
});
|
||||
}
|
||||
}, [importFile]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
left={
|
||||
@@ -46,7 +70,7 @@ export const AllPageHeader = ({
|
||||
)}
|
||||
onCreateEdgeless={createEdgeless}
|
||||
onCreatePage={createPage}
|
||||
onImportFile={importFile}
|
||||
onImportFile={onImportFile}
|
||||
>
|
||||
<PlusIcon />
|
||||
</PageListNewPageButton>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { PageDisplayMenu } from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
@@ -16,16 +17,19 @@ export const CollectionDetailHeader = ({
|
||||
return (
|
||||
<Header
|
||||
right={
|
||||
<IconButton
|
||||
type="default"
|
||||
icon={<PlusIcon fontSize={16} />}
|
||||
onClick={onCreate}
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
styles.headerCreateNewCollectionIconButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<IconButton
|
||||
type="default"
|
||||
icon={<PlusIcon fontSize={16} />}
|
||||
onClick={onCreate}
|
||||
className={clsx(
|
||||
styles.headerCreateNewButton,
|
||||
styles.headerCreateNewCollectionIconButton,
|
||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||
)}
|
||||
/>
|
||||
<PageDisplayMenu />
|
||||
</>
|
||||
}
|
||||
center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
|
||||
/>
|
||||
|
||||
@@ -32,3 +32,7 @@ export const affineDocViewport = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const scrollbar = style({
|
||||
marginRight: '4px',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||
import type { PageRootService } from '@blocksuite/blocks';
|
||||
import {
|
||||
BookmarkService,
|
||||
@@ -90,6 +91,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
||||
const docCollection = workspace.docCollection;
|
||||
const mode = useLiveData(doc.mode$);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
const isActiveView = useIsActiveView();
|
||||
// TODO: remove jotai here
|
||||
@@ -252,7 +254,11 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
docCollection={docCollection}
|
||||
/>
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
<Scrollable.Scrollbar
|
||||
className={clsx({
|
||||
[styles.scrollbar]: !appSettings.clientBorder,
|
||||
})}
|
||||
/>
|
||||
</Scrollable.Root>
|
||||
</AffineErrorBoundary>
|
||||
{isInTrash ? <TrashPageFooter /> : null}
|
||||
|
||||
@@ -66,12 +66,12 @@ export const Component = (): ReactElement => {
|
||||
},
|
||||
})
|
||||
);
|
||||
window.exportWorkspaceSnapshot = async () => {
|
||||
window.exportWorkspaceSnapshot = async (docs?: string[]) => {
|
||||
const zip = await ZipTransformer.exportDocs(
|
||||
workspace.docCollection,
|
||||
Array.from(workspace.docCollection.docs.values()).map(collection =>
|
||||
collection.getDoc()
|
||||
)
|
||||
Array.from(workspace.docCollection.docs.values())
|
||||
.filter(doc => (docs ? docs.includes(doc.id) : true))
|
||||
.map(doc => doc.getDoc())
|
||||
);
|
||||
const url = URL.createObjectURL(zip);
|
||||
// download url
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { PageDisplayMenu } from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
|
||||
export const TagDetailHeader = () => {
|
||||
return <Header center={<WorkspaceModeFilterTab activeFilter={'tags'} />} />;
|
||||
return (
|
||||
<Header
|
||||
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
|
||||
right={<PageDisplayMenu />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,17 +5,13 @@ import {
|
||||
createBrowserRouter as reactRouterCreateBrowserRouter,
|
||||
Outlet,
|
||||
redirect,
|
||||
useLocation,
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { mixpanel } from './utils';
|
||||
|
||||
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
||||
|
||||
function RootRouter() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [ready, setReady] = useState(false);
|
||||
useEffect(() => {
|
||||
@@ -23,16 +19,6 @@ function RootRouter() {
|
||||
setReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mixpanel.track_pageview({
|
||||
page: location.pathname,
|
||||
appVersion: runtimeConfig.appVersion,
|
||||
environment: runtimeConfig.appBuildType,
|
||||
editorVersion: runtimeConfig.editorVersion,
|
||||
isSelfHosted: Boolean(runtimeConfig.isSelfHosted),
|
||||
isDesktop: environment.isDesktop,
|
||||
});
|
||||
}, [location]);
|
||||
return (
|
||||
ready && (
|
||||
<NavigateContext.Provider value={navigate}>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { appSettingAtom } from '@toeverything/infra';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import mixpanel from 'mixpanel-browser';
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { mixpanel } from './utils';
|
||||
|
||||
export function Telemetry() {
|
||||
const settings = useAtomValue(appSettingAtom);
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { getI18n } from '@affine/i18n';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { CalendarTranslation } from '../intl-formatter';
|
||||
import { timestampToCalendarDate } from '../intl-formatter';
|
||||
|
||||
const translation: CalendarTranslation = {
|
||||
yesterday: () => 'Yesterday',
|
||||
today: () => 'Today',
|
||||
tomorrow: () => 'Tomorrow',
|
||||
nextWeek: () => 'Next Week',
|
||||
};
|
||||
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
describe('intl calendar date formatter', () => {
|
||||
const week = new Intl.DateTimeFormat(getI18n()?.language, {
|
||||
weekday: 'long',
|
||||
});
|
||||
|
||||
test('someday before last week', async () => {
|
||||
const timestamp = '2000-01-01 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation)).toBe('Jan 1, 2000');
|
||||
});
|
||||
|
||||
test('someday in last week', async () => {
|
||||
const timestamp = Date.now() - 6 * ONE_DAY;
|
||||
expect(timestampToCalendarDate(timestamp, translation)).toBe(
|
||||
week.format(timestamp)
|
||||
);
|
||||
});
|
||||
|
||||
test('someday is yesterday', async () => {
|
||||
const timestamp = Date.now() - ONE_DAY;
|
||||
expect(timestampToCalendarDate(timestamp, translation)).toBe('Yesterday');
|
||||
});
|
||||
|
||||
test('someday is today', async () => {
|
||||
const timestamp = Date.now();
|
||||
expect(timestampToCalendarDate(timestamp, translation)).toBe('Today');
|
||||
});
|
||||
|
||||
test('someday is tomorrow', async () => {
|
||||
const timestamp = Date.now() + ONE_DAY;
|
||||
expect(timestampToCalendarDate(timestamp, translation)).toBe('Tomorrow');
|
||||
});
|
||||
|
||||
test('someday in next week', async () => {
|
||||
const timestamp = Date.now() + 6 * ONE_DAY;
|
||||
expect(timestampToCalendarDate(timestamp, translation)).toBe(
|
||||
`Next Week ${week.format(timestamp)}`
|
||||
);
|
||||
});
|
||||
|
||||
test('someday after next week', async () => {
|
||||
const timestamp = '3000-01-01 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation)).toBe('Jan 1, 3000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('intl calendar date formatter with specific reference time', () => {
|
||||
const referenceTime = '2024-05-10 14:00';
|
||||
|
||||
test('someday before last week', async () => {
|
||||
const timestamp = '2024-04-27 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
|
||||
'Apr 27, 2024'
|
||||
);
|
||||
});
|
||||
|
||||
test('someday in last week', async () => {
|
||||
const timestamp = '2024-05-6 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
|
||||
'Monday'
|
||||
);
|
||||
});
|
||||
|
||||
test('someday is yesterday', async () => {
|
||||
const timestamp = '2024-05-9 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
|
||||
'Yesterday'
|
||||
);
|
||||
});
|
||||
|
||||
test('someday is today', async () => {
|
||||
const timestamp = '2024-05-10 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
|
||||
'Today'
|
||||
);
|
||||
});
|
||||
|
||||
test('someday is tomorrow', async () => {
|
||||
const timestamp = '2024-05-11 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
|
||||
'Tomorrow'
|
||||
);
|
||||
});
|
||||
|
||||
test('someday in next week', async () => {
|
||||
const timestamp = '2024-05-15 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
|
||||
'Next Week Wednesday'
|
||||
);
|
||||
});
|
||||
|
||||
test('someday after next week', async () => {
|
||||
const timestamp = '2024-05-30 10:00';
|
||||
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
|
||||
'May 30, 2024'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,66 @@
|
||||
import { getI18n } from '@affine/i18n';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const timeFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
timeStyle: 'short',
|
||||
});
|
||||
function createTimeFormatter() {
|
||||
return new Intl.DateTimeFormat(getI18n()?.language, {
|
||||
timeStyle: 'short',
|
||||
});
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
function createDateFormatter() {
|
||||
return new Intl.DateTimeFormat(getI18n()?.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function createWeekFormatter() {
|
||||
return new Intl.DateTimeFormat(getI18n()?.language, {
|
||||
weekday: 'long',
|
||||
});
|
||||
}
|
||||
|
||||
export const timestampToLocalTime = (ts: string | number) => {
|
||||
return timeFormatter.format(dayjs(ts).toDate());
|
||||
const formatter = createTimeFormatter();
|
||||
return formatter.format(dayjs(ts).toDate());
|
||||
};
|
||||
|
||||
export const timestampToLocalDate = (ts: string | number) => {
|
||||
return dateFormatter.format(dayjs(ts).toDate());
|
||||
const formatter = createDateFormatter();
|
||||
return formatter.format(dayjs(ts).toDate());
|
||||
};
|
||||
|
||||
export interface CalendarTranslation {
|
||||
yesterday: () => string;
|
||||
today: () => string;
|
||||
tomorrow: () => string;
|
||||
nextWeek: () => string;
|
||||
}
|
||||
|
||||
export const timestampToCalendarDate = (
|
||||
ts: string | number,
|
||||
translation: CalendarTranslation,
|
||||
referenceTime?: string | number
|
||||
) => {
|
||||
const startOfDay = dayjs(referenceTime).startOf('d');
|
||||
const diff = dayjs(ts).diff(startOfDay, 'd', true);
|
||||
const sameElse = timestampToLocalDate(ts);
|
||||
|
||||
const formatter = createWeekFormatter();
|
||||
const week = formatter.format(dayjs(ts).toDate());
|
||||
|
||||
return diff < -6
|
||||
? sameElse
|
||||
: diff < -1
|
||||
? week
|
||||
: diff < 0
|
||||
? translation.yesterday()
|
||||
: diff < 1
|
||||
? translation.today()
|
||||
: diff < 2
|
||||
? translation.tomorrow()
|
||||
: diff < 7
|
||||
? `${translation.nextWeek()} ${week}`
|
||||
: sameElse;
|
||||
};
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@blocksuite/block-std": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/blocks": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/presets": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/store": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@blocksuite/block-std": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/blocks": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/presets": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@blocksuite/store": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@electron-forge/cli": "^7.3.0",
|
||||
"@electron-forge/core": "^7.3.0",
|
||||
"@electron-forge/core-utils": "^7.3.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.3.0",
|
||||
"@electron-forge/shared-types": "^7.3.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@pengx17/electron-forge-maker-appimage": "^1.2.1",
|
||||
"@pengx17/electron-forge-maker-appimage": "^1.2.0",
|
||||
"@sentry/electron": "^4.22.0",
|
||||
"@sentry/esbuild-plugin": "^2.16.1",
|
||||
"@sentry/react": "^7.109.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^30.0.0",
|
||||
"electron-log": "^5.1.2",
|
||||
"electron-squirrel-startup": "1.0.0",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esbuild": "^0.21.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { parse } from 'node:url';
|
||||
|
||||
import { app, BrowserWindow, shell } from 'electron';
|
||||
|
||||
import { logger } from '../logger';
|
||||
|
||||
const redirectUri = 'https://affine.pro/client/auth-callback';
|
||||
|
||||
export const oauthEndpoint = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.AFFINE_GOOGLE_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=code&scope=openid https://www.googleapis.com/auth/userinfo.email profile&access_type=offline&customParameters={"prompt":"select_account"}`;
|
||||
|
||||
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
export const getExchangeTokenParams = (code: string) => {
|
||||
const postData = {
|
||||
code,
|
||||
client_id: process.env.AFFINE_GOOGLE_CLIENT_ID || '',
|
||||
client_secret: process.env.AFFINE_GOOGLE_CLIENT_SECRET || '',
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
};
|
||||
const requestInit: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams(postData).toString(),
|
||||
};
|
||||
return { requestInit, url: tokenEndpoint };
|
||||
};
|
||||
|
||||
export function getGoogleOauthCode() {
|
||||
return new Promise<ReturnType<typeof getExchangeTokenParams>>(
|
||||
(resolve, reject) => {
|
||||
shell.openExternal(oauthEndpoint).catch(e => {
|
||||
logger.error('Failed to open external url', e);
|
||||
reject(e);
|
||||
});
|
||||
const handleOpenUrl = (_: any, url: string) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows().find(
|
||||
w => !w.isDestroyed()
|
||||
);
|
||||
const urlObj = parse(url.replace('??', '?'), true);
|
||||
if (!mainWindow || !url.startsWith('affine://auth-callback')) return;
|
||||
const code = urlObj.query['code'] as string;
|
||||
if (!code) return;
|
||||
|
||||
logger.info('google sign in code received from callback', code);
|
||||
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
resolve(getExchangeTokenParams(code));
|
||||
};
|
||||
|
||||
app.on('open-url', handleOpenUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out'));
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
}, 30000);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import { getOnboardingWindow } from '../onboarding';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { launchStage } from '../windows-manager/stage';
|
||||
import { getChallengeResponse } from './challenge';
|
||||
import { getGoogleOauthCode } from './google-auth';
|
||||
|
||||
export let isOnline = true;
|
||||
|
||||
@@ -63,9 +62,6 @@ export const uiHandlers = {
|
||||
handleNetworkChange: async (_, _isOnline: boolean) => {
|
||||
isOnline = _isOnline;
|
||||
},
|
||||
getGoogleOauthCode: async () => {
|
||||
return getGoogleOauthCode();
|
||||
},
|
||||
getChallengeResponse: async (_, challenge: string) => {
|
||||
return getChallengeResponse(challenge);
|
||||
},
|
||||
|
||||
@@ -45,6 +45,8 @@ export function useI18N() {
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export { getI18n } from 'react-i18next';
|
||||
|
||||
const resources = LOCALES.reduce<Resource>((acc, { tag, res }) => {
|
||||
return Object.assign(acc, { [tag]: { translation: res } });
|
||||
}, {});
|
||||
|
||||
@@ -554,7 +554,7 @@
|
||||
"com.affine.cmdk.affine.color-scheme.to": "Change Colour Scheme to",
|
||||
"com.affine.cmdk.affine.contact-us": "Contact Us",
|
||||
"com.affine.cmdk.affine.create-new-edgeless-as": "New \"{{keyWord}}\" Edgeless",
|
||||
"com.affine.cmdk.affine.create-new-page-as": "New \"{{keyWord}}\" Doc",
|
||||
"com.affine.cmdk.affine.create-new-page-as": "New \"{{keyWord}}\" Page",
|
||||
"com.affine.cmdk.affine.display-language.to": "Change Display Language to",
|
||||
"com.affine.cmdk.affine.editor.add-to-favourites": "Add to Favourites",
|
||||
"com.affine.cmdk.affine.editor.edgeless.presentation-start": "Start Presentation",
|
||||
@@ -574,8 +574,9 @@
|
||||
"com.affine.cmdk.affine.navigation.goto-trash": "Go to Trash",
|
||||
"com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace",
|
||||
"com.affine.cmdk.affine.navigation.open-settings": "Go to Settings",
|
||||
"com.affine.cmdk.affine.navigation.open-account-settings": "Go to Account Settings",
|
||||
"com.affine.cmdk.affine.new-edgeless-page": "New Edgeless",
|
||||
"com.affine.cmdk.affine.new-page": "New Doc",
|
||||
"com.affine.cmdk.affine.new-page": "New Page",
|
||||
"com.affine.cmdk.affine.new-workspace": "New Workspace",
|
||||
"com.affine.cmdk.affine.noise-background-on-the-sidebar.to": "Change Noise Background On The Sidebar to",
|
||||
"com.affine.cmdk.affine.restart-to-upgrade": "Restart to Upgrade",
|
||||
@@ -783,6 +784,7 @@
|
||||
"com.affine.last7Days": "Last 7 Days",
|
||||
"com.affine.lastMonth": "Last month",
|
||||
"com.affine.lastWeek": "Last week",
|
||||
"com.affine.nextWeek": "Next week",
|
||||
"com.affine.lastYear": "Last year",
|
||||
"com.affine.loading": "Loading...",
|
||||
"com.affine.moreThan30Days": "Older than a month",
|
||||
@@ -1238,6 +1240,7 @@
|
||||
"com.affine.toastMessage.restored": "{{title}} restored",
|
||||
"com.affine.toastMessage.successfullyDeleted": "Successfully deleted",
|
||||
"com.affine.today": "Today",
|
||||
"com.affine.tomorrow": "Tomorrow",
|
||||
"com.affine.trashOperation.delete": "Delete",
|
||||
"com.affine.trashOperation.delete.description": "Once deleted, you can't undo this action. Do you confirm?",
|
||||
"com.affine.trashOperation.delete.title": "Permanently delete",
|
||||
|
||||
@@ -917,7 +917,7 @@
|
||||
"com.affine.payment.benefit-6": "每个工作区的成员数量 ≤ {{capacity}}",
|
||||
"com.affine.payment.benefit-7": "{{capacity}} 日的历史版本记录",
|
||||
"com.affine.payment.billing-setting.ai-plan": "AFFiNE AI",
|
||||
"com.affine.payment.billing-setting.ai.free-desc": "您当前处于<a>免费计划</a>.",
|
||||
"com.affine.payment.billing-setting.ai.free-desc": "您当前处于<a> Free 计划</a>.",
|
||||
"com.affine.payment.billing-setting.ai.purchase": "购买",
|
||||
"com.affine.payment.billing-setting.cancel-subscription": "取消订阅",
|
||||
"com.affine.payment.billing-setting.cancel-subscription.description": "一旦您取消订阅,您将不再享受该计划的福利。",
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.54",
|
||||
"@napi-rs/cli": "3.0.0-alpha.55",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"ava": "^6.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"nx": "^19.0.0",
|
||||
"nx-cloud": "^18.0.0",
|
||||
"nx-cloud": "^19.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
|
||||
@@ -136,7 +136,7 @@ test('Create a new page without keyword', async ({ page }) => {
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Doc');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
await page.waitForTimeout(300);
|
||||
await assertTitle(page, '');
|
||||
@@ -148,7 +148,9 @@ test('Create a new page with keyword', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
await insertInputText(page, '"test123456"');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New ""test123456"" Doc');
|
||||
const addNewPage = page.locator(
|
||||
'[cmdk-item] >> text=New ""test123456"" Page'
|
||||
);
|
||||
await addNewPage.click();
|
||||
await page.waitForTimeout(300);
|
||||
await assertTitle(page, '"test123456"');
|
||||
@@ -170,7 +172,7 @@ test('Create a new page and search this page', async ({ page }) => {
|
||||
// input title and create new page
|
||||
await insertInputText(page, 'test123456');
|
||||
await page.waitForTimeout(300);
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New "test123456" Doc');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New "test123456" Page');
|
||||
await addNewPage.click();
|
||||
|
||||
await page.waitForTimeout(300);
|
||||
@@ -239,7 +241,7 @@ test('Focus title after creating a new page', async ({ page }) => {
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Doc');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
await titleIsFocused(page);
|
||||
});
|
||||
@@ -272,7 +274,7 @@ test('assert the recent browse pages are on the recent list', async ({
|
||||
|
||||
// create second page
|
||||
await openQuickSearchByShortcut(page);
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Doc');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
await waitForEditorLoad(page);
|
||||
{
|
||||
@@ -312,7 +314,7 @@ test('assert the recent browse pages are on the recent list', async ({
|
||||
await waitForEditorLoad(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
{
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Doc');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
}
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"devDependencies": {
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@aws-sdk/client-s3": "3.572.0",
|
||||
"@blocksuite/presets": "0.14.0-canary-202405082235-4e0896c",
|
||||
"@aws-sdk/client-s3": "3.576.0",
|
||||
"@blocksuite/presets": "0.15.0-canary-202405131108-aa6f0b7",
|
||||
"@clack/core": "^0.3.4",
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@magic-works/i18n-codegen": "^0.6.0",
|
||||
|
||||
Reference in New Issue
Block a user