Compare commits

...

13 Commits

Author SHA1 Message Date
EYHN
cccea3ec3e fix(workspace): avoid data loss (hot-fix) (#6114) 2024-03-14 13:14:03 +08:00
EYHN
39476d16ef fix(workspace): data loss (hot-fix) (#6110) 2024-03-14 11:29:50 +08:00
DarkSky
49bead719d feat: add cloud logger sa integrate (#6089) 2024-03-12 23:26:09 +08:00
DarkSky
a9f46ed088 fi: ci container publish 2024-03-12 18:22:38 +08:00
forehalo
d3a544f4aa ci: only enable jwst codec in canary 2024-03-12 17:55:01 +08:00
EYHN
cd56d8a6e6 feat(core): new onboarding template (hotfix) (#5952) 2024-02-29 14:01:31 +08:00
liuyi
6ed5ec36bb fix(server): sender passed to nextauth is never used (#5938) 2024-02-28 14:57:23 +08:00
liuyi
0fa917aabb ci: fix selfhost (#5920)
enhancement

___

- Introduced a new ESM module resolution setup using `ts-node` to enhance the development and deployment process.
- Implemented a dynamic loader script registration mechanism to facilitate ESM module loading.
- Simplified the predeploy script execution by refining environment variable handling and stdout configuration.
- Updated `package.json` to reflect changes in script commands for better ESM support and added necessary dependencies for `ts-node` and `typescript`.

___

<table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>loader.js</strong><dd><code>Introduce ESM Module Resolution via ts-node</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/backend/server/scripts/loader.js

<li>Introduced <code>ts-node</code> configuration for ESM module resolution.<br> <li> Exported a <code>resolve</code> function for module resolution.<br>

</details>

  </td>
  <td><a href="https:/toeverything/AFFiNE/pull/5920/files#diff-9ed793897a493633028d510db0742ff38d2d86471c54b17513d4354c51597ef8">+11/-0</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>register.js</strong><dd><code>Implement Dynamic Loader Script Registration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/backend/server/scripts/register.js

<li>Implemented dynamic registration of the loader script.<br> <li> Utilized <code>node:module</code> and <code>node:url</code> for script registration.<br>

</details>

  </td>
  <td><a href="https:/toeverything/AFFiNE/pull/5920/files#diff-64831012a09f2bc4bc5a611ddb8e0871b0e83588de6c5d4f2f5cb1dae8fff244">+4/-0</a>&nbsp; &nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>self-host-predeploy.js</strong><dd><code>Simplify Predeploy Script Execution</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/backend/server/scripts/self-host-predeploy.js

<li>Simplified environment variable passing to <code>execSync</code>.<br> <li> Changed stdout handling to inherit from the parent process.<br>

</details>

  </td>
  <td><a href="https:/toeverything/AFFiNE/pull/5920/files#diff-bd7b0be14c198018c21dadda6945a779c57d13e4c8584ee62da4baa99d370664">+3/-5</a>&nbsp; &nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>package.json</strong><dd><code>Update Scripts and Dependencies for ESM Support</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/backend/server/package.json

<li>Updated script commands for ESM compatibility.<br> <li> Added <code>ts-node</code> and <code>typescript</code> dependencies.<br> <li> Removed redundant <code>--es-module-specifier-resolution=node</code> flags.<br>

</details>

  </td>
  <td><a href="https:/toeverything/AFFiNE/pull/5920/files#diff-a6530c6fe539aaa49ff0a7a80bc4362c1d95c419fdd19125415dcc869b31a443">+6/-6</a>&nbsp; &nbsp; &nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

___

>  **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools and their descriptions
2024-02-28 14:56:37 +08:00
liuyi
eb79e6bdc9 ci: fix canary deployment (#5851) 2024-02-28 12:13:03 +08:00
liuyi
cf52a43773 feat(server): allow customize mailer server (#5835) 2024-02-28 12:12:06 +08:00
LongYinan
9203980a8c build: fix selfhost config 2024-02-27 23:49:25 +08:00
liuyi
d34eb2cbe5 fix(server): apply env overrides after all config merged (#5795) 2024-02-27 21:46:08 +08:00
liuyi
7f3f993ce4 refactor(server): reorganize server configs (#5753) 2024-02-27 21:45:26 +08:00
77 changed files with 2410 additions and 23488 deletions

View File

@@ -15,12 +15,13 @@ const {
R2_SECRET_ACCESS_KEY,
ENABLE_CAPTCHA,
CAPTCHA_TURNSTILE_SECRET,
OAUTH_EMAIL_SENDER,
OAUTH_EMAIL_LOGIN,
OAUTH_EMAIL_PASSWORD,
MAILER_SENDER,
MAILER_USER,
MAILER_PASSWORD,
AFFINE_GOOGLE_CLIENT_ID,
AFFINE_GOOGLE_CLIENT_SECRET,
CLOUD_SQL_IAM_ACCOUNT,
CLOUD_LOGGER_IAM_ACCOUNT,
GCLOUD_CONNECTION_NAME,
GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT,
REDIS_HOST,
@@ -59,7 +60,9 @@ const createHelmCommand = ({ isDryRun }) => {
? [
`--set-json web.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`,
`--set-json graphql.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`,
`--set-json graphql.serviceAccount.annotations=\"{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_LOGGER_IAM_ACCOUNT}\\"}\"`,
`--set-json sync.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`,
`--set-json sync.serviceAccount.annotations=\"{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_LOGGER_IAM_ACCOUNT}\\"}\"`,
`--set-json cloud-sql-proxy.serviceAccount.annotations=\"{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }\"`,
`--set-json cloud-sql-proxy.nodeSelector=\"{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }\"`,
]
@@ -103,15 +106,15 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string graphql.app.objectStorage.r2.accountId="${R2_ACCOUNT_ID}"`,
`--set-string graphql.app.objectStorage.r2.accessKeyId="${R2_ACCESS_KEY_ID}"`,
`--set-string graphql.app.objectStorage.r2.secretAccessKey="${R2_SECRET_ACCESS_KEY}"`,
`--set-string graphql.app.oauth.email.sender="${OAUTH_EMAIL_SENDER}"`,
`--set-string graphql.app.oauth.email.login="${OAUTH_EMAIL_LOGIN}"`,
`--set-string graphql.app.oauth.email.password="${OAUTH_EMAIL_PASSWORD}"`,
`--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`,
`--set-string graphql.app.mailer.user="${MAILER_USER}"`,
`--set-string graphql.app.mailer.password="${MAILER_PASSWORD}"`,
`--set-string graphql.app.oauth.google.enabled=true`,
`--set-string graphql.app.oauth.google.clientId="${AFFINE_GOOGLE_CLIENT_ID}"`,
`--set-string graphql.app.oauth.google.clientSecret="${AFFINE_GOOGLE_CLIENT_SECRET}"`,
`--set-string graphql.app.payment.stripe.apiKey="${STRIPE_API_KEY}"`,
`--set-string graphql.app.payment.stripe.webhookKey="${STRIPE_WEBHOOK_KEY}"`,
`--set graphql.app.experimental.enableJwstCodec=true`,
`--set graphql.app.experimental.enableJwstCodec=${isInternal}`,
`--set graphql.app.features.earlyAccessPreview=false`,
`--set sync.replicaCount=${syncReplicaCount}`,
`--set-string sync.image.tag="${imageTag}"`,

View File

@@ -8,4 +8,4 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends openssl && \
rm -rf /var/lib/apt/lists/*
CMD ["node", "--es-module-specifier-resolution=node", "./dist/index.js"]
CMD ["node", "--import", "./scripts/register.js", "./dist/index.js"]

View File

@@ -1,6 +1,6 @@
services:
affine:
image: ghcr.io/toeverything/affine-graphql:beta
image: ghcr.io/toeverything/affine-graphql:stable
container_name: affine_selfhosted
command:
['sh', '-c', 'node ./scripts/self-host-predeploy && node ./dist/index.js']
@@ -23,13 +23,11 @@ services:
max-size: '1000m'
restart: unless-stopped
environment:
- NODE_OPTIONS=--es-module-specifier-resolution node
- NODE_OPTIONS="--import=./scripts/register.js"
- AFFINE_CONFIG_PATH=/root/.affine/config
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgres://affine:affine@postgres:5432/affine
- DISABLE_TELEMETRY=true
- NODE_ENV=production
- SERVER_FLAVOR=selfhosted
- AFFINE_ADMIN_EMAIL=${AFFINE_ADMIN_EMAIL}
- AFFINE_ADMIN_PASSWORD=${AFFINE_ADMIN_PASSWORD}
redis:

View File

@@ -39,6 +39,8 @@ spec:
value: "--max-old-space-size=4096"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "affine"
- name: SERVER_FLAVOR
value: "graphql"
- name: AFFINE_ENV
@@ -81,31 +83,33 @@ spec:
value: "{{ .Values.app.captcha.enabled }}"
- name: FEATURES_EARLY_ACCESS_PREVIEW
value: "{{ .Values.app.features.earlyAccessPreview }}"
- name: OAUTH_EMAIL_SENDER
- name: FEATURES_SYNC_CLIENT_VERSION_CHECK
value: "{{ .Values.app.features.syncClientVersionCheck }}"
- name: MAILER_HOST
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
key: sender
- name: OAUTH_EMAIL_LOGIN
name: "{{ .Values.app.mailer.secretName }}"
key: host
- name: MAILER_PORT
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
key: login
- name: OAUTH_EMAIL_SERVER
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
key: server
- name: OAUTH_EMAIL_PORT
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
name: "{{ .Values.app.mailer.secretName }}"
key: port
- name: OAUTH_EMAIL_PASSWORD
- name: MAILER_USER
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
name: "{{ .Values.app.mailer.secretName }}"
key: user
- name: MAILER_PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Values.app.mailer.secretName }}"
key: password
- name: MAILER_SENDER
valueFrom:
secretKeyRef:
name: "{{ .Values.app.mailer.secretName }}"
key: sender
- name: STRIPE_API_KEY
valueFrom:
secretKeyRef:

View File

@@ -0,0 +1,13 @@
{{- if .Values.app.mailer.secretName -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.mailer.secretName }}"
type: Opaque
data:
host: "{{ .Values.app.mailer.host | b64enc }}"
port: "{{ .Values.app.mailer.port | b64enc }}"
user: "{{ .Values.app.mailer.user | b64enc }}"
password: "{{ .Values.app.mailer.password | b64enc }}"
sender: "{{ .Values.app.mailer.sender | b64enc }}"
{{- end }}

View File

@@ -1,15 +1,3 @@
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.oauth.email.secretName }}"
type: Opaque
data:
sender: "{{ .Values.app.oauth.email.sender | b64enc }}"
login: "{{ .Values.app.oauth.email.login | b64enc }}"
password: "{{ .Values.app.oauth.email.password | b64enc }}"
server: "{{ .Values.app.oauth.email.server | b64enc }}"
port: "{{ .Values.app.oauth.email.port | b64enc }}"
---
{{- if .Values.app.oauth.google.enabled -}}
apiVersion: v1
kind: Secret

View File

@@ -35,14 +35,7 @@ app:
accountId: ''
accessKeyId: ''
secretAccessKey: ''
oauth:
email:
secretName: 'oauth-email'
sender: 'noreply@toeverything.info'
login: ''
password: ''
server: 'smtp.gmail.com'
port: '465'
oauth:
google:
enabled: false
secretName: oauth-google
@@ -53,6 +46,13 @@ app:
secretName: oauth-github
clientId: ''
clientSecret: ''
mailer:
secretName: 'mailer'
host: 'smtp.gmail.com'
port: '465'
user: ''
password: ''
sender: 'noreply@toeverything.info'
payment:
stripe:
secretName: 'stripe'

View File

@@ -36,6 +36,8 @@ spec:
value: "{{ .Values.env }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "affine"
- name: SERVER_FLAVOR
value: "sync"
- name: NEXTAUTH_URL

View File

@@ -19,7 +19,7 @@ env:
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
DISABLE_TELEMETRY: true
DEPLOYMENT_TYPE: affine
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -291,6 +291,7 @@ jobs:
runs-on: ubuntu-latest
needs: build-storage
env:
NODE_ENV: test
DISTRIBUTION: browser
services:
postgres:
@@ -447,7 +448,6 @@ jobs:
${{ matrix.tests.script }}
env:
DEV_SERVER_URL: http://localhost:8080
ENABLE_LOCAL_EMAIL: true
- name: Upload test results
if: ${{ failure() }}

View File

@@ -136,6 +136,10 @@ jobs:
build-docker:
name: Build Docker
runs-on: ubuntu-latest
permissions:
contents: 'write'
id-token: 'write'
packages: 'write'
needs:
- build-server
- build-core
@@ -275,9 +279,9 @@ jobs:
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
ENABLE_CAPTCHA: true
CAPTCHA_TURNSTILE_SECRET: ${{ secrets.CAPTCHA_TURNSTILE_SECRET }}
OAUTH_EMAIL_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }}
OAUTH_EMAIL_LOGIN: ${{ secrets.OAUTH_EMAIL_LOGIN }}
OAUTH_EMAIL_PASSWORD: ${{ secrets.OAUTH_EMAIL_PASSWORD }}
MAILER_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }}
MAILER_USER: ${{ secrets.OAUTH_EMAIL_LOGIN }}
MAILER_PASSWORD: ${{ secrets.OAUTH_EMAIL_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
@@ -290,6 +294,7 @@ jobs:
REDIS_HOST: ${{ secrets.REDIS_HOST }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }}
CLOUD_LOGGER_IAM_ACCOUNT: ${{ secrets.CLOUD_LOGGER_IAM_ACCOUNT }}
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
STRIPE_WEBHOOK_KEY: ${{ secrets.STRIPE_WEBHOOK_KEY }}
STATIC_IP_NAME: ${{ secrets.STATIC_IP_NAME }}

View File

@@ -59,9 +59,9 @@ You may need additional env for auth login. You may want to put your own one if
For email login & password, please refer to https://nodemailer.com/usage/using-gmail/
```
OAUTH_EMAIL_SENDER=
OAUTH_EMAIL_LOGIN=
OAUTH_EMAIL_PASSWORD=
MAILER_SENDER=
MAILER_USER=
MAILER_PASSWORD=
OAUTH_GOOGLE_ENABLED="true"
OAUTH_GOOGLE_CLIENT_ID=
OAUTH_GOOGLE_CLIENT_SECRET=

View File

@@ -9,13 +9,13 @@
},
"scripts": {
"build": "tsc",
"start": "node --loader ts-node/esm/transpile-only.mjs --es-module-specifier-resolution node ./src/index.ts",
"start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts",
"dev": "nodemon ./src/index.ts",
"test": "ava --concurrency 1 --serial",
"test:coverage": "c8 ava --concurrency 1 --serial",
"postinstall": "prisma generate",
"data-migration": "node --loader ts-node/esm/transpile-only.mjs --es-module-specifier-resolution node ./src/data/index.ts",
"predeploy": "yarn prisma migrate deploy && node --es-module-specifier-resolution node ./dist/data/index.js run"
"data-migration": "node --loader ts-node/esm/transpile-only.mjs ./src/data/index.ts",
"predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run"
},
"dependencies": {
"@apollo/server": "^4.9.5",
@@ -86,6 +86,8 @@
"semver": "^7.5.4",
"socket.io": "^4.7.2",
"stripe": "^14.5.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"ws": "^8.14.2",
"yjs": "^13.6.10",
"zod": "^3.22.4"
@@ -112,9 +114,7 @@
"c8": "^9.0.0",
"nodemon": "^3.0.1",
"sinon": "^17.0.1",
"supertest": "^6.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.3.2"
"supertest": "^6.3.3"
},
"ava": {
"timeout": "1m",
@@ -139,10 +139,11 @@
"environmentVariables": {
"TS_NODE_PROJECT": "./tests/tsconfig.json",
"NODE_ENV": "test",
"ENABLE_LOCAL_EMAIL": "true",
"OAUTH_EMAIL_LOGIN": "noreply@toeverything.info",
"OAUTH_EMAIL_PASSWORD": "affine",
"OAUTH_EMAIL_SENDER": "noreply@toeverything.info",
"MAILER_HOST": "0.0.0.0",
"MAILER_PORT": "1025",
"MAILER_USER": "noreply@toeverything.info",
"MAILER_PASSWORD": "affine",
"MAILER_SENDER": "noreply@toeverything.info",
"FEATURES_EARLY_ACCESS_PREVIEW": "false"
}
},
@@ -162,7 +163,6 @@
"env": {
"TS_NODE_TRANSPILE_ONLY": true,
"TS_NODE_PROJECT": "./tsconfig.json",
"NODE_ENV": "development",
"DEBUG": "affine:*",
"FORCE_COLOR": true,
"DEBUG_COLORS": true

View File

@@ -0,0 +1,11 @@
import { create, createEsmHooks } from 'ts-node';
const service = create({
experimentalSpecifierResolution: 'node',
transpileOnly: true,
logError: true,
skipProject: true,
});
const hooks = createEsmHooks(service);
export const resolve = hooks.resolve;

View File

@@ -0,0 +1,4 @@
import { register } from 'node:module';
import { pathToFileURL } from 'node:url';
register('./scripts/loader.js', pathToFileURL('./'));

View File

@@ -13,7 +13,10 @@ const configFiles = [
];
function configCleaner(content) {
return content.replace(/(\/\/#.*$)|(\/\/\s+TODO.*$)/gm, '');
return content.replace(
/(^\/\/#.*$)|(^\/\/\s+TODO.*$)|("use\sstrict";?)|(^.*eslint-disable.*$)/gm,
''
);
}
function prepare() {
@@ -39,11 +42,9 @@ function prepare() {
function runPredeployScript() {
console.log('running predeploy script.');
execSync('yarn predeploy', {
env: {
...process.env,
NODE_OPTIONS:
(process.env.NODE_OPTIONS ?? '') + ' --import ./dist/prelude.js',
},
encoding: 'utf-8',
env: process.env,
stdio: 'inherit',
});
}

View File

@@ -11,6 +11,7 @@ export class AppController {
return {
compatibility: this.config.version,
message: `AFFiNE ${this.config.version} Server`,
type: this.config.type,
flavor: this.config.flavor,
};
}

View File

@@ -109,7 +109,7 @@ export class AppModuleBuilder {
},
],
imports: this.modules,
controllers: this.config.flavor.selfhosted ? [] : [AppController],
controllers: this.config.isSelfhosted ? [] : [AppController],
})
class AppModule {}
@@ -132,9 +132,9 @@ function buildAppModule() {
// sync server only
.useIf(config => config.flavor.sync, SyncModule)
// main server only
// graphql server only
.useIf(
config => config.flavor.main,
config => config.flavor.graphql,
ServerConfigModule,
WebSocketModule,
GqlModule,
@@ -147,7 +147,7 @@ function buildAppModule() {
// self hosted server only
.useIf(
config => config.flavor.selfhosted,
config => config.isSelfhosted,
ServeStaticModule.forRoot({
rootPath: join('/app', 'static'),
})

View File

@@ -3,8 +3,7 @@ AFFiNE.ENV_MAP = {
AFFINE_SERVER_PORT: ['port', 'int'],
AFFINE_SERVER_HOST: 'host',
AFFINE_SERVER_SUB_PATH: 'path',
AFFIHE_SERVER_HTTPS: ['https', 'boolean'],
AFFINE_ENV: 'affineEnv',
AFFINE_SERVER_HTTPS: ['https', 'boolean'],
DATABASE_URL: 'db.url',
ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'],
CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'],
@@ -14,11 +13,12 @@ AFFiNE.ENV_MAP = {
OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'],
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
OAUTH_EMAIL_LOGIN: 'auth.email.login',
OAUTH_EMAIL_SENDER: 'auth.email.sender',
OAUTH_EMAIL_SERVER: 'auth.email.server',
OAUTH_EMAIL_PORT: ['auth.email.port', 'int'],
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
MAILER_HOST: 'mailer.host',
MAILER_PORT: ['mailer.port', 'int'],
MAILER_USER: 'mailer.auth.user',
MAILER_PASSWORD: 'mailer.auth.pass',
MAILER_SENDER: 'mailer.from.address',
MAILER_SECURE: ['mailer.secure', 'boolean'],
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
REDIS_SERVER_HOST: 'plugins.redis.host',
@@ -28,13 +28,10 @@ AFFiNE.ENV_MAP = {
REDIS_SERVER_DATABASE: ['plugins.redis.db', 'int'],
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
DOC_MERGE_USE_JWST_CODEC: [
'doc.manager.experimentalMergeWithJwstCodec',
'doc.manager.experimentalMergeWithYOcto',
'boolean',
],
ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
STRIPE_API_KEY: 'plugins.payment.stripe.keys.APIKey',
STRIPE_WEBHOOK_KEY: 'plugins.payment.stripe.keys.webhookKey',
FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'],
};
export default AFFiNE;

View File

@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// Custom configurations for AFFiNE Cloud
// ====================================================================================
// Q: WHY THIS FILE EXISTS?
// A: AFFiNE deployment environment may have a lot of custom environment variables,
// which are not suitable to be put in the `affine.ts` file.
// For example, AFFiNE Cloud Clusters are deployed on Google Cloud Platform.
// We need to enable the `gcloud` plugin to make sure the nodes working well,
// but the default selfhost version may not require it.
// So it's not a good idea to put such logic in the common `affine.ts` file.
//
// ```
// if (AFFiNE.deploy) {
// AFFiNE.plugins.use('gcloud');
// }
// ```
// ====================================================================================
const env = process.env;
AFFiNE.metrics.enabled = !AFFiNE.node.test;
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.storage.providers.r2 = {
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
credentials: {
accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!,
},
};
AFFiNE.storage.storages.avatar.provider = 'r2';
AFFiNE.storage.storages.avatar.bucket = 'account-avatar';
AFFiNE.storage.storages.avatar.publicLinkFactory = key =>
`https://avatar.affineassets.com/${key}`;
AFFiNE.storage.storages.blob.provider = 'r2';
AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${
AFFiNE.affine.canary ? 'canary' : 'prod'
}`;
}
AFFiNE.plugins.use('redis');
AFFiNE.plugins.use('payment');
if (AFFiNE.deploy) {
AFFiNE.mailer = {
service: 'gmail',
auth: {
user: env.MAILER_USER,
pass: env.MAILER_PASSWORD,
},
};
AFFiNE.plugins.use('gcloud');
}

View File

@@ -1,39 +1,94 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// Custom configurations
const env = process.env;
// TODO(@forehalo): detail explained
// Storage
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.storage.providers.r2 = {
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
credentials: {
accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!,
},
};
AFFiNE.storage.storages.avatar.provider = 'r2';
AFFiNE.storage.storages.avatar.bucket = 'account-avatar';
AFFiNE.storage.storages.avatar.publicLinkFactory = key =>
`https://avatar.affineassets.com/${key}`;
AFFiNE.storage.storages.blob.provider = 'r2';
AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${
AFFiNE.affine.canary ? 'canary' : 'prod'
}`;
}
// Metrics
AFFiNE.metrics.enabled = true;
// Plugins Section Start
AFFiNE.plugins.use('payment', {
stripe: {
keys: {},
apiVersion: '2023-10-16',
},
//
// ###############################################################
// ## AFFiNE Configuration System ##
// ###############################################################
// Here is the file of all AFFiNE configurations that will affect runtime behavior.
// Override any configuration here and it will be merged when starting the server.
// Any changes in this file won't take effect before server restarted.
//
//
// > Configurations merge order
// 1. load environment variables (`.env` if provided, and from system)
// 2. load `src/fundamentals/config/default.ts` for all default settings
// 3. apply `./affine.ts` patches (this file)
// 4. apply `./affine.env.ts` patches
//
//
// ###############################################################
// ## General settings ##
// ###############################################################
//
// /* The unique identity of the server */
// AFFiNE.serverId = 'some-randome-uuid';
//
// /* The name of AFFiNE Server, may show on the UI */
// AFFiNE.serverName = 'Your Cool AFFiNE Selfhosted Cloud';
//
// /* Whether the server is deployed behind a HTTPS proxied environment */
AFFiNE.https = false;
// /* Domain of your server that your server will be available at */
AFFiNE.host = 'localhost';
// /* The local port of your server that will listen on */
AFFiNE.port = 3010;
// /* The sub path of your server */
// /* For example, if you set `AFFiNE.path = '/affine'`, then the server will be available at `${domain}/affine` */
// AFFiNE.path = '/affine';
//
//
// ###############################################################
// ## Database settings ##
// ###############################################################
//
// /* The URL of the database where most of AFFiNE server data will be stored in */
// AFFiNE.db.url = 'postgres://user:passsword@localhost:5432/affine';
//
//
// ###############################################################
// ## Server Function settings ##
// ###############################################################
//
// /* Whether enable metrics and tracing while running the server */
// /* The metrics will be available at `http://localhost:9464/metrics` with [Prometheus] format exported */
// AFFiNE.metrics.enabled = true;
//
// /* GraphQL configurations that control the behavior of the Apollo Server behind */
// /* @see https://www.apollographql.com/docs/apollo-server/api/apollo-server */
// AFFiNE.graphql = {
// /* Path to mount GraphQL API */
// path: '/graphql',
// buildSchemaOptions: {
// numberScalarMode: 'integer',
// },
// /* Whether allow client to query the schema introspection */
// introspection: true,
// /* Whether enable GraphQL Playground UI */
// playground: true,
// }
//
// /* Doc Store & Collaberation */
// /* How long the buffer time of creating a new history snapshot when doc get updated */
// AFFiNE.doc.history.interval = 1000 * 60 * 10; // 10 minutes
//
// /* Use `y-octo` to merge updates at the same time when merging using Yjs */
// AFFiNE.doc.manager.experimentalMergeWithYOcto = true;
//
// /* How often the manager will start a new turn of merging pending updates into doc snapshot */
// AFFiNE.doc.manager.updatePollInterval = 1000 * 3;
//
//
// ###############################################################
// ## Plugins settings ##
// ###############################################################
//
// /* Redis Plugin */
// /* Provide caching and session storing backed by Redis. */
// /* Useful when you deploy AFFiNE server in a cluster. */
AFFiNE.plugins.use('redis', {
/* override options */
});
AFFiNE.plugins.use('redis');
// Plugins Section end
export default AFFiNE;
// /* Payment Plugin */
AFFiNE.plugins.use('payment', {
stripe: { keys: {}, apiVersion: '2023-10-16' },
});
//

View File

@@ -115,27 +115,9 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
};
const nextAuthOptions: NextAuthOptions = {
providers: [
// @ts-expect-error esm interop issue
Email.default({
server: {
host: config.auth.email.server,
port: config.auth.email.port,
auth: {
user: config.auth.email.login,
pass: config.auth.email.password,
},
},
from: config.auth.email.sender,
sendVerificationRequest: (params: SendVerificationRequestParams) =>
sendVerificationRequest(config, logger, mailer, session, params),
}),
],
providers: [],
adapter: prismaAdapter,
debug: !config.node.prod,
session: {
strategy: 'database',
},
logger: {
debug(code, metadata) {
logger.debug(`${code}: ${JSON.stringify(metadata)}`);
@@ -188,6 +170,16 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
})
);
if (config.mailer && mailer) {
nextAuthOptions.providers.push(
// @ts-expect-error esm interop issue
Email.default({
sendVerificationRequest: (params: SendVerificationRequestParams) =>
sendVerificationRequest(config, logger, mailer, session, params),
})
);
}
if (config.auth.oauthProviders.github) {
nextAuthOptions.providers.push(
// @ts-expect-error esm interop issue
@@ -214,6 +206,11 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
);
}
if (nextAuthOptions.providers.length > 1) {
// not only credentials provider
nextAuthOptions.session = { strategy: 'database' };
}
nextAuthOptions.jwt = {
encode: async ({ token, maxAge }) =>
encode(config, prisma, token, maxAge),

View File

@@ -136,7 +136,7 @@ export class AuthService {
return (
!!outcome.success &&
// skip hostname check in dev mode
(this.config.affineEnv === 'dev' || outcome.hostname === this.config.host)
(this.config.node.dev || outcome.hostname === this.config.host)
);
}

View File

@@ -11,7 +11,7 @@ export async function sendVerificationRequest(
session: SessionService,
params: SendVerificationRequestParams
) {
const { identifier, url, provider } = params;
const { identifier, url } = params;
const urlWithToken = new URL(url);
const callbackUrl = urlWithToken.searchParams.get('callbackUrl') || '';
if (!callbackUrl) {
@@ -28,7 +28,6 @@ export async function sendVerificationRequest(
const result = await mailer.sendSignInEmail(urlWithToken.toString(), {
to: identifier,
from: provider.from,
});
logger.log(`send verification email success: ${result.accepted.join(', ')}`);

View File

@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common';
import { Field, ObjectType, Query, registerEnumType } from '@nestjs/graphql';
import { DeploymentType } from '../fundamentals';
export enum ServerFeature {
Payment = 'payment',
}
@@ -9,6 +11,10 @@ registerEnumType(ServerFeature, {
name: 'ServerFeature',
});
registerEnumType(DeploymentType, {
name: 'ServerDeploymentType',
});
const ENABLED_FEATURES: ServerFeature[] = [];
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
ENABLED_FEATURES.push(feature);
@@ -28,6 +34,9 @@ export class ServerConfigType {
@Field({ description: 'server base url' })
baseUrl!: string;
@Field(() => DeploymentType, { description: 'server type' })
type!: DeploymentType;
/**
* @deprecated
*/
@@ -46,7 +55,11 @@ export class ServerConfigResolver {
name: AFFiNE.serverName,
version: AFFiNE.version,
baseUrl: AFFiNE.baseUrl,
flavor: AFFiNE.flavor.type,
type: AFFiNE.type,
// BACKWARD COMPATIBILITY
// the old flavors contains `selfhosted` but it actually not flavor but deployment type
// this field should be removed after frontend feature flags implemented
flavor: AFFiNE.type,
features: ENABLED_FEATURES,
};
}

View File

@@ -125,11 +125,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
const doc = await this.recoverDoc(...updates);
// test jwst codec
if (
this.config.affine.canary &&
this.config.doc.manager.experimentalMergeWithJwstCodec &&
updates.length < 100 /* avoid overloading */
) {
if (this.config.doc.manager.experimentalMergeWithYOcto) {
metrics.jwst.counter('codec_merge_counter').add(1);
const yjsResult = Buffer.from(encodeStateAsUpdate(doc));
let log = false;
@@ -180,7 +176,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
}, this.config.doc.manager.updatePollInterval);
this.logger.log('Automation started');
if (this.config.doc.manager.experimentalMergeWithJwstCodec) {
if (this.config.doc.manager.experimentalMergeWithYOcto) {
this.logger.warn(
'Experimental feature enabled: merge updates with jwst codec is enabled'
);
@@ -386,7 +382,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
// take it ease, we don't want to overload db and or cpu
// if we limit the taken number here,
// user will never see the latest doc if there are too many updates pending to be merged.
take: 100,
take: this.config.doc.manager.maxUpdatesPullCount,
});
// perf(memory): avoid sorting in db

View File

@@ -56,7 +56,7 @@ export class WorkspacesController {
this.logger.warn(`Blob ${workspaceId}/${name} has no metadata`);
}
res.setHeader('cache-control', 'public, max-age=31536000, immutable');
res.setHeader('cache-control', 'public, max-age=2592000, immutable');
body.pipe(res);
}
@@ -106,6 +106,7 @@ export class WorkspacesController {
}
res.setHeader('content-type', 'application/octet-stream');
res.setHeader('cache-control', 'no-cache');
res.send(update);
}
@@ -142,6 +143,7 @@ export class WorkspacesController {
if (history) {
res.setHeader('content-type', 'application/octet-stream');
res.setHeader('cache-control', 'public, max-age=2592000, immutable');
res.send(history.blob);
} else {
throw new NotFoundException('Doc history not found');

View File

@@ -8,7 +8,7 @@ export class SelfHostAdmin1605053000403 {
// do the migration
static async up(db: PrismaClient, ref: ModuleRef) {
const config = ref.get(Config, { strict: false });
if (config.flavor.selfhosted) {
if (config.isSelfhosted) {
if (
!process.env.AFFINE_ADMIN_EMAIL ||
!process.env.AFFINE_ADMIN_PASSWORD

View File

@@ -1,4 +1,5 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import type { LeafPaths } from '../utils/types';
import { EnvConfigType } from './env';
@@ -18,18 +19,22 @@ export enum ExternalAccount {
firebase = 'firebase',
}
export type ServerFlavor =
| 'allinone'
| 'main'
// @deprecated
| 'graphql'
| 'sync'
| 'selfhosted';
export type ServerFlavor = 'allinone' | 'graphql' | 'sync';
export type AFFINE_ENV = 'dev' | 'beta' | 'production';
export type NODE_ENV = 'development' | 'test' | 'production';
export enum DeploymentType {
Affine = 'affine',
Selfhosted = 'selfhosted',
}
export type ConfigPaths = LeafPaths<
Omit<
AFFiNEConfig,
| 'ENV_MAP'
| 'version'
| 'type'
| 'isSelfhosted'
| 'flavor'
| 'env'
| 'affine'
@@ -63,27 +68,36 @@ export interface AFFiNEConfig {
*/
readonly version: string;
/**
* Deployment type, AFFiNE Cloud, or Selfhosted
*/
get type(): DeploymentType;
/**
* Fast detect whether currently deployed in a selfhosted environment
*/
get isSelfhosted(): boolean;
/**
* Server flavor
*/
get flavor(): {
type: string;
main: boolean;
graphql: boolean;
sync: boolean;
selfhosted: boolean;
};
/**
* Deployment environment
*/
readonly affineEnv: 'dev' | 'beta' | 'production';
readonly AFFINE_ENV: AFFINE_ENV;
/**
* alias to `process.env.NODE_ENV`
*
* @default 'production'
* @default 'development'
* @env NODE_ENV
*/
readonly env: string;
readonly NODE_ENV: NODE_ENV;
/**
* fast AFFiNE environment judge
@@ -101,6 +115,7 @@ export interface AFFiNEConfig {
dev: boolean;
test: boolean;
};
get deploy(): boolean;
/**
@@ -249,18 +264,6 @@ export interface AFFiNEConfig {
}
>
>;
/**
* whether to use local email service to send email
* local debug only
*/
localEmail: boolean;
email: {
server: string;
port: number;
login: string;
sender: string;
password: string;
};
captcha: {
/**
* whether to enable captcha
@@ -284,6 +287,13 @@ export interface AFFiNEConfig {
};
};
/**
* Configurations for mail service used to post auth or bussiness mails.
*
* @see https://nodemailer.com/smtp/
*/
mailer?: SMTPTransport.Options;
doc: {
manager: {
/**
@@ -302,11 +312,17 @@ export interface AFFiNEConfig {
updatePollInterval: number;
/**
* Use JwstCodec to merge updates at the same time when merging using Yjs.
* The maximum number of updates that will be pulled from the server at once.
* Existing for avoiding the server to be overloaded when there are too many updates for one doc.
*/
maxUpdatesPullCount: number;
/**
* Use `y-octo` to merge updates at the same time when merging using Yjs.
*
* This is an experimental feature, and aimed to check the correctness of JwstCodec.
*/
experimentalMergeWithJwstCodec: boolean;
experimentalMergeWithYOcto: boolean;
};
history: {
/**

View File

@@ -6,7 +6,14 @@ import { merge } from 'lodash-es';
import parse from 'parse-duration';
import pkg from '../../../package.json' assert { type: 'json' };
import type { AFFiNEConfig, ServerFlavor } from './def';
import {
type AFFINE_ENV,
AFFiNEConfig,
DeploymentType,
type NODE_ENV,
type ServerFlavor,
} from './def';
import { readEnv } from './env';
import { getDefaultAFFiNEStorageConfig } from './storage';
// Don't use this in production
@@ -46,40 +53,62 @@ const jwtKeyPair = (function () {
})();
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
let isHttps: boolean | null = null;
let flavor = (process.env.SERVER_FLAVOR ?? 'allinone') as ServerFlavor;
const NODE_ENV = readEnv<NODE_ENV>('NODE_ENV', 'development', [
'development',
'test',
'production',
]);
const AFFINE_ENV = readEnv<AFFINE_ENV>('AFFINE_ENV', 'dev', [
'dev',
'beta',
'production',
]);
const flavor = readEnv<ServerFlavor>('SERVER_FLAVOR', 'allinone', [
'allinone',
'graphql',
'sync',
]);
const deploymentType = readEnv<DeploymentType>(
'DEPLOYMENT_TYPE',
NODE_ENV === 'development'
? DeploymentType.Affine
: DeploymentType.Selfhosted,
Object.values(DeploymentType)
);
const isSelfhosted = deploymentType === DeploymentType.Selfhosted;
const defaultConfig = {
serverId: 'affine-nestjs-server',
serverName: flavor === 'selfhosted' ? 'Self-Host Cloud' : 'AFFiNE Cloud',
serverName: isSelfhosted ? 'Self-Host Cloud' : 'AFFiNE Cloud',
version: pkg.version,
get type() {
return deploymentType;
},
get isSelfhosted() {
return isSelfhosted;
},
get flavor() {
if (flavor === 'graphql') {
flavor = 'main';
}
return {
type: flavor,
main: flavor === 'main' || flavor === 'allinone',
graphql: flavor === 'graphql' || flavor === 'allinone',
sync: flavor === 'sync' || flavor === 'allinone',
selfhosted: flavor === 'selfhosted',
};
},
ENV_MAP: {},
affineEnv: 'dev',
AFFINE_ENV,
get affine() {
const env = this.affineEnv;
return {
canary: env === 'dev',
beta: env === 'beta',
stable: env === 'production',
canary: AFFINE_ENV === 'dev',
beta: AFFINE_ENV === 'beta',
stable: AFFINE_ENV === 'production',
};
},
env: process.env.NODE_ENV ?? 'development',
NODE_ENV,
get node() {
const env = this.env;
return {
prod: env === 'production',
dev: env === 'development',
test: env === 'test',
prod: NODE_ENV === 'production',
dev: NODE_ENV === 'development',
test: NODE_ENV === 'test',
};
},
get deploy() {
@@ -88,12 +117,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
featureFlags: {
earlyAccessPreview: false,
},
get https() {
return isHttps ?? !this.node.dev;
},
set https(value: boolean) {
isHttps = value;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
@@ -142,14 +166,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
return this.privateKey;
},
oauthProviders: {},
localEmail: false,
email: {
server: 'smtp.gmail.com',
port: 465,
login: '',
sender: '',
password: '',
},
},
storage: getDefaultAFFiNEStorageConfig(),
rateLimiter: {
@@ -160,7 +176,8 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
manager: {
enableUpdateAutoMerging: flavor !== 'sync',
updatePollInterval: 3000,
experimentalMergeWithJwstCodec: false,
maxUpdatesPullCount: 500,
experimentalMergeWithYOcto: false,
},
history: {
interval: 1000 * 60 * 10 /* 10 mins */,

View File

@@ -48,3 +48,24 @@ export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
}
}
}
export function readEnv<T>(
env: string,
defaultValue: T,
availableValues?: T[]
) {
const value = process.env[env];
if (value === undefined) {
return defaultValue;
}
if (availableValues && !availableValues.includes(value as any)) {
throw new Error(
`Invalid value '${value}' for environment variable ${env}, expected one of [${availableValues.join(
', '
)}]`
);
}
return value as T;
}

View File

@@ -9,6 +9,7 @@ export {
applyEnvToConfig,
Config,
type ConfigPaths,
DeploymentType,
getDefaultAFFiNEStorageConfig,
} from './config';
export { EventEmitter, type EventPayload, OnEvent } from './event';

View File

@@ -1,11 +1,21 @@
import { Global, Module } from '@nestjs/common';
import { OptionalModule } from '../nestjs';
import { MailService } from './mail.service';
import { MAILER } from './mailer';
@Global()
@OptionalModule({
providers: [MAILER],
exports: [MAILER],
requires: ['mailer.auth.user', 'mailer.auth.pass'],
})
class MailerModule {}
@Global()
@Module({
providers: [MAILER, MailService],
imports: [MailerModule],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}

View File

@@ -1,30 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, Optional } from '@nestjs/common';
import { Config } from '../config';
import {
MAILER_SERVICE,
type MailerService,
type Options,
type Response,
} from './mailer';
import { MAILER_SERVICE, type MailerService, type Options } from './mailer';
import { emailTemplate } from './template';
@Injectable()
export class MailService {
constructor(
@Inject(MAILER_SERVICE) private readonly mailer: MailerService,
private readonly config: Config
private readonly config: Config,
@Optional() @Inject(MAILER_SERVICE) private readonly mailer?: MailerService
) {}
async sendMail(options: Options): Promise<Response> {
return this.mailer.sendMail(options);
async sendMail(options: Options) {
if (!this.mailer) {
throw new Error('Mailer service is not configured.');
}
return this.mailer.sendMail({
from: this.config.mailer?.from,
...options,
});
}
hasConfigured() {
return (
!!this.config.auth.email.login &&
!!this.config.auth.email.password &&
!!this.config.auth.email.sender
);
return !!this.mailer;
}
async sendInviteEmail(
@@ -80,7 +78,6 @@ export class MailService {
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `${invitationInfo.user.name} invited you to join ${invitationInfo.workspace.name}`,
html,
@@ -119,7 +116,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Modify your AFFiNE password`,
html,
@@ -135,7 +131,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Set your AFFiNE password`,
html,
@@ -150,7 +145,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Verify your current email for AFFiNE`,
html,
@@ -165,7 +159,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Verify your new email for AFFiNE`,
html,
@@ -177,7 +170,6 @@ export class MailService {
content: `As per your request, we have changed your email. Please make sure you're using ${to} when you log in the next time. `,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Your email has been changed`,
html,
@@ -200,7 +192,6 @@ export class MailService {
content: `${inviteeName} has joined ${workspaceName}`,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: title,
html,
@@ -223,7 +214,6 @@ export class MailService {
content: `${inviteeName} has left your workspace`,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: title,
html,

View File

@@ -11,28 +11,11 @@ export type Response = SMTPTransport.SentMessageInfo;
export type Options = SMTPTransport.Options;
export const MAILER: FactoryProvider<
Transporter<SMTPTransport.SentMessageInfo>
Transporter<SMTPTransport.SentMessageInfo> | undefined
> = {
provide: MAILER_SERVICE,
useFactory: (config: Config) => {
if (config.auth.localEmail) {
return createTransport({
host: '0.0.0.0',
port: 1025,
secure: false,
auth: {
user: config.auth.email.login,
pass: config.auth.email.password,
},
});
}
return createTransport({
service: 'gmail',
auth: {
user: config.auth.email.login,
pass: config.auth.email.password,
},
});
return config.mailer ? createTransport(config.mailer) : undefined;
},
inject: [Config],
};

View File

@@ -1,28 +1,48 @@
import { Global, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import {
Global,
Module,
OnModuleDestroy,
OnModuleInit,
Provider,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Config, parseEnvValue } from '../config';
import { createSDK, registerCustomMetrics } from './opentelemetry';
import { Config } from '../config';
import {
LocalOpentelemetryFactory,
OpentelemetryFactory,
registerCustomMetrics,
} from './opentelemetry';
const factorProvider: Provider = {
provide: OpentelemetryFactory,
useFactory: (config: Config) => {
return config.metrics.enabled ? new LocalOpentelemetryFactory() : null;
},
inject: [Config],
};
@Global()
@Module({})
@Module({
providers: [factorProvider],
exports: [factorProvider],
})
export class MetricsModule implements OnModuleInit, OnModuleDestroy {
private sdk: NodeSDK | null = null;
constructor(private readonly config: Config) {}
constructor(private readonly ref: ModuleRef) {}
onModuleInit() {
if (
this.config.metrics.enabled &&
!parseEnvValue(process.env.DISABLE_TELEMETRY, 'boolean')
) {
this.sdk = createSDK();
const factor = this.ref.get(OpentelemetryFactory, { strict: false });
if (factor) {
this.sdk = factor.create();
this.sdk.start();
registerCustomMetrics();
}
}
async onModuleDestroy() {
if (this.config.metrics.enabled && this.sdk) {
if (this.sdk) {
await this.sdk.shutdown();
}
}
@@ -30,3 +50,4 @@ export class MetricsModule implements OnModuleInit, OnModuleDestroy {
export * from './metrics';
export * from './utils';
export { OpentelemetryFactory };

View File

@@ -1,6 +1,4 @@
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
import { GcpDetectorSync } from '@google-cloud/opentelemetry-resource-util';
import { OnModuleDestroy } from '@nestjs/common';
import { metrics } from '@opentelemetry/api';
import {
CompositePropagator,
@@ -18,16 +16,13 @@ import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'
import { SocketIoInstrumentation } from '@opentelemetry/instrumentation-socket.io';
import { Resource } from '@opentelemetry/resources';
import {
ConsoleMetricExporter,
type MeterProvider,
MetricProducer,
MetricReader,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
BatchSpanProcessor,
ConsoleSpanExporter,
SpanExporter,
TraceIdRatioBasedSampler,
} from '@opentelemetry/sdk-trace-node';
@@ -38,7 +33,7 @@ import { PrismaMetricProducer } from './prisma';
const { PrismaInstrumentation } = prismaInstrument;
abstract class OpentelemetryFactor {
export abstract class OpentelemetryFactory {
abstract getMetricReader(): MetricReader;
abstract getSpanExporter(): SpanExporter;
@@ -59,7 +54,7 @@ abstract class OpentelemetryFactor {
getResource() {
return new Resource({
[SemanticResourceAttributes.K8S_NAMESPACE_NAME]: AFFiNE.affineEnv,
[SemanticResourceAttributes.K8S_NAMESPACE_NAME]: AFFiNE.AFFINE_ENV,
[SemanticResourceAttributes.SERVICE_NAME]: AFFiNE.flavor.type,
[SemanticResourceAttributes.SERVICE_VERSION]: AFFiNE.version,
});
@@ -85,32 +80,20 @@ abstract class OpentelemetryFactor {
}
}
class GCloudOpentelemetryFactor extends OpentelemetryFactor {
override getResource(): Resource {
return super.getResource().merge(new GcpDetectorSync().detect());
export class LocalOpentelemetryFactory
extends OpentelemetryFactory
implements OnModuleDestroy
{
private readonly metricsExporter = new PrometheusExporter({
metricProducers: this.getMetricsProducers(),
});
async onModuleDestroy() {
await this.metricsExporter.shutdown();
}
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exportIntervalMillis: 30000,
exportTimeoutMillis: 10000,
exporter: new MetricExporter({
prefix: 'custom.googleapis.com',
}),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new TraceExporter();
}
}
class LocalOpentelemetryFactor extends OpentelemetryFactor {
override getMetricReader(): MetricReader {
return new PrometheusExporter({
metricProducers: this.getMetricsProducers(),
});
return this.metricsExporter;
}
override getSpanExporter(): SpanExporter {
@@ -118,33 +101,6 @@ class LocalOpentelemetryFactor extends OpentelemetryFactor {
}
}
class DebugOpentelemetryFactor extends OpentelemetryFactor {
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new ConsoleSpanExporter();
}
}
// TODO(@forehalo): make it configurable
export function createSDK() {
let factor: OpentelemetryFactor | null = null;
if (process.env.NODE_ENV === 'production') {
factor = new GCloudOpentelemetryFactor();
} else if (process.env.DEBUG_METRICS) {
factor = new DebugOpentelemetryFactor();
} else {
factor = new LocalOpentelemetryFactor();
}
return factor?.create();
}
function getMeterProvider() {
return metrics.getMeterProvider();
}

View File

@@ -1,14 +1,16 @@
/// <reference types="./global.d.ts" />
// keep the config import at the top
// eslint-disable-next-line simple-import-sort/imports
import './prelude';
import { Logger } from '@nestjs/common';
import { createApp } from './app';
const app = await createApp();
const listeningHost = AFFiNE.deploy ? '0.0.0.0' : 'localhost';
await app.listen(AFFiNE.port, listeningHost);
console.log(
`AFFiNE Server has been started on http://${listeningHost}:${AFFiNE.port}.`
);
console.log(`And the public server should be recognized as ${AFFiNE.baseUrl}`);
const logger = new Logger('App');
logger.log(`AFFiNE Server is running in [${AFFiNE.type}] mode`);
logger.log(`Listening on http://${listeningHost}:${AFFiNE.port}`);
logger.log(`And the public server should be recognized as ${AFFiNE.baseUrl}`);

View File

@@ -1,3 +1,4 @@
import { GCloudConfig } from './gcloud/config';
import { PaymentConfig } from './payment';
import { RedisOptions } from './redis';
@@ -5,6 +6,7 @@ declare module '../fundamentals/config' {
interface PluginsConfig {
readonly payment: PaymentConfig;
readonly redis: RedisOptions;
readonly gcloud: GCloudConfig;
}
export type AvailablePlugins = keyof PluginsConfig;

View File

@@ -0,0 +1 @@
export interface GCloudConfig {}

View File

@@ -0,0 +1,10 @@
import { Global } from '@nestjs/common';
import { OptionalModule } from '../../fundamentals';
import { GCloudMetrics } from './metrics';
@Global()
@OptionalModule({
imports: [GCloudMetrics],
})
export class GCloudModule {}

View File

@@ -0,0 +1,46 @@
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
import { GcpDetectorSync } from '@google-cloud/opentelemetry-resource-util';
import { Global, Provider } from '@nestjs/common';
import { Resource } from '@opentelemetry/resources';
import {
MetricReader,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { SpanExporter } from '@opentelemetry/sdk-trace-node';
import { OptionalModule } from '../../fundamentals';
import { OpentelemetryFactory } from '../../fundamentals/metrics';
export class GCloudOpentelemetryFactory extends OpentelemetryFactory {
override getResource(): Resource {
return super.getResource().merge(new GcpDetectorSync().detect());
}
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exportIntervalMillis: 30000,
exportTimeoutMillis: 10000,
exporter: new MetricExporter({
prefix: 'custom.googleapis.com',
}),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new TraceExporter();
}
}
const factorProvider: Provider = {
provide: OpentelemetryFactory,
useFactory: () => new GCloudOpentelemetryFactory(),
};
@Global()
@OptionalModule({
if: config => config.metrics.enabled,
overrides: [factorProvider],
})
export class GCloudMetrics {}

View File

@@ -1,8 +1,10 @@
import type { AvailablePlugins } from '../fundamentals/config';
import { GCloudModule } from './gcloud';
import { PaymentModule } from './payment';
import { RedisModule } from './redis';
export const pluginsMap = new Map<AvailablePlugins, AFFiNEModule>([
['payment', PaymentModule],
['redis', RedisModule],
['gcloud', GCloudModule],
]);

View File

@@ -23,6 +23,7 @@ import { StripeWebhook } from './webhook';
// 'plugins.payment.stripe.keys.webhookKey',
// ],
contributesTo: ServerFeature.Payment,
if: config => config.flavor.graphql,
})
export class PaymentModule {}

View File

@@ -337,7 +337,7 @@ export class UserSubscriptionResolver {
// @FIXME(@forehalo): should not mock any api for selfhosted server
// the frontend should avoid calling such api if feature is not enabled
if (this.config.flavor.selfhosted) {
if (this.config.isSelfhosted) {
const start = new Date();
const end = new Date();
end.setFullYear(start.getFullYear() + 1);

View File

@@ -5,6 +5,7 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { config } from 'dotenv';
import { omit } from 'lodash-es';
import {
applyEnvToConfig,
@@ -43,14 +44,23 @@ async function load() {
// 3. load env => config map to `globalThis.AFFiNE.ENV_MAP
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.env.js');
// 4. apply `process.env` map overriding to `globalThis.AFFiNE`
applyEnvToConfig(globalThis.AFFiNE);
// 5. load `config/affine` to patch custom configs
// 4. load `config/affine` to patch custom configs
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.js');
if (process.env.NODE_ENV === 'development') {
console.log('AFFiNE Config:', JSON.stringify(globalThis.AFFiNE, null, 2));
// 5. load `config/affine.self` to patch custom configs
// This is the file only take effect in [AFFiNE Cloud]
if (!AFFiNE.isSelfhosted) {
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.self.js');
}
// 6. apply `process.env` map overriding to `globalThis.AFFiNE`
applyEnvToConfig(globalThis.AFFiNE);
if (AFFiNE.node.dev) {
console.log(
'AFFiNE Config:',
JSON.stringify(omit(globalThis.AFFiNE, 'ENV_MAP'), null, 2)
);
}
}

View File

@@ -240,10 +240,18 @@ type ServerConfigType {
"""server identical name could be shown as badge on user interface"""
name: String!
"""server type"""
type: ServerDeploymentType!
"""server version"""
version: String!
}
enum ServerDeploymentType {
Affine
Selfhosted
}
enum ServerFeature {
Payment
}

View File

@@ -18,7 +18,7 @@ test.afterEach.always(async () => {
test('should be able to get config', t => {
t.true(typeof config.host === 'string');
t.is(config.env, 'test');
t.is(config.NODE_ENV, 'test');
});
test('should be able to override config', async t => {

View File

@@ -7,7 +7,7 @@ import type {
WorkspaceInfoSnapshot,
} from '@blocksuite/store';
import { Job } from '@blocksuite/store';
import type { createStore, WritableAtom } from 'jotai/vanilla';
import type { createStore, WritableAtom } from 'jotai';
import { Map as YMap } from 'yjs';
import { getLatestVersions } from '../migration/blocksuite';
@@ -29,6 +29,7 @@ export function initEmptyPage(page: Page, title?: string) {
*/
export async function buildShowcaseWorkspace(
workspace: Workspace,
blobStorage: { set: (key: string, blob: Blob) => Promise<string> },
{
store,
atoms,
@@ -46,6 +47,7 @@ export async function buildShowcaseWorkspace(
const { onboarding } = await import('@affine/templates');
const info = onboarding['info.json'] as WorkspaceInfoSnapshot;
const blob = onboarding['blob.json'] as { [key: string]: string };
const migrationMiddleware: JobMiddleware = ({ slots, workspace }) => {
slots.afterImport.on(payload => {
@@ -88,27 +90,29 @@ export async function buildShowcaseWorkspace(
.getMap('meta')
.set('blockVersions', new YMap(Object.entries(newVersions)));
for (const [key, base64] of Object.entries(blob)) {
await blobStorage.set(key, new Blob([base64ToUint8Array(base64)]));
}
// todo: find better way to do the following
// perhaps put them into middleware?
{
// the "AFFiNE - not just a note-taking app" page should be set to edgeless mode
// the "Write, Draw, Plan all at Once." page should be set to edgeless mode
const edgelessPage1 = (workspace.meta.pages as PageMeta[])?.find(
p => p.title === 'AFFiNE - not just a note-taking app'
p => p.title === 'Write, Draw, Plan all at Once.'
)?.id;
if (edgelessPage1) {
workspace.setPageMeta(edgelessPage1, { jumpOnce: true });
store.set(atoms.pageMode, edgelessPage1, 'edgeless');
}
// should jump to "Getting Started" by default
const gettingStartedPage = (workspace.meta.pages as PageMeta[])?.find(p =>
p.title.startsWith('Getting Started')
)?.id;
if (gettingStartedPage) {
workspace.setPageMeta(gettingStartedPage, {
jumpOnce: true,
});
}
}
}
function base64ToUint8Array(base64: string) {
const binaryString = atob(base64);
const binaryArray = binaryString.split('').map(function (char) {
return char.charCodeAt(0);
});
return new Uint8Array(binaryArray);
}

View File

@@ -13,6 +13,9 @@ export function fixWorkspaceVersion(rootDoc: YDoc) {
* Blocksuite just set the value, do nothing else.
*/
function doFix() {
if (meta.size === 0) {
return;
}
const workspaceVersion = meta.get('workspaceVersion');
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
transact(

View File

@@ -19,6 +19,7 @@ export interface SyncPeerStatus {
loadedDocs: number;
pendingPullUpdates: number;
pendingPushUpdates: number;
rootDocLoaded: boolean;
}
/**
@@ -54,6 +55,7 @@ export class SyncPeer {
loadedDocs: 0,
pendingPullUpdates: 0,
pendingPushUpdates: 0,
rootDocLoaded: false,
};
onStatusChange = new Slot<SyncPeerStatus>();
readonly abort = new AbortController();
@@ -119,6 +121,7 @@ export class SyncPeer {
loadedDocs: 0,
pendingPullUpdates: 0,
pendingPushUpdates: 0,
rootDocLoaded: this.status.rootDocLoaded,
};
await Promise.race([
new Promise<void>(resolve => {
@@ -291,6 +294,13 @@ export class SyncPeer {
(await this.storage.pull(doc.guid, encodeStateVector(doc))) ?? {};
throwIfAborted(abort);
if (docData !== undefined && doc.guid === this.rootDoc.guid) {
this.status = {
...this.status,
rootDocLoaded: true,
};
}
if (docData) {
applyUpdate(doc, docData, 'load');
}
@@ -391,6 +401,7 @@ export class SyncPeer {
this.state.pullUpdatesQueue.length + (this.state.subdocLoading ? 1 : 0),
pendingPushUpdates:
this.state.pushUpdatesQueue.length + (this.state.pushingUpdate ? 1 : 0),
rootDocLoaded: this.status.rootDocLoaded,
};
}

View File

@@ -19,10 +19,10 @@ export async function createFirstAppData() {
localStorage.setItem('is-first-open', 'false');
const workspaceId = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL,
async workspace => {
async (workspace, blob) => {
workspace.meta.setName(DEFAULT_WORKSPACE_NAME);
if (runtimeConfig.enablePreloading) {
await buildShowcaseWorkspace(workspace, {
await buildShowcaseWorkspace(workspace, blob, {
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,

View File

@@ -150,10 +150,10 @@ export const CreateWorkspaceModal = ({
// fix me later
const id = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL,
async workspace => {
async (workspace, blob) => {
workspace.meta.setName(name);
if (runtimeConfig.enablePreloading) {
await buildShowcaseWorkspace(workspace, {
await buildShowcaseWorkspace(workspace, blob, {
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,

View File

@@ -21,12 +21,12 @@ class CustomAttachmentService extends AttachmentService {
}
function customLoadFonts(service: PageService): void {
const officialDomains = new Set(['affine.pro', 'affine.fail']);
const officialDomains = new Set(['app.affine.pro', 'affine.fail']);
if (!officialDomains.has(window.location.host)) {
const fonts = CanvasTextFonts.map(font => ({
...font,
// self-hosted fonts are served from /assets
url: '/assets' + new URL(font.url).pathname.split('/').pop(),
url: '/assets/' + new URL(font.url).pathname.split('/').pop(),
}));
service.fontLoader.load(fonts);
} else {

View File

@@ -1,4 +1,4 @@
import { serverConfigQuery } from '@affine/graphql';
import { serverConfigQuery, ServerDeploymentType } from '@affine/graphql';
import type { BareFetcher, Middleware } from 'swr';
import { useQueryImmutable } from '../use-query';
@@ -25,20 +25,20 @@ const useServerConfig = () => {
return config.serverConfig;
};
export const useServerFlavor = () => {
export const useServerType = () => {
const config = useServerConfig();
if (!config) {
return 'local';
}
return config.flavor;
return config.type;
};
export const useSelfHosted = () => {
const serverFlavor = useServerFlavor();
const serverType = useServerType();
return ['local', 'selfhosted'].includes(serverFlavor);
return ['local', ServerDeploymentType.Selfhosted].includes(serverType);
};
export const useServerBaseUrl = () => {

View File

@@ -6,7 +6,8 @@ import {
workspaceListLoadingStatusAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { type Workspace } from '@affine/workspace';
import type { Workspace } from '@affine/workspace';
import { type SyncEngineStatus } from '@affine/workspace';
import { useAtom, useAtomValue } from 'jotai';
import {
type ReactElement,
@@ -75,25 +76,32 @@ export const Component = (): ReactElement => {
const [workspaceIsLoading, setWorkspaceIsLoading] = useState(true);
const [syncEngineStatus, setSyncEngineStatus] = useState<SyncEngineStatus>();
useEffect(() => {
if (!workspace) {
setSyncEngineStatus(undefined);
}
return workspace?.engine.sync.onStatusChange.on(setSyncEngineStatus)
.dispose;
}, [workspace]);
// hotfix: avoid doing operation, before workspace is loaded
useEffect(() => {
if (!workspace) {
setWorkspaceIsLoading(true);
return;
}
const metaYMap = workspace.blockSuiteWorkspace.doc.getMap('meta');
const handleYMapChanged = () => {
setWorkspaceIsLoading(metaYMap.size === 0);
};
handleYMapChanged();
metaYMap.observe(handleYMapChanged);
return () => {
metaYMap.unobserve(handleYMapChanged);
};
}, [workspace]);
if (
[syncEngineStatus?.local, ...(syncEngineStatus?.remotes ?? [])].some(
p => p?.rootDocLoaded === true
)
) {
setWorkspaceIsLoading(false);
} else {
setWorkspaceIsLoading(true);
}
}, [syncEngineStatus, workspace]);
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
if (listLoading === false && meta === undefined) {

View File

@@ -677,7 +677,7 @@ query serverConfig {
baseUrl
name
features
flavor
type
}
}`,
};

View File

@@ -4,6 +4,6 @@ query serverConfig {
baseUrl
name
features
flavor
type
}
}

View File

@@ -71,6 +71,11 @@ export enum PublicPageMode {
Page = 'Page',
}
export enum ServerDeploymentType {
Affine = 'Affine',
Selfhosted = 'Selfhosted',
}
export enum ServerFeature {
Payment = 'Payment',
}
@@ -673,7 +678,7 @@ export type ServerConfigQuery = {
baseUrl: string;
name: string;
features: Array<ServerFeature>;
flavor: string;
type: ServerDeploymentType;
};
};

View File

@@ -1,211 +0,0 @@
{
"type": "page",
"meta": {
"id": "-P-O4GSfVGTkI16zJRVa4",
"title": "Templates Galleries ",
"createDate": 1691551731225,
"tags": []
},
"blocks": {
"type": "block",
"id": "Tz41kDyemg",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Templates Galleries "
}
]
}
},
"children": [
{
"type": "block",
"id": "PCxQvHuwt1",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "PMY4JPuq4o",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,529]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "1KJadeDAj-",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "_a8e4OM_PP",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "No matter if you're "
},
{
"insert": "organizing your personal life",
"attributes": {
"bold": true
}
},
{
"insert": " or "
},
{
"insert": "getting things done at work",
"attributes": {
"bold": true
}
},
{
"insert": ", our templates galleries have got you covered! "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "ndmUz6zEr1",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Here We offer a wide range of resources to meet your unique needs and help you achieve your goals, whether in your personal or professional life."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "yIcapYtWKm",
"flavour": "affine:divider",
"props": {},
"children": []
},
{
"type": "block",
"id": "gPqHv8Whaq",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Tired of managing your notes or coming up with a effient work plan? Whether you're a student, parent, or have diverse interests, here we provide a few templates to kick off your journey and unlock your true potential with AFFiNE and reap incredible benefits."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "pp12c2slOV",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "0nee_XzkrN23Xy7HMoXL-"
}
}
},
{
"insert": " "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "VyoH5kUerc",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Staying organized and efficient in a dynamic work environment is essential for today's working people. AFFiNE elevates productivity for project managers, software engineers, and professionals alike. We're excited to provide insights into how AFFiNE transforms work life. "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "XvCeZ-f-ib",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "nPPyOV0JBNKu5hvW9hE00"
}
}
}
]
}
},
"children": []
}
]
}
]
}
}

View File

@@ -1,178 +0,0 @@
{
"type": "page",
"meta": {
"id": "0nee_XzkrN23Xy7HMoXL-",
"title": "Personal Home",
"createDate": 1691552082822,
"tags": []
},
"blocks": {
"type": "block",
"id": "QmQb34xduM",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Personal Home"
}
]
}
},
"children": [
{
"type": "block",
"id": "f4WAKZ67ki",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "xj7QSmgQgT",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,547]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "qlnEliaAwr",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "U6w_UTi2p5",
"flavour": "affine:list",
"props": {
"type": "bulleted",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "e5cYLNpIRUb-nwpSZGdix"
}
}
},
{
"insert": " "
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "YN7S4Pe19c",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Personal knowledge management (PKM for short) is "
},
{
"insert": "the process of collecting, organizing, and storing information",
"attributes": {
"bold": true
}
},
{
"insert": ", so it's easier to search for, retrieve, share, expand upon, and use later on."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "V9nK8F7B0R",
"flavour": "affine:divider",
"props": {},
"children": []
},
{
"type": "block",
"id": "jj-qOseCj-",
"flavour": "affine:list",
"props": {
"type": "bulleted",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "Scod6coKmJJ-waH1-jkiW"
}
}
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "hgSgQdYwWJ",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "An event planning template ",
"attributes": {
"bold": true
}
},
{
"insert": "is an efficient framework that details the steps you and your team need to consider taking to plan and execute a successful event."
}
]
}
},
"children": []
}
]
}
]
}
}

View File

@@ -1,310 +0,0 @@
{
"type": "page",
"meta": {
"id": "GWsRbUtMF4Ee6foVg-H9R",
"title": "Meeting Summary ",
"createDate": 1691635388447,
"tags": [
"Oe5dSe1DDJ"
]
},
"blocks": {
"type": "block",
"id": "EYboj1it1i",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Meeting Summary "
}
]
}
},
"children": [
{
"type": "block",
"id": "86K_nlhKXG",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "q7fYMX90uJ",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,1237]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "IuRC73HZLH",
"flavour": "affine:image",
"props": {
"caption": "",
"sourceId": "/static/f9yKnlNMgKhF-CxOgHBsXkxfViCCkC6KwTv6Uj2Fcjw=.png",
"width": 752,
"height": 501.6328125,
"index": "a0",
"xywh": "[0,0,0,0]",
"rotate": 0,
"size": -1
},
"children": []
},
{
"type": "block",
"id": "YuOqeDHzoe",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Project:"
},
{
"insert": " [Link the project plan]\n",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
},
{
"insert": "]Attendees: "
},
{
"insert": "[@mention teammates]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
},
{
"insert": "\nDate & Time:"
},
{
"insert": " [type /today]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "jPGxHHA_QX",
"flavour": "affine:paragraph",
"props": {
"type": "h1",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Agenda"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "sCk70nT5eX",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Write down the main topic to discuss."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "iKwcxVFpCH",
"flavour": "affine:list",
"props": {
"type": "bulleted",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "E.g. content roadmap"
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "RPED0t8fSj",
"flavour": "affine:list",
"props": {
"type": "bulleted",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Get aligned on the main upcoming topics"
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "D3Y93H8WvU",
"flavour": "affine:paragraph",
"props": {
"type": "h1",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Notes"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "BfO7S4Gkfm",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Cover any key information discussed in the meeting."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "Stcz30R0Ad",
"flavour": "affine:paragraph",
"props": {
"type": "h1",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Outcomes"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "YKIkBUmv01",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Summarize key decisions in a tidy paragraph and communicate them to the team."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "JzAgpzljrp",
"flavour": "affine:paragraph",
"props": {
"type": "h1",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Action items"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "FSkrwB-T33",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "E.g. @jess review the content roadmap"
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "abGz-82vvA",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "E.g. @mike contact copywriter freelancers"
}
]
},
"checked": false,
"collapsed": false
},
"children": []
}
]
}
]
}
}

View File

@@ -1,457 +0,0 @@
{
"type": "page",
"meta": {
"id": "RCpxnWMtBWmUZy5awgJBh",
"title": "OKR Template",
"createDate": 1691636192263,
"tags": [
"q3mceOl_zi",
"g1L5dXKctL"
]
},
"blocks": {
"type": "block",
"id": "0tJt1nfXpr",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "OKR Template"
}
]
}
},
"children": [
{
"type": "block",
"id": "-SeDPuI6pE",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "gjPKqwOdlZ",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,1418]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "syUYSO6Hlw",
"flavour": "affine:image",
"props": {
"caption": "",
"sourceId": "/static/VuXYyM9JUv1Fv_qjg1v5Go4Zksz0r4NXFeh3Na7JkIc=.png",
"width": 752,
"height": 501.2734375,
"index": "a0",
"xywh": "[0,0,0,0]",
"rotate": 0,
"size": -1
},
"children": []
},
{
"type": "block",
"id": "B12ua5IVdH",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Using "
},
{
"insert": "OKR (Objectives and Key Results)",
"attributes": {
"link": "https://en.wikipedia.org/wiki/Objectives_and_key_results"
}
},
{
"insert": " helps teams work together to create and break down their goals. They also figure out how to reach those goals and use their own ideas and creativity. Every week, the team checks in to make sure everyone understands the goals, shares their thoughts, and makes changes as needed to get the job done well and quickly."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "UWKiht4_yF",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "tsqRR7Aqqe",
"flavour": "affine:paragraph",
"props": {
"type": "h2",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "I. Synchronization of team OKR progress "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "TzALQTyjsF",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "-RNdJMcB-M",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "All team members collectively update the latest progress and the next step plan for the person responsible for the OKR. These updates are discussed during the weekly Team OKR meeting."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "_E43A1XjkR",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "lbLc-rsTQ_",
"flavour": "affine:paragraph",
"props": {
"type": "h1",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "O1: Fourth quarter revenue 1 million "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "IbLsSTqZWu",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "LfH4-lvRy1",
"flavour": "affine:database",
"props": {
"views": [
{
"id": "BjTEwK-3iR",
"name": "table",
"mode": "table",
"columns": [
{
"id": "f15PcPjtU8",
"width": 246
},
{
"id": "LfH4-lvRy1",
"hide": false,
"width": 260
},
{
"id": "ftL1qPRgYl",
"hide": false,
"width": 200
},
{
"id": "3s4GtwNZTH",
"hide": false,
"width": 108
},
{
"id": "IwZCoBvV9W",
"hide": false,
"width": 142
}
],
"filter": {
"type": "group",
"op": "and",
"conditions": []
}
}
],
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "KR setting & reviewing "
}
]
},
"cells": {
"C75yOBGfeP": {
"ftL1qPRgYl": {
"columnId": "ftL1qPRgYl",
"value": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "the planning case has been reviewed, see doc for details "
}
]
}
},
"IwZCoBvV9W": {
"columnId": "IwZCoBvV9W",
"value": 85
}
},
"iouBY8Zk33": {
"ftL1qPRgYl": {
"columnId": "ftL1qPRgYl",
"value": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "worning on it "
}
]
}
},
"IwZCoBvV9W": {
"columnId": "IwZCoBvV9W",
"value": 46
},
"3s4GtwNZTH": {
"columnId": "3s4GtwNZTH",
"value": "Qp1kGLAbfE"
}
},
"jRFdpU21Jf": {
"ftL1qPRgYl": {
"columnId": "ftL1qPRgYl",
"value": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "waiting "
}
]
}
},
"IwZCoBvV9W": {
"columnId": "IwZCoBvV9W",
"value": 3
},
"3s4GtwNZTH": {
"columnId": "3s4GtwNZTH",
"value": "yCF1NgLWhh"
}
},
"CLLfj990JA": {
"ftL1qPRgYl": {
"columnId": "ftL1qPRgYl",
"value": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "The planning case has been reviewed, see doc for details "
}
]
}
},
"IwZCoBvV9W": {
"columnId": "IwZCoBvV9W",
"value": 70
},
"3s4GtwNZTH": {
"columnId": "3s4GtwNZTH",
"value": "KHKc4e79W8"
}
}
},
"columns": [
{
"id": "f15PcPjtU8",
"type": "title",
"name": "Key Resluts ",
"data": {}
},
{
"type": "title",
"id": "LfH4-lvRy1",
"name": "Title",
"data": {}
},
{
"type": "progress",
"name": "progress bar ",
"data": {},
"id": "IwZCoBvV9W"
},
{
"type": "rich-text",
"name": "Notes ",
"data": {
"options": []
},
"id": "ftL1qPRgYl"
},
{
"type": "select",
"name": "priority ",
"data": {
"options": [
{
"id": "yCF1NgLWhh",
"value": "not urgent",
"color": "var(--affine-tag-blue)"
},
{
"id": "Qp1kGLAbfE",
"value": "urgent",
"color": "var(--affine-tag-red)"
},
{
"id": "KHKc4e79W8",
"value": "important",
"color": "var(--affine-tag-yellow)"
}
]
},
"id": "3s4GtwNZTH"
}
]
},
"children": [
{
"type": "block",
"id": "CLLfj990JA",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "The campus activity was successfully released, and the exposure was not less than 1 million people"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "iouBY8Zk33",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "describe Key results "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "jRFdpU21Jf",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "KR1: Describe Key results "
}
]
}
},
"children": []
}
]
},
{
"type": "block",
"id": "PmstSRjisx",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
}
]
}
]
}
}

View File

@@ -1,330 +0,0 @@
{
"type": "page",
"meta": {
"id": "Scod6coKmJJ-waH1-jkiW",
"title": "Brief Event planning ",
"createDate": 1691634722239,
"tags": [
"ze07JVwBu4"
]
},
"blocks": {
"type": "block",
"id": "SMCf2aOH8T",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Brief Event planning "
}
]
}
},
"children": [
{
"type": "block",
"id": "_07_dOaECY",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "0n7YghSHPc",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,527]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "d6Alacwr32",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "9slwdYgqgq",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "An event brief outline the goals and key elements needed to plan a successful event. This AFFiNE Event Planning template will help you gain alignment and kick off your event planning. "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "6dH3Amz0rn",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "ObsV2gBxUe",
"flavour": "affine:database",
"props": {
"views": [
{
"id": "sfIqqrnI2q",
"name": "table",
"mode": "table",
"columns": [
{
"id": "WtNvmw4ls-",
"width": 186
},
{
"id": "ObsV2gBxUe",
"hide": false,
"width": 246
},
{
"id": "FCYCytB4wx",
"hide": false,
"width": 162
},
{
"id": "mGltZNTJmw",
"hide": false,
"width": 176
},
{
"id": "ozFBusFd0s",
"hide": false,
"width": 200
}
],
"filter": {
"type": "group",
"op": "and",
"conditions": []
}
}
],
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Event Planning"
}
]
},
"cells": {
"pFlIzSXLOj": {
"mGltZNTJmw": {
"columnId": "mGltZNTJmw",
"value": [
"bsTnd_L1aE",
"oU49ElCHCG"
]
},
"FCYCytB4wx": {
"columnId": "FCYCytB4wx",
"value": 100
},
"ozFBusFd0s": {
"columnId": "ozFBusFd0s",
"value": "https://affine.pro/blog/free-event-planning-templates-for-professsional-2023"
}
},
"4XPamQdWpq": {
"mGltZNTJmw": {
"columnId": "mGltZNTJmw",
"value": [
"oU49ElCHCG",
"d6riek6K4d"
]
},
"FCYCytB4wx": {
"columnId": "FCYCytB4wx",
"value": 50
}
},
"3uJL-VNu7K": {
"mGltZNTJmw": {
"columnId": "mGltZNTJmw",
"value": [
"bsTnd_L1aE",
"z3vOXFIT-T"
]
},
"FCYCytB4wx": {
"columnId": "FCYCytB4wx",
"value": 8
}
}
},
"columns": [
{
"id": "WtNvmw4ls-",
"type": "title",
"name": "Title",
"data": {}
},
{
"type": "title",
"id": "ObsV2gBxUe",
"name": "Title",
"data": {}
},
{
"type": "multi-select",
"name": "Tag",
"data": {
"options": [
{
"id": "z3vOXFIT-T",
"value": "not important",
"color": "var(--affine-tag-teal)"
},
{
"id": "ZbJHxXJK3L",
"value": "nit important",
"color": "var(--affine-tag-pink)"
},
{
"id": "d6riek6K4d",
"value": "important",
"color": "var(--affine-tag-yellow)"
},
{
"id": "oU49ElCHCG",
"value": "urgent",
"color": "var(--affine-tag-purple)"
},
{
"id": "bsTnd_L1aE",
"value": "must",
"color": "var(--affine-tag-green)"
},
{
"id": "hHjqAauZu6",
"value": "urgent",
"color": "var(--affine-tag-gray)"
}
]
},
"id": "mGltZNTJmw"
},
{
"type": "progress",
"name": "progress",
"data": {},
"id": "FCYCytB4wx"
},
{
"type": "link",
"name": "additional link ",
"data": {},
"id": "ozFBusFd0s"
}
]
},
"children": [
{
"type": "block",
"id": "pFlIzSXLOj",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "research"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "4XPamQdWpq",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "book accommondation"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "3uJL-VNu7K",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "arrange transportation "
}
]
}
},
"children": []
}
]
},
{
"type": "block",
"id": "xf1m62_pbc",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
}
]
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,750 +0,0 @@
{
"type": "page",
"meta": {
"id": "b1F2xKjgZ4ISHhXNe1kck",
"title": "Annual Performance Review",
"createDate": 1691575011078,
"tags": [
"8qcYPCTK0h"
]
},
"blocks": {
"type": "block",
"id": "mMBWzZtpvM",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Annual Performance Review"
}
]
}
},
"children": [
{
"type": "block",
"id": "XSMCxudQf4",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "2xxenQgE1-",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,91]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "G7wJk4W9lz",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
}
]
},
{
"type": "block",
"id": "ca2-VFQV45",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,1719]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "viFZDjrBqJ",
"flavour": "affine:image",
"props": {
"caption": "",
"sourceId": "/static/gZLmSgmwumNdgf0eIfOSW44emctrLyFUaZapbk8eZ6s=.png",
"width": 752,
"height": 422.9140625,
"index": "a0",
"xywh": "[0,0,0,0]",
"rotate": 0,
"size": -1
},
"children": []
},
{
"type": "block",
"id": "XPiPjHYnvE",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Employee Information:",
"attributes": {
"underline": true,
"bold": true
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "qpyYi7g_c6",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Name:",
"attributes": {
"bold": true
}
},
{
"insert": " "
},
{
"insert": "[Employee Name]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "NpbigpLTNh",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Department:",
"attributes": {
"bold": true
}
},
{
"insert": " "
},
{
"insert": "[Department Name]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "e1ax4lVFjW",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Job Title:",
"attributes": {
"bold": true
}
},
{
"insert": " "
},
{
"insert": "[Job Title]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "rEKXjYtGDR",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Date of Joining:",
"attributes": {
"bold": true
}
},
{
"insert": " "
},
{
"insert": "[Date of Joining]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "gYcGYxQ88e",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Review Date: ",
"attributes": {
"bold": true
}
},
{
"insert": " "
},
{
"insert": "[Review Date]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "OyFSyuWbzw",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "VcUt-bpjIK",
"flavour": "affine:database",
"props": {
"views": [
{
"id": "bIFdicaEON",
"name": "table",
"mode": "table",
"columns": [
{
"id": "jPry_IOhQt",
"width": 343
},
{
"id": "VcUt-bpjIK",
"hide": false,
"width": 260
},
{
"id": "teyC7JgUni",
"hide": false,
"width": 200
}
],
"filter": {
"type": "group",
"op": "and",
"conditions": []
}
}
],
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Employee's performance "
}
]
},
"cells": {
"VwZ3681tdd": {
"teyC7JgUni": {
"columnId": "teyC7JgUni",
"value": [
"rwjdYVF3jd"
]
}
},
"jT3Oo2XSeJ": {
"teyC7JgUni": {
"columnId": "teyC7JgUni",
"value": [
"4dHxaZ8cKk"
]
}
},
"RviIfLJUPX": {
"teyC7JgUni": {
"columnId": "teyC7JgUni",
"value": [
"wgvB2fzMu6"
]
}
}
},
"columns": [
{
"id": "jPry_IOhQt",
"type": "title",
"name": "Title",
"data": {}
},
{
"type": "title",
"id": "VcUt-bpjIK",
"name": "Title",
"data": {}
},
{
"type": "multi-select",
"name": "score ",
"data": {
"options": [
{
"id": "rwjdYVF3jd",
"value": "awsome",
"color": "var(--affine-tag-yellow)"
},
{
"id": "4dHxaZ8cKk",
"value": "well",
"color": "var(--affine-tag-purple)"
},
{
"id": "wgvB2fzMu6",
"value": "good",
"color": "var(--affine-tag-green)"
},
{
"id": "YGPLhMJkni",
"value": "done",
"color": "var(--affine-tag-pink)"
},
{
"id": "Q1-TuffR-9",
"value": "in progress",
"color": "var(--affine-tag-white)"
}
]
},
"id": "teyC7JgUni"
}
]
},
"children": [
{
"type": "block",
"id": "VwZ3681tdd",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[List Employee's Performance Goal 1]"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "jT3Oo2XSeJ",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[List Employee's Performance Goal 2]"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "RviIfLJUPX",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[List Employee's Performance Goal 3]"
}
]
}
},
"children": []
}
]
},
{
"type": "block",
"id": "a0xWifxvFi",
"flavour": "affine:database",
"props": {
"views": [
{
"id": "MvodpC_iAj",
"name": "table",
"mode": "table",
"columns": [
{
"id": "lvyHgZEelb",
"width": 354
},
{
"id": "a0xWifxvFi",
"hide": false,
"width": 260
},
{
"id": "ZdTyvkjQwz",
"hide": false,
"width": 200
}
],
"filter": {
"type": "group",
"op": "and",
"conditions": []
}
}
],
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Job responsibility "
}
]
},
"cells": {},
"columns": [
{
"id": "lvyHgZEelb",
"type": "title",
"name": "Title",
"data": {}
},
{
"type": "title",
"id": "a0xWifxvFi",
"name": "Title",
"data": {}
},
{
"type": "multi-select",
"name": "Tag",
"data": {
"options": []
},
"id": "ZdTyvkjQwz"
}
]
},
"children": [
{
"type": "block",
"id": "ZEi_-9SS-Y",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[List Job Responsibility 1]"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "8LPVaagwtP",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[List Job Responsibility 2]"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "lYnANRxW0A",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[List Job Responsibility 3]"
}
]
}
},
"children": []
}
]
},
{
"type": "block",
"id": "YiQINiDM6W",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Employee's Signature: "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "Zp-YtzbUUK",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Date: "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "ZMfsmg_5oZ",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "XH7xQMlX_0",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Manager's Signature: "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "mBO-_pWGoi",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Date: "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "X_SNf6rQf4",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "0LF9mjFcX9",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[Company Name]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "d2IrxHbD5D",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[Address]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "IdLESwrZ-U",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[Phone Number]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "6YwnA2HHQM",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "[Email]",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
}
]
}
},
"children": []
}
]
}
]
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,175 +1,21 @@
{
"type": "info",
"id": "XdwdkEYm8fFZcYAdDa0qT",
"blockVersions": {
"affine:code": 1,
"affine:paragraph": 1,
"affine:page": 2,
"affine:list": 1,
"affine:note": 1,
"affine:divider": 1,
"affine:image": 1,
"affine:surface": 5,
"affine:bookmark": 1,
"affine:frame": 1,
"affine:database": 3,
"affine:surface-ref": 1,
"affine:data-view": 1,
"affine:attachment": 1,
"affine:embed-github": 1,
"affine:embed-html": 1
},
"id": "dc61f2e3-a973-432f-a463-164a15cfc778",
"pageVersion": 2,
"workspaceVersion": 2,
"properties": {
"tags": {
"options": [
{
"id": "icg1n5UdkP",
"value": "Travel",
"color": "var(--affine-tag-gray)"
},
{
"id": "Oe5dSe1DDJ",
"value": "Quick summary",
"color": "var(--affine-tag-green)"
},
{
"id": "g1L5dXKctL",
"value": "OKR",
"color": "var(--affine-tag-purple)"
},
{
"id": "q3mceOl_zi",
"value": "Streamline your workflow",
"color": "var(--affine-tag-teal)"
},
{
"id": "ze07JVwBu4",
"value": "Plan",
"color": "var(--affine-tag-teal)"
},
{
"id": "8qcYPCTK0h",
"value": "Review",
"color": "var(--affine-tag-orange)"
},
{
"id": "wg-fBtd2eI",
"value": "Engage",
"color": "var(--affine-tag-pink)"
},
{
"id": "QYFD_HeQc-",
"value": "Create",
"color": "var(--affine-tag-blue)"
},
{
"id": "ZHBa2NtdSo",
"value": "Learn",
"color": "var(--affine-tag-yellow)"
}
]
"options": []
}
},
"pages": [
{
"id": "rzyBHDgN5KIlEYzB9oBaD",
"title": "Getting Started👇",
"createDate": 1691548231530,
"tags": [
"ZHBa2NtdSo",
"QYFD_HeQc-",
"wg-fBtd2eI"
],
"updatedDate": 1691676331623,
"favorite": true,
"jumpOnce": false
},
{
"id": "9iIScyvuIB_kUKbs1AYOQ",
"title": "AFFiNE - not just a note-taking app",
"createDate": 1691548220794,
"id": "W-d9_llZ6rE-qoTiHKTk4",
"createDate": 1706862386590,
"tags": [],
"updatedDate": 1691676775642,
"favorite": false
},
{
"id": "-P-O4GSfVGTkI16zJRVa4",
"title": "Templates Galleries ",
"createDate": 1691551731225,
"tags": [],
"updatedDate": 1691654611175,
"favorite": false
},
{
"id": "0nee_XzkrN23Xy7HMoXL-",
"title": "Personal Home",
"createDate": 1691552082822,
"tags": [],
"updatedDate": 1691654606912,
"favorite": false
},
{
"id": "nPPyOV0JBNKu5hvW9hE00",
"title": "Working Home",
"createDate": 1691552090989,
"tags": [],
"updatedDate": 1691646748171,
"favorite": false
},
{
"id": "wbXL4bZcblxLKC6ETqcQ1",
"title": "AFFiNE's Personal project management",
"createDate": 1691564303138,
"tags": [],
"updatedDate": 1691646845195
},
{
"id": "e5cYLNpIRUb-nwpSZGdix",
"title": "Personal-Knowledge Management ",
"createDate": 1691574859042,
"tags": [],
"updatedDate": 1691648159371
},
{
"id": "b1F2xKjgZ4ISHhXNe1kck",
"title": "Annual Performance Review",
"createDate": 1691575011078,
"tags": [
"8qcYPCTK0h"
],
"updatedDate": 1691645074511,
"favorite": false
},
{
"id": "Scod6coKmJJ-waH1-jkiW",
"title": "Brief Event planning ",
"createDate": 1691634722239,
"tags": [
"ze07JVwBu4"
],
"updatedDate": 1691647069662,
"favorite": false
},
{
"id": "GWsRbUtMF4Ee6foVg-H9R",
"title": "Meeting Summary ",
"createDate": 1691635388447,
"tags": [
"Oe5dSe1DDJ"
],
"updatedDate": 1691645873930
},
{
"id": "RCpxnWMtBWmUZy5awgJBh",
"title": "OKR Template",
"createDate": 1691636192263,
"tags": [
"q3mceOl_zi",
"g1L5dXKctL"
],
"updatedDate": 1691645102104
"favorite": false,
"title": "Write, Draw, Plan all at Once.",
"updatedDate": 1709110332309
}
]
}

View File

@@ -1,355 +0,0 @@
{
"type": "page",
"meta": {
"id": "nPPyOV0JBNKu5hvW9hE00",
"title": "Working Home",
"createDate": 1691552090989,
"tags": []
},
"blocks": {
"type": "block",
"id": "qsMt8nCetT",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Working Home"
}
]
}
},
"children": [
{
"type": "block",
"id": "yC-F6Rj9bA",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "pk_Cjkpyd4",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,939]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "IAOXx-DBBz",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "5hR-BR03ms",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "b1F2xKjgZ4ISHhXNe1kck"
}
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "zVgCQKWH-c",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "This "
},
{
"insert": "Annual Performance Review template",
"attributes": {
"bold": true
}
},
{
"insert": " is ideal for businesses seeking a structured and comprehensive approach to evaluating employee performance. By utilizing this template, you will gain clarity on individual strengths and areas for improvement, fostering effective communication and driving professional growth within your organization."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "ym2g48zSU_",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "o1UyrWV30t",
"flavour": "affine:divider",
"props": {},
"children": []
},
{
"type": "block",
"id": "1EUiH1t60b",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "GWsRbUtMF4Ee6foVg-H9R"
}
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "HmzMXKFaci",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Meeting minutes are an official record of a meeting",
"attributes": {
"bold": true
}
},
{
"insert": " for its participants. They're also sources of information for teammates who were unable to attend."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "v5CGQQPDrt",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "lcf4te4rxB",
"flavour": "affine:divider",
"props": {},
"children": []
},
{
"type": "block",
"id": "AbN8iFn0RW",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "uXdWxSSWgl",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "RCpxnWMtBWmUZy5awgJBh"
}
}
}
]
}
},
"children": []
},
{
"type": "block",
"id": "TBjwPij8cJ",
"flavour": "affine:paragraph",
"props": {
"type": "quote",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Maximize your team's performance and drive goal-oriented success with our extensive collection of "
},
{
"insert": "OKR templates",
"attributes": {
"bold": true
}
},
{
"insert": ", specifically crafted to facilitate effective objective setting, key result tracking, and overall performance improvement."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "mjgDUguqmV",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "c_gX1ik0kK",
"flavour": "affine:divider",
"props": {},
"children": []
},
{
"type": "block",
"id": "IU3gGmIffA",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "LnERZXy0xj",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "🔔Reminder: Click the "
},
{
"insert": "+New page ",
"attributes": {
"bold": true
}
},
{
"insert": "to start your journey in AFFiNE "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "oi0BxPtiit",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "✏Unleash your creativity and elevate your work efficiency to new heights by customizing templates that perfectly align with your unique needs and objectives."
}
]
}
},
"children": []
},
{
"type": "block",
"id": "DuRxwNJe2w",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
}
]
}
]
}
}

View File

@@ -1,419 +0,0 @@
{
"type": "page",
"meta": {
"id": "rzyBHDgN5KIlEYzB9oBaD",
"title": "Getting Started👇",
"createDate": 1691548231530,
"tags": [
"ZHBa2NtdSo",
"QYFD_HeQc-",
"wg-fBtd2eI"
]
},
"blocks": {
"type": "block",
"id": "1FO1aYcosq",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Getting Started👇"
}
]
}
},
"children": [
{
"type": "block",
"id": "f7o2Osxfa_",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "NDAzEfCZnv",
"flavour": "affine:note",
"props": {
"xywh": "[-12.5684605441112,-52.45959387999099,800,582]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "jArWNRgiLB",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Basic things you should know: "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "TUx2T-R2zT",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Drag "
},
{
"insert": "blocks ",
"attributes": {
"bold": true
}
},
{
"insert": "(put a symbol) at the bottom right of the page to insert a new block, list, database, etc."
}
]
},
"checked": false,
"collapsed": false
},
"children": [
{
"type": "block",
"id": "kyqLH3oNHK",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Hit \""
},
{
"insert": "/",
"attributes": {
"bold": true
}
},
{
"insert": " \"to see all the types of content you can add - headings, videos, link pages, etc."
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "kyQSXZ39ce",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Edit any text, and "
},
{
"insert": "stylish",
"attributes": {
"bold": true
}
},
{
"insert": " "
},
{
"insert": "your",
"attributes": {
"italic": true
}
},
{
"insert": " "
},
{
"insert": "writing",
"attributes": {
"underline": true
}
},
{
"insert": " freely."
}
]
},
"checked": false,
"collapsed": false
},
"children": []
}
]
},
{
"type": "block",
"id": "tKK8W8BwHQ",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Follow "
},
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "9iIScyvuIB_kUKbs1AYOQ"
}
}
},
{
"insert": "knowing how to "
},
{
"insert": "write, draw, and plan all at once",
"attributes": {
"bold": true
}
},
{
"insert": "."
}
]
},
"checked": false,
"collapsed": false
},
"children": [
{
"type": "block",
"id": "APwWQJEqlF",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Not sure where to start with? Refer to"
},
{
"insert": " ",
"attributes": {
"reference": {
"type": "LinkedPage",
"pageId": "-P-O4GSfVGTkI16zJRVa4"
}
}
},
{
"insert": "in your sidebar to get started with pre-built pages."
}
]
},
"checked": false,
"collapsed": false
},
"children": []
}
]
},
{
"type": "block",
"id": "q4UGzfn08o",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Click the drop-down menu "
},
{
"insert": "\"v\" next to the title for more advanced options",
"attributes": {
"bold": true
}
},
{
"insert": ", such as the ability to one-click export your work into PDF/HTML/Markdown formats."
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "yDnnnpnCFh",
"flavour": "affine:list",
"props": {
"type": "todo",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "👉 Encounter"
},
{
"insert": " ",
"attributes": {
"bold": true
}
},
{
"insert": "a question? Click the \""
},
{
"insert": "?\" Button ",
"attributes": {
"bold": true
}
},
{
"insert": "at the bottom right for more guides, or directly send us a message."
}
]
},
"checked": false,
"collapsed": false
},
"children": []
},
{
"type": "block",
"id": "Uq0yIH932N",
"flavour": "affine:paragraph",
"props": {
"type": "h6",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "🎉 Congratulations! You already go through all of this list!"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "Ag86EJnVv0",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Click the"
},
{
"insert": " ",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
},
{
"insert": "+ New Page",
"attributes": {
"background": "var(--affine-text-highlight-grey)",
"bold": true
}
},
{
"insert": " ",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
},
{
"insert": "button at the bottom of your sidebar to start your journey in AFFiNE"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "iQUJlC0oQT",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "wtxc_a6qxN",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "⏰ Kindly Reminder: This page is automatically set up in the"
},
{
"insert": " ",
"attributes": {
"background": "var(--affine-text-highlight-grey)"
}
},
{
"insert": "+New Workspace",
"attributes": {
"background": "var(--affine-text-highlight-grey)",
"bold": true
}
},
{
"insert": ". If you ever find yourself unsure about how to use AFFiNE, simply click on the workspace avatar and select \"Add a New Workspace\" to revisit this page and get the guidance you need."
}
]
}
},
"children": []
}
]
}
]
}
}

View File

@@ -1,191 +0,0 @@
{
"type": "page",
"meta": {
"id": "wbXL4bZcblxLKC6ETqcQ1",
"title": "AFFiNE's Personal project management",
"createDate": 1691564303138,
"tags": []
},
"blocks": {
"type": "block",
"id": "LvRIWQ1EAT",
"flavour": "affine:page",
"props": {
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "AFFiNE's Personal project management"
}
]
}
},
"children": [
{
"type": "block",
"id": "9eBKQxt8ax",
"flavour": "affine:surface",
"props": {
"elements": {}
},
"children": []
},
{
"type": "block",
"id": "2nHeyqzDMf",
"flavour": "affine:note",
"props": {
"xywh": "[0,0,800,361]",
"background": "--affine-background-secondary-color",
"index": "a0",
"hidden": false,
"edgeless": {
"style": {
"borderRadius": 8,
"borderSize": 4,
"borderStyle": "solid",
"shadowType": "--affine-note-shadow-box"
}
}
},
"children": [
{
"type": "block",
"id": "g0M-Toj8iK",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "To-do-list "
}
]
}
},
"children": []
},
{
"type": "block",
"id": "JpcfJa6HqU",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "/"
}
]
}
},
"children": []
},
{
"type": "block",
"id": "w9Oyl2J6bd",
"flavour": "affine:database",
"props": {
"views": [
{
"id": "zp1yuVbPHH",
"name": "table",
"mode": "table",
"columns": [
{
"id": "46SguLuWz3",
"width": 185
},
{
"id": "uSHvJhR2FN",
"width": 200
}
],
"filter": {
"type": "group",
"op": "and",
"conditions": []
}
}
],
"title": {
"$blocksuite:internal:text$": true,
"delta": [
{
"insert": "Database"
}
]
},
"cells": {},
"columns": [
{
"id": "46SguLuWz3",
"type": "title",
"name": "Title",
"data": {}
},
{
"type": "title",
"id": "w9Oyl2J6bd",
"name": "Title",
"data": {}
},
{
"type": "multi-select",
"name": "Tag",
"data": {
"options": []
},
"id": "uSHvJhR2FN"
}
]
},
"children": [
{
"type": "block",
"id": "yP88q8kMa_",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "xo7ok9TmO7",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
},
{
"type": "block",
"id": "dOQULUJfK6",
"flavour": "affine:paragraph",
"props": {
"type": "text",
"text": {
"$blocksuite:internal:text$": true,
"delta": []
}
},
"children": []
}
]
}
]
}
]
}
}

View File

@@ -1,29 +1,11 @@
/* eslint-disable simple-import-sort/imports */
// Auto generated, do not edit manually
import json_0 from './onboarding/wbXL4bZcblxLKC6ETqcQ1.snapshot.json';
import json_1 from './onboarding/rzyBHDgN5KIlEYzB9oBaD.snapshot.json';
import json_2 from './onboarding/nPPyOV0JBNKu5hvW9hE00.snapshot.json';
import json_3 from './onboarding/info.json';
import json_4 from './onboarding/e5cYLNpIRUb-nwpSZGdix.snapshot.json';
import json_5 from './onboarding/b1F2xKjgZ4ISHhXNe1kck.snapshot.json';
import json_6 from './onboarding/Scod6coKmJJ-waH1-jkiW.snapshot.json';
import json_7 from './onboarding/RCpxnWMtBWmUZy5awgJBh.snapshot.json';
import json_8 from './onboarding/GWsRbUtMF4Ee6foVg-H9R.snapshot.json';
import json_9 from './onboarding/9iIScyvuIB_kUKbs1AYOQ.snapshot.json';
import json_10 from './onboarding/0nee_XzkrN23Xy7HMoXL-.snapshot.json';
import json_11 from './onboarding/-P-O4GSfVGTkI16zJRVa4.snapshot.json';
import json_0 from './onboarding/info.json';
import json_1 from './onboarding/blob.json';
import json_2 from './onboarding/W-d9_llZ6rE-qoTiHKTk4.snapshot.json';
export const onboarding = {
'wbXL4bZcblxLKC6ETqcQ1.snapshot.json': json_0,
'rzyBHDgN5KIlEYzB9oBaD.snapshot.json': json_1,
'nPPyOV0JBNKu5hvW9hE00.snapshot.json': json_2,
'info.json': json_3,
'e5cYLNpIRUb-nwpSZGdix.snapshot.json': json_4,
'b1F2xKjgZ4ISHhXNe1kck.snapshot.json': json_5,
'Scod6coKmJJ-waH1-jkiW.snapshot.json': json_6,
'RCpxnWMtBWmUZy5awgJBh.snapshot.json': json_7,
'GWsRbUtMF4Ee6foVg-H9R.snapshot.json': json_8,
'9iIScyvuIB_kUKbs1AYOQ.snapshot.json': json_9,
'0nee_XzkrN23Xy7HMoXL-.snapshot.json': json_10,
'-P-O4GSfVGTkI16zJRVa4.snapshot.json': json_11
'info.json': json_0,
'blob.json': json_1,
'W-d9_llZ6rE-qoTiHKTk4.snapshot.json': json_2
}

View File

@@ -19,6 +19,13 @@ export function createSQLiteStorage(workspaceId: string): SyncStorage {
);
if (update) {
if (
update.byteLength === 0 ||
(update.byteLength === 2 && update[0] === 0 && update[1] === 0)
) {
return null;
}
return {
data: update,
state: encodeStateVectorFromUpdate(update),

View File

@@ -50,13 +50,12 @@ const config: PlaywrightTestConfig = {
DEBUG: 'affine:*',
FORCE_COLOR: 'true',
DEBUG_COLORS: 'true',
ENABLE_LOCAL_EMAIL: process.env.ENABLE_LOCAL_EMAIL ?? 'true',
NEXTAUTH_URL: 'http://localhost:8080',
OAUTH_EMAIL_SENDER: 'noreply@toeverything.info',
OAUTH_EMAIL_LOGIN: 'noreply@toeverything.info',
OAUTH_EMAIL_PASSWORD: 'affine',
STRIPE_API_KEY: '1',
STRIPE_WEBHOOK_KEY: '1',
MAILER_HOST: '0.0.0.0',
MAILER_PORT: '1025',
MAILER_SENDER: 'noreply@toeverything.info',
MAILER_USER: 'noreply@toeverything.info',
MAILER_PASSWORD: 'affine',
},
},
],

View File

@@ -45,9 +45,8 @@ const config: PlaywrightTestConfig = {
DEBUG: 'affine:*',
FORCE_COLOR: 'true',
DEBUG_COLORS: 'true',
ENABLE_LOCAL_EMAIL: process.env.ENABLE_LOCAL_EMAIL ?? 'true',
NEXTAUTH_URL: 'http://localhost:8080',
OAUTH_EMAIL_SENDER: 'noreply@toeverything.info',
MAILER_SENDER: 'noreply@toeverything.info',
},
},
],

View File

@@ -709,8 +709,8 @@ __metadata:
socket.io: "npm:^4.7.2"
stripe: "npm:^14.5.0"
supertest: "npm:^6.3.3"
ts-node: "npm:^10.9.1"
typescript: "npm:^5.3.2"
ts-node: "npm:^10.9.2"
typescript: "npm:^5.3.3"
ws: "npm:^8.14.2"
yjs: "npm:^13.6.10"
zod: "npm:^3.22.4"
@@ -34173,7 +34173,7 @@ __metadata:
languageName: node
linkType: hard
"ts-node@npm:10.9.1, ts-node@npm:^10.9.1":
"ts-node@npm:10.9.1":
version: 10.9.1
resolution: "ts-node@npm:10.9.1"
dependencies:
@@ -34211,6 +34211,44 @@ __metadata:
languageName: node
linkType: hard
"ts-node@npm:^10.9.1, ts-node@npm:^10.9.2":
version: 10.9.2
resolution: "ts-node@npm:10.9.2"
dependencies:
"@cspotcode/source-map-support": "npm:^0.8.0"
"@tsconfig/node10": "npm:^1.0.7"
"@tsconfig/node12": "npm:^1.0.7"
"@tsconfig/node14": "npm:^1.0.0"
"@tsconfig/node16": "npm:^1.0.2"
acorn: "npm:^8.4.1"
acorn-walk: "npm:^8.1.1"
arg: "npm:^4.1.0"
create-require: "npm:^1.1.0"
diff: "npm:^4.0.1"
make-error: "npm:^1.1.1"
v8-compile-cache-lib: "npm:^3.0.1"
yn: "npm:3.1.1"
peerDependencies:
"@swc/core": ">=1.2.50"
"@swc/wasm": ">=1.2.50"
"@types/node": "*"
typescript: ">=2.7"
peerDependenciesMeta:
"@swc/core":
optional: true
"@swc/wasm":
optional: true
bin:
ts-node: dist/bin.js
ts-node-cwd: dist/bin-cwd.js
ts-node-esm: dist/bin-esm.js
ts-node-script: dist/bin-script.js
ts-node-transpile-only: dist/bin-transpile.js
ts-script: dist/bin-script-deprecated.js
checksum: a91a15b3c9f76ac462f006fa88b6bfa528130dcfb849dd7ef7f9d640832ab681e235b8a2bc58ecde42f72851cc1d5d4e22c901b0c11aa51001ea1d395074b794
languageName: node
linkType: hard
"tsconfck@npm:^2.1.0":
version: 2.1.2
resolution: "tsconfck@npm:2.1.2"
@@ -34392,7 +34430,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:5.3.3, typescript@npm:^5.2.2, typescript@npm:^5.3.2":
"typescript@npm:5.3.3, typescript@npm:^5.2.2, typescript@npm:^5.3.2, typescript@npm:^5.3.3":
version: 5.3.3
resolution: "typescript@npm:5.3.3"
bin:
@@ -34402,7 +34440,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.2.2#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.3.2#optional!builtin<compat/typescript>":
"typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.2.2#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.3.2#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin<compat/typescript>":
version: 5.3.3
resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>::version=5.3.3&hash=e012d7"
bin: