mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
Merge remote-tracking branch 'origin/canary' into stable
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "optimized_blobs" ADD COLUMN "deleted_at" TIMESTAMPTZ(6);
|
||||
@@ -178,13 +178,15 @@ model Blob {
|
||||
}
|
||||
|
||||
model OptimizedBlob {
|
||||
id Int @id @default(autoincrement()) @db.Integer
|
||||
hash String @db.VarChar
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
params String @db.VarChar
|
||||
blob Bytes @db.ByteA
|
||||
id Int @id @default(autoincrement()) @db.Integer
|
||||
hash String @db.VarChar
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
params String @db.VarChar
|
||||
blob Bytes @db.ByteA
|
||||
length BigInt
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
// not for keeping, but for snapshot history
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
@@unique([workspaceId, hash, params])
|
||||
@@map("optimized_blobs")
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Field, ObjectType, Query } from '@nestjs/graphql';
|
||||
|
||||
import { SERVER_FLAVOR } from '../modules';
|
||||
|
||||
@ObjectType()
|
||||
export class ServerConfigType {
|
||||
@Field({ description: 'server version' })
|
||||
version!: string;
|
||||
|
||||
@Field({ description: 'server flavor' })
|
||||
flavor!: string;
|
||||
}
|
||||
|
||||
export class ServerConfigResolver {
|
||||
@Query(() => ServerConfigType, {
|
||||
description: 'server config',
|
||||
})
|
||||
serverConfig(): ServerConfigType {
|
||||
return {
|
||||
version: AFFiNE.version,
|
||||
flavor: SERVER_FLAVOR || 'allinone',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [ServerConfigResolver],
|
||||
})
|
||||
export class ServerConfigModule {}
|
||||
@@ -90,7 +90,7 @@ export class DocHistoryManager {
|
||||
workspaceId,
|
||||
id,
|
||||
timestamp: {
|
||||
lte: before,
|
||||
lt: before,
|
||||
},
|
||||
// only include the ones has not expired
|
||||
expiredAt: {
|
||||
|
||||
@@ -3,9 +3,10 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
import { GqlModule } from '../graphql.module';
|
||||
import { AuthModule } from './auth';
|
||||
import { ServerConfigModule } from './config';
|
||||
import { DocModule } from './doc';
|
||||
import { PaymentModule } from './payment';
|
||||
import { SelfHostedModule } from './self-hosted';
|
||||
import { SyncModule } from './sync';
|
||||
import { UsersModule } from './users';
|
||||
import { WorkspaceModule } from './workspaces';
|
||||
@@ -22,13 +23,25 @@ switch (SERVER_FLAVOR) {
|
||||
case 'sync':
|
||||
BusinessModules.push(SyncModule, DocModule.forSync());
|
||||
break;
|
||||
case 'graphql':
|
||||
case 'selfhosted':
|
||||
BusinessModules.push(
|
||||
ServerConfigModule,
|
||||
SelfHostedModule,
|
||||
ScheduleModule.forRoot(),
|
||||
GqlModule,
|
||||
WorkspaceModule,
|
||||
UsersModule,
|
||||
SyncModule,
|
||||
DocModule.forRoot()
|
||||
);
|
||||
break;
|
||||
case 'graphql':
|
||||
BusinessModules.push(
|
||||
ServerConfigModule,
|
||||
ScheduleModule.forRoot(),
|
||||
GqlModule,
|
||||
WorkspaceModule,
|
||||
UsersModule,
|
||||
AuthModule,
|
||||
DocModule.forRoot(),
|
||||
PaymentModule
|
||||
);
|
||||
@@ -36,11 +49,11 @@ switch (SERVER_FLAVOR) {
|
||||
case 'allinone':
|
||||
default:
|
||||
BusinessModules.push(
|
||||
ServerConfigModule,
|
||||
ScheduleModule.forRoot(),
|
||||
GqlModule,
|
||||
WorkspaceModule,
|
||||
UsersModule,
|
||||
AuthModule,
|
||||
SyncModule,
|
||||
DocModule.forRoot(),
|
||||
PaymentModule
|
||||
@@ -48,4 +61,4 @@ switch (SERVER_FLAVOR) {
|
||||
break;
|
||||
}
|
||||
|
||||
export { BusinessModules };
|
||||
export { BusinessModules, SERVER_FLAVOR };
|
||||
|
||||
@@ -52,7 +52,7 @@ class SubscriptionPrice {
|
||||
}
|
||||
|
||||
@ObjectType('UserSubscription')
|
||||
class UserSubscriptionType implements Partial<UserSubscription> {
|
||||
export class UserSubscriptionType implements Partial<UserSubscription> {
|
||||
@Field({ name: 'id' })
|
||||
stripeSubscriptionId!: string;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export enum SubscriptionPlan {
|
||||
Pro = 'pro',
|
||||
Team = 'team',
|
||||
Enterprise = 'enterprise',
|
||||
SelfHosted = 'selfhosted',
|
||||
}
|
||||
|
||||
export function encodeLookupKey(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { UserSubscriptionType } from './payment/resolver';
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionStatus,
|
||||
} from './payment/service';
|
||||
import { UserType } from './users';
|
||||
|
||||
const YEAR = 1000 * 60 * 60 * 24 * 30 * 12;
|
||||
|
||||
@Resolver(() => UserType)
|
||||
export class SelfHostedDummyResolver {
|
||||
private readonly start = new Date();
|
||||
private readonly end = new Date(Number(this.start) + YEAR);
|
||||
constructor() {}
|
||||
|
||||
@ResolveField(() => UserSubscriptionType)
|
||||
async subscription() {
|
||||
return {
|
||||
stripeSubscriptionId: 'dummy',
|
||||
plan: SubscriptionPlan.SelfHosted,
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
status: SubscriptionStatus.Active,
|
||||
start: this.start,
|
||||
end: this.end,
|
||||
createdAt: this.start,
|
||||
updatedAt: this.start,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [SelfHostedDummyResolver],
|
||||
})
|
||||
export class SelfHostedModule {}
|
||||
@@ -2,6 +2,14 @@
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
type ServerConfigType {
|
||||
"""server version"""
|
||||
version: String!
|
||||
|
||||
"""server flavor"""
|
||||
flavor: String!
|
||||
}
|
||||
|
||||
type UserType {
|
||||
id: ID!
|
||||
|
||||
@@ -73,6 +81,7 @@ enum SubscriptionPlan {
|
||||
Pro
|
||||
Team
|
||||
Enterprise
|
||||
SelfHosted
|
||||
}
|
||||
|
||||
type UserSubscription {
|
||||
@@ -240,6 +249,9 @@ type DocHistoryType {
|
||||
}
|
||||
|
||||
type Query {
|
||||
"""server config"""
|
||||
serverConfig: ServerConfigType!
|
||||
|
||||
"""Get is owner of workspace"""
|
||||
isOwner(workspaceId: String!): Boolean!
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
jwst-codec = { git = "https://github.com/toeverything/OctoBase.git", rev = "aad9e5b" }
|
||||
jwst-core = { git = "https://github.com/toeverything/OctoBase.git", rev = "aad9e5b" }
|
||||
jwst-storage = { git = "https://github.com/toeverything/OctoBase.git", rev = "aad9e5b" }
|
||||
jwst-codec = { git = "https://github.com/toeverything/OctoBase.git", rev = "49a6b7a" }
|
||||
jwst-core = { git = "https://github.com/toeverything/OctoBase.git", rev = "49a6b7a" }
|
||||
jwst-storage = { git = "https://github.com/toeverything/OctoBase.git", rev = "49a6b7a" }
|
||||
napi = { version = "2", default-features = false, features = [
|
||||
"napi5",
|
||||
"async",
|
||||
|
||||
Vendored
+2
-2
@@ -3,8 +3,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"vitest": "0.34.6",
|
||||
|
||||
Vendored
+1
@@ -32,6 +32,7 @@ export const runtimeFlagsSchema = z.object({
|
||||
enableCaptcha: z.boolean(),
|
||||
enableEnhanceShareMode: z.boolean(),
|
||||
enablePayment: z.boolean(),
|
||||
enablePageHistory: z.boolean(),
|
||||
// this is for the electron app
|
||||
serverUrlPrefix: z.string(),
|
||||
enableMoveDatabase: z.boolean(),
|
||||
|
||||
@@ -55,9 +55,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/sdk": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"jotai": "^2.5.1",
|
||||
"jotai-effect": "^0.2.3",
|
||||
"tinykeys": "^2.1.0",
|
||||
@@ -66,8 +66,8 @@
|
||||
"devDependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"electron": "link:../../frontend/electron/node_modules/electron",
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"jotai": "^2.5.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
"y-provider": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "3.6.0",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-dts": "3.6.0",
|
||||
"vitest": "0.34.6",
|
||||
|
||||
@@ -64,12 +64,12 @@
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/icons": "2.1.36",
|
||||
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@storybook/jest": "^0.2.3",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
export const hoverMaxWidth = createVar();
|
||||
|
||||
export const root = style({
|
||||
position: 'relative',
|
||||
@@ -15,7 +17,8 @@ export const tagsContainer = style({
|
||||
export const tagsScrollContainer = style([
|
||||
tagsContainer,
|
||||
{
|
||||
overflow: 'auto',
|
||||
overflowX: 'hidden',
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
gap: '8px',
|
||||
},
|
||||
@@ -41,7 +44,7 @@ export const innerContainer = style({
|
||||
transition: 'all 0.2s 0.3s ease-in-out',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
maxWidth: 'var(--hover-max-width)',
|
||||
maxWidth: hoverMaxWidth,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -66,6 +69,16 @@ export const innerBackdrop = style({
|
||||
|
||||
export const tag = style({
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
minWidth: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
':last-child': {
|
||||
minWidth: 'max-content',
|
||||
},
|
||||
});
|
||||
|
||||
export const tagInnerWrapper = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
@@ -74,7 +87,7 @@ export const tag = style({
|
||||
});
|
||||
|
||||
export const tagSticky = style([
|
||||
tag,
|
||||
tagInnerWrapper,
|
||||
{
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
borderRadius: '10px',
|
||||
@@ -82,10 +95,8 @@ export const tagSticky = style([
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
maxWidth: '128px',
|
||||
position: 'sticky',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
left: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Tag } from '@affine/env/filter';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './page-tags.css';
|
||||
import { stopPropagation } from './utils';
|
||||
@@ -42,18 +43,22 @@ const TagItem = ({ tag, idx, mode, style }: TagItemProps) => {
|
||||
return (
|
||||
<div
|
||||
data-testid="page-tag"
|
||||
className={mode === 'sticky' ? styles.tagSticky : styles.tagListItem}
|
||||
className={styles.tag}
|
||||
data-idx={idx}
|
||||
title={tag.value}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={styles.tagIndicator}
|
||||
style={{
|
||||
backgroundColor: tagColorMap(tag.color),
|
||||
}}
|
||||
/>
|
||||
<div className={styles.tagLabel}>{tag.value}</div>
|
||||
className={mode === 'sticky' ? styles.tagSticky : styles.tagListItem}
|
||||
>
|
||||
<div
|
||||
className={styles.tagIndicator}
|
||||
style={{
|
||||
backgroundColor: tagColorMap(tag.color),
|
||||
}}
|
||||
/>
|
||||
<div className={styles.tagLabel}>{tag.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -69,26 +74,6 @@ export const PageTags = ({
|
||||
? widthOnHover
|
||||
: `${widthOnHover}px`
|
||||
: 'auto';
|
||||
const tagsContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (tagsContainerRef.current) {
|
||||
const tagsContainer = tagsContainerRef.current;
|
||||
const listener = () => {
|
||||
// on mouseleave, reset scroll position to the hoverExpandDirection
|
||||
tagsContainer.scrollTo({
|
||||
left: hoverExpandDirection === 'left' ? Number.MAX_SAFE_INTEGER : 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
listener();
|
||||
tagsContainerRef.current.addEventListener('mouseleave', listener);
|
||||
return () => {
|
||||
tagsContainer.removeEventListener('mouseleave', listener);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [hoverExpandDirection]);
|
||||
|
||||
const tagsInPopover = useMemo(() => {
|
||||
const lastTags = tags.slice(maxItems);
|
||||
@@ -107,36 +92,17 @@ export const PageTags = ({
|
||||
// sort tags by length
|
||||
nTags.sort((a, b) => a.value.length - b.value.length);
|
||||
|
||||
const tagRightCharLength = nTags.reduceRight<number[]>(
|
||||
(acc, tag) => {
|
||||
const curr = acc[0] + Math.min(tag.value.length, 10);
|
||||
return [curr, ...acc];
|
||||
},
|
||||
[0]
|
||||
);
|
||||
|
||||
tagRightCharLength.shift();
|
||||
|
||||
return nTags.map((tag, idx) => (
|
||||
<TagItem
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
idx={idx}
|
||||
mode="sticky"
|
||||
style={{
|
||||
right: `calc(${tagRightCharLength[idx]}em)`,
|
||||
}}
|
||||
/>
|
||||
<TagItem key={tag.id} tag={tag} idx={idx} mode="sticky" />
|
||||
));
|
||||
}, [maxItems, tags]);
|
||||
return (
|
||||
<div
|
||||
data-testid="page-tags"
|
||||
className={styles.root}
|
||||
style={{
|
||||
// @ts-expect-error it's fine
|
||||
'--hover-max-width': sanitizedWidthOnHover,
|
||||
}}
|
||||
style={assignInlineVars({
|
||||
[styles.hoverMaxWidth]: sanitizedWidthOnHover,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -146,9 +112,7 @@ export const PageTags = ({
|
||||
className={clsx(styles.innerContainer)}
|
||||
>
|
||||
<div className={styles.innerBackdrop} />
|
||||
<div className={styles.tagsScrollContainer} ref={tagsContainerRef}>
|
||||
{tagsNormal}
|
||||
</div>
|
||||
<div className={styles.tagsScrollContainer}>{tagsNormal}</div>
|
||||
{maxItems && tags.length > maxItems ? (
|
||||
<Menu
|
||||
items={tagsInPopover}
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as styles from './share.css';
|
||||
export interface StorageProgressProgress {
|
||||
max: number;
|
||||
value: number;
|
||||
upgradable?: boolean;
|
||||
onUpgrade: () => void;
|
||||
plan: SubscriptionPlan;
|
||||
}
|
||||
@@ -23,6 +24,7 @@ enum ButtonType {
|
||||
export const StorageProgress = ({
|
||||
max: upperLimit,
|
||||
value,
|
||||
upgradable = true,
|
||||
onUpgrade,
|
||||
plan,
|
||||
}: StorageProgressProgress) => {
|
||||
@@ -63,22 +65,24 @@ export const StorageProgress = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
options={{ hidden: percent < 100 }}
|
||||
content={t['com.affine.storage.maximum-tips']()}
|
||||
>
|
||||
<span tabIndex={0}>
|
||||
<Button
|
||||
type={buttonType}
|
||||
onClick={onUpgrade}
|
||||
className={styles.storageButton}
|
||||
>
|
||||
{plan === 'Free'
|
||||
? t['com.affine.storage.upgrade']()
|
||||
: t['com.affine.storage.change-plan']()}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{upgradable ? (
|
||||
<Tooltip
|
||||
options={{ hidden: percent < 100 }}
|
||||
content={t['com.affine.storage.maximum-tips']()}
|
||||
>
|
||||
<span tabIndex={0}>
|
||||
<Button
|
||||
type={buttonType}
|
||||
onClick={onUpgrade}
|
||||
className={styles.storageButton}
|
||||
>
|
||||
{plan === 'Free'
|
||||
? t['com.affine.storage.upgrade']()
|
||||
: t['com.affine.storage.change-plan']()}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableCaptcha: true,
|
||||
enableEnhanceShareMode: false,
|
||||
enablePayment: true,
|
||||
enablePageHistory: false,
|
||||
serverUrlPrefix: 'https://insider.affine.pro', // Let insider be stable environment temporarily.
|
||||
editorFlags,
|
||||
appVersion: packageJson.version,
|
||||
@@ -41,6 +42,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
get beta() {
|
||||
return {
|
||||
...this.stable,
|
||||
enablePageHistory: false,
|
||||
serverUrlPrefix: 'https://insider.affine.pro',
|
||||
};
|
||||
},
|
||||
@@ -75,6 +77,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableCaptcha: true,
|
||||
enableEnhanceShareMode: false,
|
||||
enablePayment: true,
|
||||
enablePageHistory: true,
|
||||
serverUrlPrefix: 'https://affine.fail',
|
||||
editorFlags,
|
||||
appVersion: packageJson.version,
|
||||
@@ -142,6 +145,11 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
: buildFlags.mode === 'development'
|
||||
? true
|
||||
: currentBuildPreset.enablePayment,
|
||||
enablePageHistory: process.env.ENABLE_PAGE_HISTORY
|
||||
? process.env.ENABLE_PAGE_HISTORY === 'true'
|
||||
: buildFlags.mode === 'development'
|
||||
? true
|
||||
: currentBuildPreset.enablePageHistory,
|
||||
};
|
||||
|
||||
if (buildFlags.mode === 'development') {
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/icons": "2.1.36",
|
||||
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/virgo": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/virgo": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
// make page history controllable by atom to make it easier to use in CMDK
|
||||
export const pageHistoryModalAtom = atom({
|
||||
open: false,
|
||||
pageId: '',
|
||||
});
|
||||
@@ -28,6 +28,11 @@ const UserPlanButtonWithData = () => {
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
if (plan === SubscriptionPlan.SelfHosted) {
|
||||
// Self hosted version doesn't have a payment apis.
|
||||
return <div className={styles.userPlanButton}>{plan}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={t['com.affine.payment.tag-tooltips']()} side="top">
|
||||
<div className={styles.userPlanButton} onClick={handleClick}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useSelfHosted } from '../../../hooks/affine/use-server-flavor';
|
||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
import { ExportPanel } from './export';
|
||||
@@ -20,6 +21,7 @@ import type { WorkspaceSettingDetailProps } from './types';
|
||||
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
const { workspaceId } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const isSelfHosted = useSelfHosted();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
|
||||
@@ -56,7 +58,11 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
||||
<PublishPanel workspace={workspace} {...props} />
|
||||
<MembersPanel workspace={workspace} {...props} />
|
||||
<MembersPanel
|
||||
workspace={workspace}
|
||||
upgradable={!isSelfHosted}
|
||||
{...props}
|
||||
/>
|
||||
</SettingWrapper>
|
||||
{storageAndExportSetting}
|
||||
<SettingWrapper>
|
||||
|
||||
+14
-8
@@ -51,6 +51,7 @@ enum MemberLimitCount {
|
||||
|
||||
const COUNT_PER_PAGE = 8;
|
||||
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
||||
upgradable: boolean;
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
type OnRevoke = (memberId: string) => void;
|
||||
@@ -70,6 +71,7 @@ const MembersPanelLocal = () => {
|
||||
export const CloudWorkspaceMembersPanel = ({
|
||||
workspace,
|
||||
isOwner,
|
||||
upgradable,
|
||||
}: MembersPanelProps) => {
|
||||
const workspaceId = workspace.id;
|
||||
const memberCount = useMemberCount(workspaceId);
|
||||
@@ -165,16 +167,20 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
planName: plan,
|
||||
memberLimit,
|
||||
})}
|
||||
,
|
||||
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
|
||||
<span className={style.goUpgrade}>
|
||||
{t['com.affine.payment.member.description.go-upgrade']()}
|
||||
</span>
|
||||
<ArrowRightBigIcon className={style.arrowRight} />
|
||||
</div>
|
||||
{upgradable ? (
|
||||
<>
|
||||
,
|
||||
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
|
||||
<span className={style.goUpgrade}>
|
||||
{t['com.affine.payment.member.description.go-upgrade']()}
|
||||
</span>
|
||||
<ArrowRightBigIcon className={style.arrowRight} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}, [handleUpgrade, memberLimit, plan, t]);
|
||||
}, [handleUpgrade, memberLimit, plan, t, upgradable]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import {
|
||||
fetchWithTraceReport,
|
||||
type ListHistoryQuery,
|
||||
listHistoryQuery,
|
||||
recoverDocMutation,
|
||||
} from '@affine/graphql';
|
||||
import {
|
||||
useMutateQueryResource,
|
||||
useMutation,
|
||||
useQueryInfinite,
|
||||
} from '@affine/workspace/affine/gql';
|
||||
import { createAffineCloudBlobEngine } from '@affine/workspace/blob';
|
||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { revertUpdate } from '@toeverything/y-indexeddb';
|
||||
import { useMemo } from 'react';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
import { applyUpdate } from 'yjs';
|
||||
|
||||
const logger = new DebugLogger('page-history');
|
||||
|
||||
type DocHistory = ListHistoryQuery['workspace']['histories'][number];
|
||||
|
||||
export const usePageSnapshotList = (workspaceId: string, pageDocId: string) => {
|
||||
const pageSize = 10;
|
||||
const { data, loadingMore, loadMore } = useQueryInfinite({
|
||||
query: listHistoryQuery,
|
||||
getVariables: (_, previousPageData) => {
|
||||
// use the timestamp of the last history as the cursor
|
||||
const before = previousPageData?.workspace.histories.at(-1)?.timestamp;
|
||||
const vars = {
|
||||
pageDocId: pageDocId,
|
||||
workspaceId: workspaceId,
|
||||
before: before,
|
||||
take: pageSize,
|
||||
};
|
||||
|
||||
return vars;
|
||||
},
|
||||
});
|
||||
|
||||
const shouldLoadMore = useMemo(() => {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const lastPage = data.at(-1);
|
||||
if (!lastPage) {
|
||||
return false;
|
||||
}
|
||||
return lastPage.workspace.histories.length === pageSize;
|
||||
}, [data]);
|
||||
|
||||
const histories = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
return data.flatMap(page => page.workspace.histories);
|
||||
}, [data]);
|
||||
|
||||
return [
|
||||
histories,
|
||||
shouldLoadMore ? loadMore : undefined,
|
||||
loadingMore,
|
||||
] as const;
|
||||
};
|
||||
|
||||
const snapshotFetcher = async (
|
||||
[workspaceId, pageDocId, ts]: [
|
||||
workspaceId: string,
|
||||
pageDocId: string,
|
||||
ts: string,
|
||||
] // timestamp is the key to the history/snapshot
|
||||
) => {
|
||||
if (!ts) {
|
||||
return null;
|
||||
}
|
||||
const res = await fetchWithTraceReport(
|
||||
runtimeConfig.serverUrlPrefix +
|
||||
`/api/workspaces/${workspaceId}/docs/${pageDocId}/histories/${ts}`,
|
||||
{
|
||||
priority: 'high',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch snapshot');
|
||||
}
|
||||
|
||||
const snapshot = await res.arrayBuffer();
|
||||
if (!snapshot) {
|
||||
throw new Error('Invalid snapshot');
|
||||
}
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
// attach the Page shown in the modal to a temporary workspace
|
||||
// so that we do not need to worry about providers etc
|
||||
// todo: fix references to the page (the referenced page will shown as deleted)
|
||||
// if we simply clone the current workspace, it maybe time consuming right?
|
||||
const workspaceMap = new Map<string, Workspace>();
|
||||
|
||||
// assume the workspace is a cloud workspace since the history feature is only enabled for cloud workspace
|
||||
const getOrCreateWorkspace = (workspaceId: string) => {
|
||||
let workspace = workspaceMap.get(workspaceId);
|
||||
if (!workspace) {
|
||||
const blobEngine = createAffineCloudBlobEngine(workspaceId);
|
||||
workspace = new Workspace({
|
||||
id: workspaceId,
|
||||
providerCreators: [],
|
||||
blobStorages: [
|
||||
() => ({
|
||||
crud: {
|
||||
async get(key) {
|
||||
return (await blobEngine.get(key)) ?? null;
|
||||
},
|
||||
async set(key, value) {
|
||||
await blobEngine.set(key, value);
|
||||
return key;
|
||||
},
|
||||
async delete(key) {
|
||||
return blobEngine.delete(key);
|
||||
},
|
||||
async list() {
|
||||
return blobEngine.list();
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
workspaceMap.set(workspaceId, workspace);
|
||||
}
|
||||
return workspace;
|
||||
};
|
||||
|
||||
// workspace id + page id + timestamp -> snapshot (update binary)
|
||||
export const usePageHistory = (
|
||||
workspaceId: string,
|
||||
pageDocId: string,
|
||||
ts?: string
|
||||
) => {
|
||||
// snapshot should be immutable. so we use swr immutable to disable revalidation
|
||||
const { data } = useSWRImmutable<ArrayBuffer | null>(
|
||||
[workspaceId, pageDocId, ts],
|
||||
{
|
||||
fetcher: snapshotFetcher,
|
||||
suspense: false,
|
||||
}
|
||||
);
|
||||
return data ?? undefined;
|
||||
};
|
||||
|
||||
// workspace id + page id + timestamp + snapshot -> Page (to be used for rendering in blocksuite editor)
|
||||
export const useSnapshotPage = (
|
||||
workspaceId: string,
|
||||
pageDocId: string,
|
||||
ts?: string,
|
||||
snapshot?: ArrayBuffer
|
||||
) => {
|
||||
const page = useMemo(() => {
|
||||
if (!ts) {
|
||||
return null;
|
||||
}
|
||||
const pageId = pageDocId + '-' + ts;
|
||||
const historyShellWorkspace = getOrCreateWorkspace(workspaceId);
|
||||
let page = historyShellWorkspace.getPage(pageId);
|
||||
if (!page && snapshot) {
|
||||
page = historyShellWorkspace.createPage({
|
||||
id: pageId,
|
||||
});
|
||||
page.awarenessStore.setReadonly(page, true);
|
||||
const spaceDoc = page.spaceDoc;
|
||||
page
|
||||
.load(() => applyUpdate(spaceDoc, new Uint8Array(snapshot)))
|
||||
.catch(console.error); // must load before applyUpdate
|
||||
}
|
||||
return page;
|
||||
}, [pageDocId, snapshot, ts, workspaceId]);
|
||||
|
||||
return page;
|
||||
};
|
||||
|
||||
export const historyListGroupByDay = (histories: DocHistory[]) => {
|
||||
const map = new Map<string, DocHistory[]>();
|
||||
for (const history of histories) {
|
||||
const day = new Date(history.timestamp).toLocaleDateString();
|
||||
const list = map.get(day) ?? [];
|
||||
list.push(history);
|
||||
map.set(day, list);
|
||||
}
|
||||
return [...map.entries()];
|
||||
};
|
||||
|
||||
export const useRestorePage = (workspace: Workspace, pageId: string) => {
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
const mutateQueryResource = useMutateQueryResource();
|
||||
const { trigger: recover, isMutating } = useMutation({
|
||||
mutation: recoverDocMutation,
|
||||
});
|
||||
const { getPageMeta, setPageTitle } = usePageMetaHelper(workspace);
|
||||
|
||||
const onRestore = useMemo(() => {
|
||||
return async (version: string, update: Uint8Array) => {
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
const pageDocId = page.spaceDoc.guid;
|
||||
revertUpdate(page.spaceDoc, update, key => {
|
||||
assertEquals(key, 'blocks'); // only expect this value is 'blocks'
|
||||
return 'Map';
|
||||
});
|
||||
|
||||
// should also update the page title, since it may be changed in the history
|
||||
const title = page.meta.title;
|
||||
|
||||
if (getPageMeta(pageDocId)?.title !== title) {
|
||||
setPageTitle(pageDocId, title);
|
||||
}
|
||||
|
||||
await recover({
|
||||
docId: pageDocId,
|
||||
timestamp: version,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
await mutateQueryResource(listHistoryQuery, vars => {
|
||||
return (
|
||||
vars.pageDocId === pageDocId && vars.workspaceId === workspace.id
|
||||
);
|
||||
});
|
||||
|
||||
logger.info('Page restored', pageDocId, version);
|
||||
};
|
||||
}, [
|
||||
getPageMeta,
|
||||
mutateQueryResource,
|
||||
page,
|
||||
recover,
|
||||
setPageTitle,
|
||||
workspace.id,
|
||||
]);
|
||||
|
||||
return {
|
||||
onRestore,
|
||||
isMutating,
|
||||
};
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
export const EmptyHistoryShape = () => (
|
||||
<svg
|
||||
width="200"
|
||||
height="174"
|
||||
viewBox="0 0 200 174"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="51.724"
|
||||
y="38.4615"
|
||||
width="96.5517"
|
||||
height="96.5517"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M51.8339 86.7374L99.9999 38.5714L148.166 86.7374L99.9999 134.903L51.8339 86.7374Z"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M99.6052 38.1966C107.662 33.4757 117.043 30.7695 127.055 30.7695C157.087 30.7695 181.432 55.1148 181.432 85.1462C181.432 107.547 167.887 126.783 148.541 135.113"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M148.375 86.4722C153.096 94.5291 155.802 103.91 155.802 113.922C155.802 143.954 131.457 168.299 101.426 168.299C79.0254 168.299 59.7886 154.754 51.4587 135.408"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M100.394 135.113C92.3373 139.834 82.9568 142.54 72.9441 142.54C42.9127 142.54 18.5675 118.195 18.5675 88.1634C18.5675 65.763 32.1124 46.5261 51.4587 38.1963"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M51.4585 87.1318C46.7377 79.0749 44.0315 69.6943 44.0315 59.6817C44.0315 29.6503 68.3767 5.30503 98.4081 5.30503C120.809 5.30503 140.045 18.8499 148.375 38.1963"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M51.4587 38.1963L148.541 135.279"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M148.541 38.1963L51.4587 135.279"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M99.9998 38.1963V135.279"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<path
|
||||
d="M148.541 86.7378L51.4588 86.7378"
|
||||
stroke="var(--affine-text-disable-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="148.275"
|
||||
cy="38.4617"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="148.275"
|
||||
cy="135.013"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="148.275"
|
||||
cy="86.7376"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="51.7241"
|
||||
cy="38.4617"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="51.7241"
|
||||
cy="135.013"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="51.7241"
|
||||
cy="86.7376"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="100"
|
||||
cy="38.4617"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
transform="rotate(-90 100 38.4617)"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="100"
|
||||
cy="86.2073"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
transform="rotate(-90 100 86.2073)"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="100"
|
||||
cy="135.013"
|
||||
rx="3.97878"
|
||||
ry="3.97878"
|
||||
transform="rotate(-90 100 135.013)"
|
||||
fill="var(--affine-text-primary-color)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,446 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
import {
|
||||
BlockSuiteEditor,
|
||||
BlockSuiteFallback,
|
||||
} from '@affine/component/block-suite-editor';
|
||||
import type { PageMode } from '@affine/core/atoms';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { ConfirmModal, Modal } from '@toeverything/components/modal';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
||||
import {
|
||||
EdgelessSwitchItem,
|
||||
PageSwitchItem,
|
||||
} from '../../blocksuite/block-suite-mode-switch/switch-items';
|
||||
import { AffineErrorBoundary } from '../affine-error-boundary';
|
||||
import {
|
||||
historyListGroupByDay,
|
||||
usePageHistory,
|
||||
usePageSnapshotList,
|
||||
useRestorePage,
|
||||
useSnapshotPage,
|
||||
} from './data';
|
||||
import { EmptyHistoryShape } from './empty-history-shape';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface PageHistoryModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
const contentOptions: DialogContentProps = {
|
||||
['data-testid' as string]: 'page-history-modal',
|
||||
onPointerDownOutside: e => {
|
||||
e.preventDefault();
|
||||
},
|
||||
style: {
|
||||
padding: 0,
|
||||
maxWidth: 944,
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
};
|
||||
|
||||
const ModalContainer = ({
|
||||
onOpenChange,
|
||||
open,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}>) => {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
width="calc(100% - 64px)"
|
||||
height="80%"
|
||||
withoutCloseButton
|
||||
contentOptions={contentOptions}
|
||||
>
|
||||
<AffineErrorBoundary>{children}</AffineErrorBoundary>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const localTimeFormatter = new Intl.DateTimeFormat('en', {
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
const timestampToLocalTime = (ts: string) => {
|
||||
return localTimeFormatter.format(new Date(ts));
|
||||
};
|
||||
|
||||
interface HistoryEditorPreviewProps {
|
||||
workspaceId: string;
|
||||
pageDocId: string;
|
||||
ts?: string;
|
||||
snapshot?: ArrayBuffer;
|
||||
mode: PageMode;
|
||||
onModeChange: (mode: PageMode) => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const HistoryEditorPreview = ({
|
||||
ts,
|
||||
snapshot,
|
||||
onModeChange,
|
||||
mode,
|
||||
workspaceId,
|
||||
pageDocId,
|
||||
title,
|
||||
}: HistoryEditorPreviewProps) => {
|
||||
const onSwitchToPageMode = useCallback(() => {
|
||||
onModeChange('page');
|
||||
}, [onModeChange]);
|
||||
const onSwitchToEdgelessMode = useCallback(() => {
|
||||
onModeChange('edgeless');
|
||||
}, [onModeChange]);
|
||||
const page = useSnapshotPage(workspaceId, pageDocId, ts, snapshot);
|
||||
|
||||
return (
|
||||
<div className={styles.previewWrapper}>
|
||||
<div className={styles.previewHeader}>
|
||||
<StyledEditorModeSwitch switchLeft={mode === 'page'}>
|
||||
<PageSwitchItem
|
||||
data-testid="switch-page-mode-button"
|
||||
active={mode === 'page'}
|
||||
onClick={onSwitchToPageMode}
|
||||
/>
|
||||
<EdgelessSwitchItem
|
||||
data-testid="switch-edgeless-mode-button"
|
||||
active={mode === 'edgeless'}
|
||||
onClick={onSwitchToEdgelessMode}
|
||||
/>
|
||||
</StyledEditorModeSwitch>
|
||||
|
||||
<div className={styles.previewHeaderTitle}>{title}</div>
|
||||
|
||||
<div className={styles.previewHeaderTimestamp}>
|
||||
{ts ? timestampToLocalTime(ts) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{page ? (
|
||||
<BlockSuiteEditor
|
||||
className={styles.editor}
|
||||
mode={mode}
|
||||
page={page}
|
||||
onModeChange={onModeChange}
|
||||
/>
|
||||
) : (
|
||||
<BlockSuiteFallback />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PageHistoryList = ({
|
||||
pageDocId,
|
||||
workspaceId,
|
||||
activeVersion,
|
||||
onVersionChange,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
pageDocId: string;
|
||||
activeVersion?: string;
|
||||
onVersionChange: (version: string) => void;
|
||||
}) => {
|
||||
const [historyList, loadMore, loadingMore] = usePageSnapshotList(
|
||||
workspaceId,
|
||||
pageDocId
|
||||
);
|
||||
const historyListByDay = useMemo(() => {
|
||||
return historyListGroupByDay(historyList);
|
||||
}, [historyList]);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (historyList.length > 0 && !activeVersion) {
|
||||
onVersionChange(historyList[0].timestamp);
|
||||
}
|
||||
}, [activeVersion, historyList, onVersionChange]);
|
||||
|
||||
return (
|
||||
<div className={styles.historyList}>
|
||||
<div className={styles.historyListHeader}>
|
||||
{t['com.affine.history.version-history']()}
|
||||
</div>
|
||||
<Scrollable.Root className={styles.historyListScrollable}>
|
||||
<Scrollable.Viewport className={styles.historyListScrollableInner}>
|
||||
{historyListByDay.map(([day, list]) => {
|
||||
return (
|
||||
<div key={day} className={styles.historyItemGroup}>
|
||||
<div className={styles.historyItemGroupTitle}>{day}</div>
|
||||
{list.map(history => (
|
||||
<div
|
||||
className={styles.historyItem}
|
||||
key={history.timestamp}
|
||||
data-testid="version-history-item"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onVersionChange(history.timestamp);
|
||||
}}
|
||||
data-active={activeVersion === history.timestamp}
|
||||
>
|
||||
<button>{timestampToLocalTime(history.timestamp)}</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{loadMore ? (
|
||||
<Button
|
||||
type="plain"
|
||||
loading={loadingMore}
|
||||
disabled={loadingMore}
|
||||
className={styles.historyItemLoadMore}
|
||||
onClick={loadMore}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
) : null}
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ConfirmRestoreModalProps {
|
||||
open: boolean;
|
||||
onConfirm: (res: boolean) => void;
|
||||
isMutating: boolean;
|
||||
}
|
||||
|
||||
const ConfirmRestoreModal = ({
|
||||
isMutating,
|
||||
open,
|
||||
onConfirm,
|
||||
}: ConfirmRestoreModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onConfirm(true);
|
||||
}, [onConfirm]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
onConfirm(false);
|
||||
}, [onConfirm]);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
open={open}
|
||||
onOpenChange={handleCancel}
|
||||
title={t['com.affine.history.restore-current-version']()}
|
||||
description={t['com.affine.history.confirm-restore-modal.hint']()}
|
||||
cancelText={t['Cancel']()}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'confirm-restore-history-modal',
|
||||
style: {
|
||||
padding: '20px 26px',
|
||||
},
|
||||
}}
|
||||
confirmButtonOptions={{
|
||||
loading: isMutating,
|
||||
type: 'primary',
|
||||
['data-testid' as string]: 'confirm-restore-history-button',
|
||||
children: t['com.affine.history.confirm-restore-modal.restore'](),
|
||||
}}
|
||||
onConfirm={handleConfirm}
|
||||
></ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyHistoryPrompt = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.emptyHistoryPrompt}
|
||||
data-testid="empty-history-prompt"
|
||||
>
|
||||
<EmptyHistoryShape />
|
||||
<div className={styles.emptyHistoryPromptTitle}>
|
||||
{t['com.affine.history.empty-prompt.title']()}
|
||||
</div>
|
||||
<div className={styles.emptyHistoryPromptDescription}>
|
||||
{t['com.affine.history.empty-prompt.description']()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PageHistoryManager = ({
|
||||
workspace,
|
||||
pageId,
|
||||
onClose,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const workspaceId = workspace.id;
|
||||
const [activeVersion, setActiveVersion] = useState<string>();
|
||||
|
||||
const pageDocId = useMemo(() => {
|
||||
return workspace.getPage(pageId)?.spaceDoc.guid ?? pageId;
|
||||
}, [pageId, workspace]);
|
||||
|
||||
const snapshot = usePageHistory(workspaceId, pageDocId, activeVersion);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { onRestore, isMutating } = useRestorePage(workspace, pageId);
|
||||
|
||||
const handleRestore = useMemo(
|
||||
() => async () => {
|
||||
if (!activeVersion || !snapshot) {
|
||||
return;
|
||||
}
|
||||
await onRestore(activeVersion, new Uint8Array(snapshot));
|
||||
// close the modal after restore
|
||||
onClose();
|
||||
},
|
||||
[activeVersion, onClose, onRestore, snapshot]
|
||||
);
|
||||
|
||||
const defaultPreviewPageMode = useAtomValue(currentModeAtom);
|
||||
const [mode, setMode] = useState<PageMode>(defaultPreviewPageMode);
|
||||
|
||||
const title = useMemo(
|
||||
() => workspace.getPage(pageId)?.meta.title || t['Untitled'](),
|
||||
[pageId, t, workspace]
|
||||
);
|
||||
|
||||
const [showRestoreConfirmModal, setShowRestoreConfirmModal] = useState(false);
|
||||
|
||||
const showRestoreConfirm = useCallback(() => {
|
||||
setShowRestoreConfirmModal(true);
|
||||
}, []);
|
||||
|
||||
const onConfirmRestore = useAsyncCallback(
|
||||
async res => {
|
||||
if (res) {
|
||||
await handleRestore();
|
||||
}
|
||||
setShowRestoreConfirmModal(false);
|
||||
},
|
||||
[handleRestore]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.modalContent} data-empty={!activeVersion}>
|
||||
<HistoryEditorPreview
|
||||
workspaceId={workspaceId}
|
||||
pageDocId={pageDocId}
|
||||
ts={activeVersion}
|
||||
snapshot={snapshot}
|
||||
mode={mode}
|
||||
onModeChange={setMode}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
<PageHistoryList
|
||||
workspaceId={workspaceId}
|
||||
pageDocId={pageDocId}
|
||||
activeVersion={activeVersion}
|
||||
onVersionChange={setActiveVersion}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!activeVersion ? (
|
||||
<div className={styles.modalContent}>
|
||||
<EmptyHistoryPrompt />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={styles.historyFooter}>
|
||||
<Button type="plain" onClick={onClose}>
|
||||
{t['com.affine.history.back-to-page']()}
|
||||
</Button>
|
||||
<div className={styles.spacer} />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={showRestoreConfirm}
|
||||
disabled={isMutating || !activeVersion}
|
||||
>
|
||||
{t['com.affine.history.restore-current-version']()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConfirmRestoreModal
|
||||
open={showRestoreConfirmModal}
|
||||
isMutating={isMutating}
|
||||
onConfirm={onConfirmRestore}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageHistoryModal = ({
|
||||
onOpenChange,
|
||||
open,
|
||||
pageId,
|
||||
workspace,
|
||||
}: PageHistoryModalProps) => {
|
||||
const onClose = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
return (
|
||||
<ModalContainer onOpenChange={onOpenChange} open={open}>
|
||||
<Suspense fallback={<BlockSuiteFallback />}>
|
||||
<PageHistoryManager
|
||||
onClose={onClose}
|
||||
pageId={pageId}
|
||||
workspace={workspace}
|
||||
/>
|
||||
</Suspense>
|
||||
</ModalContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const GlobalPageHistoryModal = () => {
|
||||
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
open,
|
||||
}));
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
return (
|
||||
<PageHistoryModal
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
pageId={pageId}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './history-modal';
|
||||
@@ -0,0 +1,185 @@
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
const headerHeight = createVar('header-height');
|
||||
const footerHeight = createVar('footer-height');
|
||||
const historyListWidth = createVar('history-list-width');
|
||||
|
||||
export const root = style({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
vars: {
|
||||
[headerHeight]: '52px',
|
||||
[footerHeight]: '68px',
|
||||
[historyListWidth]: '160px',
|
||||
},
|
||||
});
|
||||
|
||||
export const modalContent = style({
|
||||
display: 'flex',
|
||||
height: `calc(100% - ${footerHeight})`,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
selectors: {
|
||||
'&[data-empty="true"]': {
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const previewWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
width: `calc(100% - ${historyListWidth})`,
|
||||
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||
});
|
||||
|
||||
export const previewHeader = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: headerHeight,
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
padding: '0 12px',
|
||||
flexShrink: 0,
|
||||
gap: 12,
|
||||
});
|
||||
|
||||
export const previewHeaderTitle = style({
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 600,
|
||||
maxWidth: 400, // better responsiveness
|
||||
});
|
||||
|
||||
export const previewHeaderTimestamp = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||
padding: '0 10px',
|
||||
borderRadius: 4,
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
||||
|
||||
export const editor = style({
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const historyList = style({
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
width: historyListWidth,
|
||||
flexShrink: 0,
|
||||
borderLeft: '1px solid var(--affine-border-color)',
|
||||
});
|
||||
|
||||
export const historyListScrollable = style({
|
||||
height: `calc(100% - ${headerHeight})`,
|
||||
});
|
||||
|
||||
export const historyListScrollableInner = style({
|
||||
display: 'flex',
|
||||
gap: 16,
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const historyListHeader = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: 52,
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
fontWeight: 'bold',
|
||||
flexShrink: 0,
|
||||
padding: '0 12px',
|
||||
});
|
||||
|
||||
export const historyItemGroup = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
rowGap: 6,
|
||||
});
|
||||
|
||||
export const historyItemGroupTitle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
});
|
||||
|
||||
export const historyItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 12px',
|
||||
height: 32,
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover, &[data-active=true]': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const historyItemLoadMore = style([
|
||||
historyItem,
|
||||
{
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
flexShrink: 0,
|
||||
borderRadius: 0,
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
globalStyle(`${historyItem} button`, {
|
||||
color: 'inherit',
|
||||
});
|
||||
|
||||
export const historyFooter = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 68,
|
||||
borderTop: '1px solid var(--affine-border-color)',
|
||||
padding: '0 24px',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
});
|
||||
|
||||
export const spacer = style({
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
export const emptyHistoryPrompt = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
zIndex: 1,
|
||||
gap: 20,
|
||||
});
|
||||
|
||||
export const emptyHistoryPromptTitle = style({
|
||||
fontWeight: 600,
|
||||
fontSize: 'var(--affine-font-h-5)',
|
||||
});
|
||||
|
||||
export const emptyHistoryPromptDescription = style({
|
||||
width: 320,
|
||||
textAlign: 'center',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
openSignOutModalAtom,
|
||||
} from '../../../../atoms';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
|
||||
import { useUserSubscription } from '../../../../hooks/use-subscription';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import * as style from './style.css';
|
||||
@@ -167,6 +168,7 @@ export const AvatarAndName = () => {
|
||||
|
||||
const StoragePanel = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const isSelfHosted = useSelfHosted();
|
||||
|
||||
const { data } = useQuery({
|
||||
query: allBlobSizesQuery,
|
||||
@@ -175,6 +177,7 @@ const StoragePanel = () => {
|
||||
const [subscription] = useUserSubscription();
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
|
||||
// TODO(@JimmFly): get limit from user usage query directly after #4720 is merged
|
||||
const maxLimit = useMemo(() => {
|
||||
return bytes.parse(plan === SubscriptionPlan.Free ? '10GB' : '100GB');
|
||||
}, [plan]);
|
||||
@@ -199,6 +202,7 @@ const StoragePanel = () => {
|
||||
plan={plan}
|
||||
value={data.collectAllBlobSizes.size}
|
||||
onUpgrade={onUpgrade}
|
||||
upgradable={!isSelfHosted}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
|
||||
+15
-12
@@ -8,6 +8,7 @@ import {
|
||||
import type { ReactElement, SVGProps } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
||||
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
|
||||
import { AboutAffine } from './about';
|
||||
import { AppearanceSettings } from './appearance';
|
||||
import { BillingSettings } from './billing';
|
||||
@@ -36,6 +37,7 @@ export type GeneralSettingList = GeneralSettingListItem[];
|
||||
export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
const t = useAFFiNEI18N();
|
||||
const status = useCurrentLoginStatus();
|
||||
const isSelfHosted = useSelfHosted();
|
||||
|
||||
const settings: GeneralSettingListItem[] = [
|
||||
{
|
||||
@@ -50,13 +52,6 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
icon: KeyboardIcon,
|
||||
testId: 'shortcuts-panel-trigger',
|
||||
},
|
||||
{
|
||||
key: 'plans',
|
||||
title: t['com.affine.payment.title'](),
|
||||
icon: UpgradeIcon,
|
||||
testId: 'plans-panel-trigger',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'plugins',
|
||||
title: 'Plugins',
|
||||
@@ -71,13 +66,21 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
},
|
||||
];
|
||||
|
||||
if (status === 'authenticated') {
|
||||
if (!isSelfHosted) {
|
||||
settings.splice(3, 0, {
|
||||
key: 'billing',
|
||||
title: t['com.affine.payment.billing-setting.title'](),
|
||||
icon: PaymentIcon,
|
||||
testId: 'billing-panel-trigger',
|
||||
key: 'plans',
|
||||
title: t['com.affine.payment.title'](),
|
||||
icon: UpgradeIcon,
|
||||
testId: 'plans-panel-trigger',
|
||||
});
|
||||
if (status === 'authenticated') {
|
||||
settings.splice(3, 0, {
|
||||
key: 'billing',
|
||||
title: t['com.affine.payment.billing-setting.title'](),
|
||||
icon: PaymentIcon,
|
||||
testId: 'billing-panel-trigger',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
|
||||
+35
-1
@@ -1,5 +1,6 @@
|
||||
import { FlexWrapper } from '@affine/component';
|
||||
import { Export, MoveToTrash } from '@affine/component/page-list';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
EditIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
HistoryIcon,
|
||||
ImportIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { setPageModeAtom } from '../../../atoms';
|
||||
@@ -36,6 +38,7 @@ import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helpe
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { toast } from '../../../utils';
|
||||
import { PageHistoryModal } from '../../affine/page-history-modal/history-modal';
|
||||
import { HeaderDropDownButton } from '../../pure/header-drop-down-button';
|
||||
import { usePageHelper } from '../block-suite-page-list/utils';
|
||||
|
||||
@@ -68,6 +71,12 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
|
||||
|
||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
setHistoryModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleOpenTrashModal = useCallback(() => {
|
||||
setTrashModal({
|
||||
open: true,
|
||||
@@ -207,6 +216,23 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
>
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
|
||||
{workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
|
||||
runtimeConfig.enablePageHistory ? (
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<HistoryIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="editor-option-menu-history"
|
||||
onSelect={openHistoryModal}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['com.affine.history.view-history-version']()}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
||||
<Export exportHandler={exportHandler} />
|
||||
<MenuSeparator />
|
||||
<MoveToTrash
|
||||
@@ -228,6 +254,14 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
>
|
||||
<HeaderDropDownButton />
|
||||
</Menu>
|
||||
{workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
||||
<PageHistoryModal
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
open={historyModalOpen}
|
||||
pageId={pageId}
|
||||
onOpenChange={setHistoryModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,8 +30,8 @@ import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||
import { useDeleteCollectionInfo } from '../../hooks/affine/use-delete-collection-info';
|
||||
import { useGeneralShortcuts } from '../../hooks/affine/use-shortcuts';
|
||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/use-shortcut-commands';
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
|
||||
@@ -169,7 +169,7 @@ export const RootAppSidebar = ({
|
||||
const closeUserWorkspaceList = useCallback(() => {
|
||||
setOpenUserWorkspaceList(false);
|
||||
}, [setOpenUserWorkspaceList]);
|
||||
useRegisterBlocksuiteEditorCommands(router.back, router.forward);
|
||||
useRegisterBrowserHistoryCommands(router.back, router.forward);
|
||||
const userInfo = useDeleteCollectionInfo();
|
||||
return (
|
||||
<AppSidebar
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSWRConfig } from 'swr';
|
||||
export function useMutateCloud() {
|
||||
const { mutate } = useSWRConfig();
|
||||
return useCallback(async () => {
|
||||
// todo: should not mutate all graphql cache
|
||||
return mutate(key => {
|
||||
if (Array.isArray(key)) {
|
||||
return key[0] === 'cloud';
|
||||
|
||||
+35
-5
@@ -1,25 +1,29 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@toeverything/infra/command';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { pageHistoryModalAtom } from '../../atoms/page-history';
|
||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||
import { useExportPage } from './use-export-page';
|
||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands(
|
||||
blockSuiteWorkspace: Workspace,
|
||||
pageId: string,
|
||||
mode: 'page' | 'edgeless'
|
||||
) {
|
||||
const t = useAFFiNEI18N();
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(currentPage);
|
||||
@@ -28,6 +32,15 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
const favorite = pageMeta.favorite ?? false;
|
||||
const trash = pageMeta.trash ?? false;
|
||||
|
||||
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
setPageHistoryModalState(() => ({
|
||||
pageId,
|
||||
open: true,
|
||||
}));
|
||||
}, [pageId, setPageHistoryModalState]);
|
||||
|
||||
const { togglePageMode, toggleFavorite, restoreFromTrash } =
|
||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const exportHandler = useExportPage(currentPage);
|
||||
@@ -40,12 +53,14 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
});
|
||||
}, [pageId, pageMeta.title, setTrashModal]);
|
||||
|
||||
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
const preconditionStrategy = () =>
|
||||
PreconditionStrategy.InPaperOrEdgeless && !trash;
|
||||
|
||||
//TODO: add back when edgeless presentation is ready
|
||||
// TODO: add back when edgeless presentation is ready
|
||||
|
||||
// this is pretty hack and easy to break. need a better way to communicate with blocksuite editor
|
||||
// unsubs.push(
|
||||
@@ -189,6 +204,20 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
})
|
||||
);
|
||||
|
||||
if (runtimeConfig.enablePageHistory && isCloudWorkspace) {
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-page-history`,
|
||||
category: `editor:${mode}`,
|
||||
icon: <HistoryIcon />,
|
||||
label: t['com.affine.cmdk.affine.editor.reveal-page-history-modal'](),
|
||||
run() {
|
||||
openHistoryModal();
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
@@ -198,11 +227,12 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
onClickDelete,
|
||||
exportHandler,
|
||||
pageId,
|
||||
pageMeta.title,
|
||||
restoreFromTrash,
|
||||
t,
|
||||
toggleFavorite,
|
||||
togglePageMode,
|
||||
trash,
|
||||
isCloudWorkspace,
|
||||
openHistoryModal,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { serverConfigQuery } from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
import type { BareFetcher, Middleware } from 'swr';
|
||||
|
||||
const wrappedFetcher = (fetcher: BareFetcher<any> | null, ...args: any[]) =>
|
||||
fetcher?.(...args).catch(() => null);
|
||||
|
||||
const errorHandler: Middleware = useSWRNext => (key, fetcher, config) => {
|
||||
return useSWRNext(key, wrappedFetcher.bind(null, fetcher), config);
|
||||
};
|
||||
|
||||
export const useServerFlavor = () => {
|
||||
const { data: config, error } = useQuery(
|
||||
{ query: serverConfigQuery },
|
||||
{
|
||||
use: [errorHandler],
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnMount: false,
|
||||
revalidateIfStale: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !config) {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
return config.serverConfig.flavor;
|
||||
};
|
||||
|
||||
export const useSelfHosted = () => {
|
||||
const serverFlavor = useServerFlavor();
|
||||
|
||||
return ['local', 'selfhosted'].includes(serverFlavor);
|
||||
};
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
} from '@toeverything/infra/command';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands(
|
||||
export function useRegisterBrowserHistoryCommands(
|
||||
back: () => unknown,
|
||||
forward: () => unknown
|
||||
) {
|
||||
@@ -2,6 +2,8 @@ import { type SubscriptionQuery, subscriptionQuery } from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
|
||||
import { useSelfHosted } from './affine/use-server-flavor';
|
||||
|
||||
export type Subscription = NonNullable<
|
||||
NonNullable<SubscriptionQuery['currentUser']>['subscription']
|
||||
>;
|
||||
@@ -12,6 +14,7 @@ const selector = (data: SubscriptionQuery) =>
|
||||
data.currentUser?.subscription ?? null;
|
||||
|
||||
export const useUserSubscription = () => {
|
||||
const isSelfHosted = useSelfHosted();
|
||||
const { data, mutate } = useQuery({
|
||||
query: subscriptionQuery,
|
||||
});
|
||||
@@ -36,5 +39,9 @@ export const useUserSubscription = () => {
|
||||
[mutate]
|
||||
);
|
||||
|
||||
if (isSelfHosted) {
|
||||
return [selector(data), () => {}] as const;
|
||||
}
|
||||
|
||||
return [selector(data), set] as const;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { setPageModeAtom } from '../../atoms';
|
||||
import { collectionsCRUDAtom } from '../../atoms/collections';
|
||||
import { currentModeAtom } from '../../atoms/mode';
|
||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
import { GlobalPageHistoryModal } from '../../components/affine/page-history-modal';
|
||||
import { WorkspaceHeader } from '../../components/workspace-header';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
|
||||
import { useCurrentSyncEngineStatus } from '../../hooks/current/use-current-sync-engine';
|
||||
@@ -41,7 +42,7 @@ const DetailPageImpl = (): ReactElement => {
|
||||
const { setTemporaryFilter } = useCollectionManager(collectionsCRUDAtom);
|
||||
const mode = useAtomValue(currentModeAtom);
|
||||
const setPageMode = useSetAtom(setPageModeAtom);
|
||||
useRegisterBlocksuiteEditorCommands(blockSuiteWorkspace, currentPageId, mode);
|
||||
useRegisterBlocksuiteEditorCommands(currentPageId, mode);
|
||||
const onLoad = useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
try {
|
||||
@@ -101,6 +102,8 @@ const DetailPageImpl = (): ReactElement => {
|
||||
currentPageId={currentPageId}
|
||||
onLoadEditor={onLoad}
|
||||
/>
|
||||
|
||||
<GlobalPageHistoryModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
"@affine/sdk": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/vue-hello-world-plugin": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@electron-forge/cli": "^7.1.0",
|
||||
"@electron-forge/core": "^7.1.0",
|
||||
"@electron-forge/core-utils": "^7.1.0",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import type { BuildContext } from 'esbuild';
|
||||
import * as esbuild from 'esbuild';
|
||||
|
||||
import { config, electronDir } from './common';
|
||||
import { config, electronDir, rootDir } from './common';
|
||||
|
||||
// this means we don't spawn electron windows, mainly for testing
|
||||
const watchMode = process.argv.includes('--watch');
|
||||
@@ -29,7 +30,10 @@ function spawnOrReloadElectron() {
|
||||
spawnProcess = null;
|
||||
}
|
||||
|
||||
spawnProcess = spawn('electron', ['.'], {
|
||||
const ext = process.platform === 'win32' ? '.cmd' : '';
|
||||
const exe = resolve(rootDir, 'node_modules', '.bin', `electron${ext}`);
|
||||
|
||||
spawnProcess = spawn(exe, ['.'], {
|
||||
cwd: electronDir,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
query listHistory(
|
||||
$workspaceId: String!
|
||||
$pageDocId: String!
|
||||
$take: Int
|
||||
$before: DateTime
|
||||
) {
|
||||
workspace(id: $workspaceId) {
|
||||
histories(guid: $pageDocId, take: $take, before: $before) {
|
||||
id
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,6 +362,22 @@ query getWorkspaces {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const listHistoryQuery = {
|
||||
id: 'listHistoryQuery' as const,
|
||||
operationName: 'listHistory',
|
||||
definitionName: 'workspace',
|
||||
containsFile: false,
|
||||
query: `
|
||||
query listHistory($workspaceId: String!, $pageDocId: String!, $take: Int, $before: DateTime) {
|
||||
workspace(id: $workspaceId) {
|
||||
histories(guid: $pageDocId, take: $take, before: $before) {
|
||||
id
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getInvoicesCountQuery = {
|
||||
id: 'getInvoicesCountQuery' as const,
|
||||
operationName: 'getInvoicesCount',
|
||||
@@ -445,6 +461,17 @@ mutation publishPage($workspaceId: String!, $pageId: String!, $mode: PublicPageM
|
||||
}`,
|
||||
};
|
||||
|
||||
export const recoverDocMutation = {
|
||||
id: 'recoverDocMutation' as const,
|
||||
operationName: 'recoverDoc',
|
||||
definitionName: 'recoverDoc',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation recoverDoc($workspaceId: String!, $docId: String!, $timestamp: DateTime!) {
|
||||
recoverDoc(workspaceId: $workspaceId, guid: $docId, timestamp: $timestamp)
|
||||
}`,
|
||||
};
|
||||
|
||||
export const removeAvatarMutation = {
|
||||
id: 'removeAvatarMutation' as const,
|
||||
operationName: 'removeAvatar',
|
||||
@@ -545,6 +572,20 @@ mutation sendVerifyChangeEmail($token: String!, $email: String!, $callbackUrl: S
|
||||
}`,
|
||||
};
|
||||
|
||||
export const serverConfigQuery = {
|
||||
id: 'serverConfigQuery' as const,
|
||||
operationName: 'serverConfig',
|
||||
definitionName: 'serverConfig',
|
||||
containsFile: false,
|
||||
query: `
|
||||
query serverConfig {
|
||||
serverConfig {
|
||||
version
|
||||
flavor
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const setWorkspacePublicByIdMutation = {
|
||||
id: 'setWorkspacePublicByIdMutation' as const,
|
||||
operationName: 'setWorkspacePublicById',
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation recoverDoc(
|
||||
$workspaceId: String!
|
||||
$docId: String!
|
||||
$timestamp: DateTime!
|
||||
) {
|
||||
recoverDoc(workspaceId: $workspaceId, guid: $docId, timestamp: $timestamp)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
query serverConfig {
|
||||
serverConfig {
|
||||
version
|
||||
flavor
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ export enum SubscriptionPlan {
|
||||
Enterprise = 'Enterprise',
|
||||
Free = 'Free',
|
||||
Pro = 'Pro',
|
||||
SelfHosted = 'SelfHosted',
|
||||
Team = 'Team',
|
||||
}
|
||||
|
||||
@@ -372,6 +373,25 @@ export type GetWorkspacesQuery = {
|
||||
workspaces: Array<{ __typename?: 'WorkspaceType'; id: string }>;
|
||||
};
|
||||
|
||||
export type ListHistoryQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
pageDocId: Scalars['String']['input'];
|
||||
take: InputMaybe<Scalars['Int']['input']>;
|
||||
before: InputMaybe<Scalars['DateTime']['input']>;
|
||||
}>;
|
||||
|
||||
export type ListHistoryQuery = {
|
||||
__typename?: 'Query';
|
||||
workspace: {
|
||||
__typename?: 'WorkspaceType';
|
||||
histories: Array<{
|
||||
__typename?: 'DocHistoryType';
|
||||
id: string;
|
||||
timestamp: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetInvoicesCountQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type GetInvoicesCountQuery = {
|
||||
@@ -444,6 +464,17 @@ export type PublishPageMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type RecoverDocMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
docId: Scalars['String']['input'];
|
||||
timestamp: Scalars['DateTime']['input'];
|
||||
}>;
|
||||
|
||||
export type RecoverDocMutation = {
|
||||
__typename?: 'Mutation';
|
||||
recoverDoc: string;
|
||||
};
|
||||
|
||||
export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type RemoveAvatarMutation = {
|
||||
@@ -533,6 +564,17 @@ export type SendVerifyChangeEmailMutation = {
|
||||
sendVerifyChangeEmail: boolean;
|
||||
};
|
||||
|
||||
export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type ServerConfigQuery = {
|
||||
__typename?: 'Query';
|
||||
serverConfig: {
|
||||
__typename?: 'ServerConfigType';
|
||||
version: string;
|
||||
flavor: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type SetWorkspacePublicByIdMutationVariables = Exact<{
|
||||
id: Scalars['ID']['input'];
|
||||
public: Scalars['Boolean']['input'];
|
||||
@@ -717,6 +759,11 @@ export type Queries =
|
||||
variables: GetWorkspacesQueryVariables;
|
||||
response: GetWorkspacesQuery;
|
||||
}
|
||||
| {
|
||||
name: 'listHistoryQuery';
|
||||
variables: ListHistoryQueryVariables;
|
||||
response: ListHistoryQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getInvoicesCountQuery';
|
||||
variables: GetInvoicesCountQueryVariables;
|
||||
@@ -732,6 +779,11 @@ export type Queries =
|
||||
variables: PricesQueryVariables;
|
||||
response: PricesQuery;
|
||||
}
|
||||
| {
|
||||
name: 'serverConfigQuery';
|
||||
variables: ServerConfigQueryVariables;
|
||||
response: ServerConfigQuery;
|
||||
}
|
||||
| {
|
||||
name: 'subscriptionQuery';
|
||||
variables: SubscriptionQueryVariables;
|
||||
@@ -799,6 +851,11 @@ export type Mutations =
|
||||
variables: PublishPageMutationVariables;
|
||||
response: PublishPageMutation;
|
||||
}
|
||||
| {
|
||||
name: 'recoverDocMutation';
|
||||
variables: RecoverDocMutationVariables;
|
||||
response: RecoverDocMutation;
|
||||
}
|
||||
| {
|
||||
name: 'removeAvatarMutation';
|
||||
variables: RemoveAvatarMutationVariables;
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
"devDependencies": {
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/image-blob-reduce": "^4.1.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
"com.affine.cmdk.affine.switch-state.on": "ON",
|
||||
"com.affine.cmdk.affine.translucent-ui-on-the-sidebar.to": "Change Translucent UI On The Sidebar to",
|
||||
"com.affine.cmdk.affine.whats-new": "What's New",
|
||||
"com.affine.cmdk.affine.editor.reveal-page-history-modal": "Reveal Page History Modal",
|
||||
"com.affine.cmdk.placeholder": "Type a command or search anything...",
|
||||
"com.affine.collection-bar.action.tooltip.delete": "Delete",
|
||||
"com.affine.collection-bar.action.tooltip.edit": "Edit",
|
||||
@@ -972,5 +973,13 @@
|
||||
"system": "System",
|
||||
"upgradeBrowser": "Please upgrade to the latest version of Chrome for the best experience.",
|
||||
"will be moved to Trash": "{{title}} will be moved to Trash",
|
||||
"will delete member": "will delete member"
|
||||
"will delete member": "will delete member",
|
||||
"com.affine.history.version-history": "Version History",
|
||||
"com.affine.history.view-history-version": "View History Version",
|
||||
"com.affine.history.restore-current-version": "Restore current version",
|
||||
"com.affine.history.back-to-page": "Back to page",
|
||||
"com.affine.history.empty-prompt.title": "Empty",
|
||||
"com.affine.history.empty-prompt.description": "This document is such a spring chicken, it hasn't sprouted a single historical sprig yet!",
|
||||
"com.affine.history.confirm-restore-modal.restore": "Restore",
|
||||
"com.affine.history.confirm-restore-modal.hint": "You are about to restore the current version of the page to the latest version available. This action will overwrite any changes made prior to the latest version."
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ rand = "0.8"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
sqlx = { version = "0.7.2", default-features = false, features = [
|
||||
sqlx = { version = "0.7.3", default-features = false, features = [
|
||||
"sqlite",
|
||||
"migrate",
|
||||
"runtime-tokio",
|
||||
@@ -44,7 +44,7 @@ uuid = { version = "1", default-features = false, features = [
|
||||
affine_schema = { path = "./schema" }
|
||||
dotenv = "0.15"
|
||||
napi-build = "2"
|
||||
sqlx = { version = "0.7.2", default-features = false, features = [
|
||||
sqlx = { version = "0.7.3", default-features = false, features = [
|
||||
"sqlite",
|
||||
"runtime-tokio",
|
||||
"tls-rustls",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"exports": {
|
||||
"./atom": "./src/atom.ts",
|
||||
"./manager": "./src/manager/index.ts",
|
||||
"./blob": "./src/blob/index.ts",
|
||||
"./local/crud": "./src/local/crud.ts",
|
||||
"./affine/*": "./src/affine/*.ts",
|
||||
"./providers": "./src/providers/index.ts"
|
||||
|
||||
@@ -5,12 +5,15 @@ import type {
|
||||
QueryOptions,
|
||||
QueryResponse,
|
||||
QueryVariables,
|
||||
RecursiveMaybeFields,
|
||||
} from '@affine/graphql';
|
||||
import { gqlFetcherFactory } from '@affine/graphql';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import type { GraphQLError } from 'graphql';
|
||||
import { useMemo } from 'react';
|
||||
import type { Key, SWRConfiguration, SWRResponse } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import type {
|
||||
SWRMutationConfiguration,
|
||||
SWRMutationResponse,
|
||||
@@ -86,6 +89,63 @@ export function useQuery<Query extends GraphQLQuery>(
|
||||
);
|
||||
}
|
||||
|
||||
export function useQueryInfinite<Query extends GraphQLQuery>(
|
||||
options: Omit<QueryOptions<Query>, 'variables'> & {
|
||||
getVariables: (
|
||||
pageIndex: number,
|
||||
previousPageData: QueryResponse<Query>
|
||||
) => QueryOptions<Query>['variables'];
|
||||
},
|
||||
config?: Omit<
|
||||
SWRConfiguration<
|
||||
QueryResponse<Query>,
|
||||
GraphQLError | GraphQLError[],
|
||||
typeof fetcher<Query>
|
||||
>,
|
||||
'fetcher'
|
||||
>
|
||||
) {
|
||||
const configWithSuspense: SWRConfiguration = useMemo(
|
||||
() => ({
|
||||
suspense: true,
|
||||
...config,
|
||||
}),
|
||||
[config]
|
||||
);
|
||||
|
||||
const { data, setSize, size, error } = useSWRInfinite<
|
||||
QueryResponse<Query>,
|
||||
GraphQLError | GraphQLError[]
|
||||
>(
|
||||
(pageIndex: number, previousPageData: QueryResponse<Query>) => [
|
||||
'cloud',
|
||||
options.query.id,
|
||||
options.getVariables(pageIndex, previousPageData),
|
||||
],
|
||||
async ([_, __, variables]) => {
|
||||
const params = { ...options, variables } as QueryOptions<Query>;
|
||||
return fetcher(params);
|
||||
},
|
||||
configWithSuspense
|
||||
);
|
||||
|
||||
const loadingMore = size > 0 && data && !data[size - 1];
|
||||
|
||||
// todo: find a generic way to know whether or not there are more items to load
|
||||
const loadMore = useAsyncCallback(async () => {
|
||||
if (loadingMore) {
|
||||
return;
|
||||
}
|
||||
await setSize(size => size + 1);
|
||||
}, [loadingMore, setSize]);
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loadingMore,
|
||||
loadMore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A useSWRMutation wrapper for sending graphql mutations
|
||||
*
|
||||
@@ -138,3 +198,32 @@ export function useMutation(
|
||||
}
|
||||
|
||||
export const gql = fetcher;
|
||||
|
||||
// use this to revalidate all queries that match the filter
|
||||
export const useMutateQueryResource = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const revalidateResource = useMemo(
|
||||
() =>
|
||||
<Q extends GraphQLQuery>(
|
||||
query: Q,
|
||||
varsFilter: (
|
||||
vars: RecursiveMaybeFields<QueryVariables<Q>>
|
||||
) => boolean = _vars => true
|
||||
) => {
|
||||
return mutate(key => {
|
||||
const res =
|
||||
Array.isArray(key) &&
|
||||
key[0] === 'cloud' &&
|
||||
key[1] === query.id &&
|
||||
varsFilter(key[2]);
|
||||
if (res) {
|
||||
console.debug('revalidate resource', key);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return revalidateResource;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user