Compare commits

...

341 Commits

Author SHA1 Message Date
Alex Yang
77efcad89d v0.10.0-canary.1 2023-10-16 16:30:17 -05:00
Alex Yang
1d06114f00 build(y-indexeddb): fix output package.json (#4640) 2023-10-16 16:25:09 -05:00
dependabot[bot]
579fa1ae4c chore: bump @faker-js/faker from 8.1.0 to 8.2.0 (#4631)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 19:23:30 +00:00
Peng Xiao
fea6b81a53 fix(electron): app image icon (#4442) 2023-10-16 15:54:28 +00:00
Alex Yang
7911d67439 chore: bump version (#4604) 2023-10-16 12:47:06 +00:00
JimmFly
efca651429 feat(core): add history shortcut (#4595) 2023-10-16 12:27:06 +00:00
JimmFly
07b5d18441 feat(core): add sign out confirm modal (#4592) 2023-10-16 08:44:09 +00:00
electron97
c1d386d932 fix: disabled form item in the settings can still be operated via keyboard (#4605) 2023-10-16 05:14:11 +00:00
Alex Yang
710a2f2c97 chore: bump version 2023-10-13 15:40:07 -05:00
JimmFly
2e1e486bc6 chore: bump playwright version (#4602) 2023-10-13 14:37:18 -05:00
Alex Yang
227017625d test(core): fix flaky (#4597) 2023-10-13 13:46:32 -05:00
Joooye_34
6ea10860b4 refactor(infra): record legacy data to improve testing stability (#4590) 2023-10-13 03:03:42 +00:00
LongYinan
286347420d 0.10.0-canary.0 2023-10-12 15:21:07 +08:00
JimmFly
daa976ca62 fix(component): adjust dialog and input style (#4566) 2023-10-12 05:49:39 +00:00
Alex Yang
d05897b724 chore: prohibit unnecessary await (#4586) 2023-10-12 05:04:58 +00:00
JimmFly
5ebd82dc04 feat(core): adjust share menu style (#4584) 2023-10-12 03:26:13 +00:00
Alex Yang
ae4322b75f chore: bump version (#4587) 2023-10-12 02:46:52 +00:00
Alex Yang
a0e6ff9bd1 test: fix migration (#4588) 2023-10-11 19:30:37 -05:00
Alex Yang
491cd75fe0 fix(infra): create template workspace with isolated nanoid (#4569) 2023-10-11 13:48:30 -05:00
LongYinan
5be5863693 chore: upgrade yarn@3.6.4 (#4585) 2023-10-11 17:23:22 +00:00
LongYinan
23abb97ccb fix: dependabot security issues (#4579) 2023-10-11 07:36:45 +00:00
dependabot[bot]
b09d5f3e18 chore: bump @types/eslint from 8.44.2 to 8.44.3 (#4551)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-11 07:35:41 +00:00
Alex Yang
a731499024 chore: remove unused file (#4576) 2023-10-11 06:05:21 +00:00
Alex Yang
8f5ee9234c test: remove deprecated api usage (#4577) 2023-10-11 06:05:06 +00:00
JimmFly
1f6a105e5c feat(core): add setting commands (#4568)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-10-11 03:31:04 +00:00
LongYinan
b1eb926b7b style: enable no-extraneous-dependencies lint rule (#4575) 2023-10-10 17:51:47 +00:00
Alex Yang
c8d1de3a59 chore: bump version (#4571) 2023-10-10 17:43:41 +00:00
dependabot[bot]
aa7e0dd85b chore: bump @vitest/ui from 0.34.5 to 0.34.6 (#4553)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 03:47:44 +00:00
liuyi
0092a19812 refactor(server): deprecate unstable redis manager (#4567) 2023-10-10 03:23:12 +00:00
dependabot[bot]
4a6cfedc4a chore: bump react-i18next from 13.2.1 to 13.2.2 (#4562)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 03:11:07 +00:00
dependabot[bot]
8c97fd1d28 chore: bump sinon from 16.0.0 to 16.1.0 (#4563)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 03:10:21 +00:00
Peng Xiao
d9fe3e73d5 fix: list page storybook not rendering issue (#4560) 2023-10-09 07:54:05 +00:00
dependabot[bot]
59a4b3bc31 chore: bump electron from 26.2.2 to 26.3.0 (#4564)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 07:14:50 +00:00
Zero King
0161c98d65 chore: reword template galleries introduction (#4548) 2023-10-06 16:11:08 +08:00
dependabot[bot]
d3ffa2c5f2 chore: bump marked from 7.0.5 to 9.0.3 (#4554)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 15:28:38 +00:00
dependabot[bot]
0a6859a1d7 chore: bump esbuild from 0.19.3 to 0.19.4 (#4550)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 15:28:10 +00:00
JimmFly
69db99636b feat(core): add editor commanads (#4514)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-10-02 03:22:12 +00:00
Alex Yang
aab1a1e50a fix(electron): output check (#4547) 2023-09-30 02:02:17 -05:00
Alex Yang
19646a97af fix: twitter preview (#4545) 2023-09-30 01:19:54 -05:00
Qinghao Huang
f59a35d8d2 fix: spacing issue in getting-started template (#4540) 2023-09-30 01:02:43 -05:00
Alex Yang
c911806062 refactor: remove bookmark plugin (#4544) 2023-09-30 00:48:33 -05:00
Qinghao Huang
b440c3a820 docs: update CLA.md (#4541) 2023-09-30 00:26:50 +00:00
Alex Yang
98cabc44e4 ci: remove unstable nx.yml (#4543) 2023-09-29 18:57:31 -05:00
LongYinan
dd94ea5b45 ci: speedup ci by reduce installation packages in certain job (#4457) 2023-09-29 03:02:26 +00:00
Joooye_34
b012e615ba fix(component): content should subtract height of the header (#4507) 2023-09-28 07:04:12 +00:00
Alex Yang
603f82ffc2 refactor: using unified nanoid (#4519) 2023-09-28 02:53:04 +00:00
Alex Yang
56f75160f3 refactor: remove unused packages (#4532) 2023-09-28 01:33:42 +00:00
dependabot[bot]
a860cf8e43 chore: bump electron from 26.1.0 to 26.2.1 (#4527)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-27 10:40:30 +00:00
liuyi
504dda2092 fix(core): setting ui regression (#4525) 2023-09-27 09:56:01 +00:00
JimmFly
1df8b6edfb feat(component): add private copy link button (#4508) 2023-09-27 04:54:02 +00:00
Alex Yang
ddfa5d394d chore: bump version (#4518) 2023-09-27 02:02:54 +00:00
Alex Yang
a69820e4ca fix: type in pluginImportsFunctionMap (#4517) 2023-09-27 01:53:01 +00:00
Peng Xiao
369db3fea5 fix(component): cmdk flaky (#4512) 2023-09-27 01:37:00 +00:00
liuyi
4a03fa65d1 fix(server): wrong member count query (#4506) 2023-09-26 15:36:08 +00:00
Alex Yang
a633fb6dea fix: current page atom (#4515) 2023-09-26 14:53:01 +00:00
JimmFly
1b6cd70247 chore(core): temporarily remove set-syncing-mode (#4489) 2023-09-26 14:11:04 +00:00
Peng Xiao
29fa237dfb fix: storybook previews (#4504) 2023-09-26 05:51:39 +00:00
Alex Yang
61044d91a8 fix(core): page update date (#4502) 2023-09-26 04:09:52 +00:00
Peng Xiao
eb728f7ef2 fix: give content match a lower score (#4499) 2023-09-26 03:15:40 +00:00
JimmFly
1bdc402b7b fix: adjust 404 page style (#4491) 2023-09-26 02:51:58 +00:00
Alex Yang
127a84b4e1 refactor(plugin-cli): use @plugxjs/vite-plugin (#4501) 2023-09-26 02:49:23 +00:00
JimmFly
2e1acec3c0 fix: unexpected pop ups (#4468) 2023-09-26 02:41:45 +00:00
dependabot[bot]
672a01b385 chore: bump sinon from 15.2.0 to 16.0.0 (#4480)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 20:17:25 +00:00
Joooye_34
ad63c5a525 fix(component): background animation is different (#4495) 2023-09-25 16:50:39 +00:00
Alex Yang
3a79346ce0 test: workspace provider (#4497) 2023-09-25 16:49:23 +00:00
dependabot[bot]
bf729df7fe chore: bump vite-tsconfig-paths from 4.2.0 to 4.2.1 (#4481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 16:05:09 +00:00
dependabot[bot]
b785840d91 chore: bump marked-gfm-heading-id from 3.0.6 to 3.1.0 (#4479)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 16:04:47 +00:00
Peng Xiao
e8410b948d chore(component): bump themes (#4484) 2023-09-25 12:03:28 +00:00
Peng Xiao
3f09ba92bc fix: cmdk scrollbar gutter (#4488) 2023-09-25 12:03:02 +00:00
JimmFly
35dc6d6687 fix: unexpected hover behavior of collection sidebar (#4490) 2023-09-25 10:22:59 +00:00
JimmFly
5b4ce75e13 feat: add commands (#4477) 2023-09-25 10:10:53 +00:00
Peng Xiao
dc6b66c32f fix: register command re-rendering (#4476) 2023-09-25 02:40:53 +00:00
LongYinan
5f7f5b74ca fix(core): error state for non early access user while signing in with email (#4467) 2023-09-23 00:00:09 -07:00
LongYinan
7b5157aa89 fix(server): missing dependency in sync app (#4465) 2023-09-22 21:32:45 +00:00
Alex Yang
bd0ed7f474 test: fix flaky (#4463) 2023-09-22 20:18:41 +00:00
Alex Yang
2da6702991 refactor(infra): simplify currentWorkspaceAtom (#4462) 2023-09-22 20:07:26 +00:00
Alex Yang
56d8fa5d29 docs: upload LICENSE.md 2023-09-22 14:38:19 -05:00
Alex Yang
4e5e48ce9f docs: update README.md
There are no core members actually, people is just a paid guy.
2023-09-22 14:31:15 -05:00
Peng Xiao
e0063ebc9b feat: new CMD-K (#4408) 2023-09-22 14:31:26 +00:00
Peng Xiao
27e4599c94 chore: bump components version (#4454) 2023-09-22 08:56:10 +00:00
JimmFly
edd7d00104 refactor: workspace list (#4432) 2023-09-22 15:02:31 +08:00
Alex Yang
092e2e0a3d fix(electron): missing video (#4451) 2023-09-22 05:56:43 +00:00
LongYinan
a6d19abc73 fix(core): bump latest blocksuite fixes (#4450) 2023-09-22 00:00:37 +00:00
Om Raut
cf7a55b3e8 fix: some english words when switched to chinese. (#4448) 2023-09-21 17:45:47 +00:00
Alex Yang
d09f6fb7cc refactor: replace with data source (#4447) 2023-09-21 17:31:17 +00:00
Alex Yang
98f6b3e685 test: workspace passive provider (#4446) 2023-09-21 15:50:14 +00:00
Hongtao Lye
d5f4fbcdb5 fix: providers get disconnected after opening the setting and close it (#4429)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-21 09:22:23 -05:00
DarkSky
1ddae40fb2 feat: add auth support for websocket (#4445) 2023-09-21 13:05:26 +00:00
Peng Xiao
872dc3521b fix: allow file protocol streaming (#4441) 2023-09-21 19:10:03 +08:00
Alex Yang
2521f2ed12 v0.9.0-canary.13 2023-09-21 01:02:19 -05:00
Alex Yang
dc95b1ded5 chore: bump version (#4434) 2023-09-21 01:01:46 -05:00
dependabot[bot]
f4e49bf0ff chore: bump vitest-mock-extended from 1.2.0 to 1.2.1 (#4397)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-20 18:52:12 +00:00
Alex Yang
e55fc3b74d docs: update LICENSE 2023-09-20 13:36:04 -05:00
Joooye_34
73f83cc97e fix(core): editor popover covered header popover (#4342) 2023-09-20 14:07:15 +00:00
Peng Xiao
6e79858f41 fix: add prompt select_account for google login (#4415) 2023-09-20 03:22:43 +00:00
Joooye_34
e2764179bc ci(storybook): fix import page failed problem (#4424) 2023-09-20 01:59:03 +00:00
Alex Yang
9b449e9a28 chore: bump version (#4421) 2023-09-19 15:16:05 -05:00
dependabot[bot]
9ed32e6bec chore: bump @types/cookie-parser from 1.4.3 to 1.4.4 (#4398)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-19 17:56:15 +00:00
JimmFly
38417878fc chore: add back&forward for web​ (#4403) 2023-09-19 17:49:39 +00:00
Joooye_34
f395a955a2 ci(storybook): add production file of deps to be inputs of storybook (#4414) 2023-09-18 23:53:12 -05:00
Alex Yang
9aafaf865e test(electron): fix cloud test (#4411) 2023-09-19 02:08:35 +00:00
dependabot[bot]
63bd1fbf02 chore: bump ky from 0.33.3 to 1.0.1 (#4399)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 19:20:13 +00:00
X1a0t
feb3e64839 feat: apply higher priority for doc request (#4401) 2023-09-18 19:19:56 +00:00
DarkSky
65bb30558e fix: read permission for subpage (#4405) 2023-09-18 16:13:36 +08:00
Alex Yang
cdad7edf15 test(electron): add cloud test (#4184)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-09-17 20:26:06 +00:00
Alex Yang
40e094dcdd test: fix e2e (#4390) 2023-09-17 03:50:26 +00:00
Alex Yang
bebe69d483 chore: fix tsconfig (#4389) 2023-09-16 14:40:33 -07:00
Qi
26877ffd52 feat: modify 404 page (#4383) 2023-09-15 17:57:08 -07:00
LongYinan
70e731e066 test(server): move env variables into playwright config (#4384) 2023-09-15 17:56:26 -07:00
Qi
acecb4bf69 fix: error style of setting sidebar (#4368) 2023-09-15 17:56:08 -07:00
JimmFly
d3635208f6 refactor: delete page style (#4347)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-15 17:55:56 -07:00
Peng Xiao
b9656b1e22 fix: orderby in members list (#4375)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2023-09-15 19:24:00 +00:00
Qi
4577fb7e1a fix: remove useRef in menu & tooltip (#4369) 2023-09-15 12:29:55 -07:00
Alex Yang
c41c3c81c8 fix(core): local workspace collections (#4378) 2023-09-15 02:19:22 -07:00
Peng Xiao
eea38a08c5 fix: add platform selector to storybook (#4380) 2023-09-15 08:59:29 +00:00
Chi Zhang
f730f2b242 docs: update licenses (#4180)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-15 07:50:00 +00:00
LongYinan
1aec1ce7d0 test(server): move tests out of src folder (#4366) 2023-09-15 07:34:14 +00:00
Peng Xiao
b5e8fecfd0 fix: update windows install gif (#4379) 2023-09-15 15:07:58 +08:00
X1a0t
7d1b66c13f fix: add missing static file list (#4374) 2023-09-14 23:44:10 -07:00
JimmFly
4ad0e2ffd6 chore: adjust workspace card style (#4371) 2023-09-14 23:19:38 -07:00
Qi
d5c41d29af fix: pagation items are not easy to trigger (#4372) 2023-09-14 23:15:21 -07:00
LongYinan
a9b6529bcb fix(server): workspace memebers sort (#4370) 2023-09-14 23:08:09 -07:00
Qi
d550804cf5 fix: error style of quick search modal (#4359) 2023-09-15 02:40:22 +00:00
LongYinan
d68545cb29 chore(i18n): fix sync languages script (#4367) 2023-09-14 17:37:32 -07:00
Alex Yang
d01203daad chore: bump version (#4352) 2023-09-14 14:42:06 -07:00
3720
5418d3048e feat: hide page info in public pages (#4365) 2023-09-14 18:43:55 +00:00
Qi
465098cc9a feat: support remove user & workspace avatar (#4302) 2023-09-14 14:35:05 +00:00
X1a0t
e1a330a0a6 fix: use cdn api when querying static CDN files (#4361)
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2023-09-14 16:56:55 +08:00
X1a0t
f79cd76cec fix: should return null when getting blob fails (#4360) 2023-09-14 16:55:57 +08:00
LongYinan
aa3b97b056 style: imporve tsconfig (#4358) 2023-09-14 01:47:15 -07:00
Alex Yang
8fb0620bc8 chore: bump version (#4354) 2023-09-14 06:33:47 +00:00
LongYinan
f0de34a60b fix(server): everyone can share page in workspace (#4357) 2023-09-14 06:26:41 +00:00
Qi
1016a47b51 fix: error invite email title (#4355) 2023-09-13 23:20:51 -07:00
Peng Xiao
1d77b2c9da docs: add readme for developing @affine/server (#4351) 2023-09-14 02:58:13 +00:00
fourdim
10a3a05e70 fix: incomplete URL substring sanitization (#4309)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-09-13 19:38:48 -07:00
Peng Xiao
b023c79d5c fix: test hang (#4349) 2023-09-14 02:13:36 +00:00
Alex Yang
09510479e2 v0.9.0-canary.12 2023-09-13 17:10:43 -07:00
Qi
0be142e4e2 feat: add verify process in change email progress (#4306)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-09-13 16:54:02 +00:00
Qi
0b1ba6bf43 feat: replace modal with new design (#4324)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-09-13 16:05:19 +08:00
Alex Yang
49e0172316 chore(server): ignore build test files (#4337) 2023-09-13 06:41:17 +00:00
Alex Yang
74faee887e refactor: move mailer test (#4328) 2023-09-13 05:11:19 +00:00
JimmFly
bdc2695caf fix: wrong share status display (#4336) 2023-09-13 05:11:05 +00:00
Qi
32ee946670 feat: add tooltip in user & workspace setting (#4260) 2023-09-13 04:37:25 +00:00
Alex Yang
5e0fd0b839 chore: add circular check (#4334) 2023-09-13 04:14:38 +00:00
Peng Xiao
ab7be0b940 fix: remove open app timeout (#4332) 2023-09-13 05:08:44 +00:00
Alex Yang
37e5c464d6 refactor(core): move notification center top level (#4331) 2023-09-13 04:01:31 +00:00
Joooye_34
f445604e5e fix(core): back home botton has no reaction (#4318)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-13 03:19:00 +00:00
Peng Xiao
923148d841 fix: force syncing DB when export db (#4312) 2023-09-13 03:13:38 +00:00
Peng Xiao
9ec2b9cf51 fix: signout account when logging through oauth signin via desktop (#4321) 2023-09-13 02:10:03 +00:00
JimmFly
7d6c150ecd fix: unexpected react warning (#4316) 2023-09-12 22:32:45 +00:00
JimmFly
a94512a3fb feat: add animation for add favorites (#4317) 2023-09-12 16:06:03 +00:00
Peng Xiao
9e5213f074 fix: potential issue that may push whole window up a bit (#4311) 2023-09-12 06:45:41 +00:00
Alex Yang
944a128b53 chore: bump version (#4310) 2023-09-12 05:44:00 +00:00
Peng Xiao
fc76163dd1 fix: get auth token for development (#4295) 2023-09-12 13:31:58 +08:00
Qi
98429bf89e feat: support pagination for member list (#4231) 2023-09-12 03:37:59 +00:00
dependabot[bot]
9fe9efe465 chore: bump nestjs-throttler-storage-redis from 0.3.3 to 0.4.0 (#4299)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 03:36:21 +00:00
Peng Xiao
ea2196b039 fix: circular dependencies (#4307) 2023-09-11 17:00:48 +00:00
Peng Xiao
892cae5599 fix: SHOULD_REPORT_TRACE condition (#4273)
Co-authored-by: X1a0t <405028157@qq.com>
2023-09-11 09:47:02 +00:00
X1a0t
a25a31c105 test: always exec afterEach in ava test (#4303)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-09-11 09:30:39 +00:00
JimmFly
49c8928a09 chore: adjust preloading page (#4225) 2023-09-11 06:42:45 +00:00
JimmFly
b7b14c2241 fix: menu style (#4287) 2023-09-11 06:41:36 +00:00
Peng Xiao
46fd732ee6 fix: cookie name (#4293) 2023-09-08 23:42:09 -07:00
Alex Yang
2db0cec443 chore: bump version (#4294) 2023-09-09 04:28:17 +00:00
Alex Yang
162016884c refactor: remove unused package (#4291) 2023-09-09 00:00:26 +00:00
Peng Xiao
e00f40537b fix: allow login with credentials on production (#4288) 2023-09-08 23:02:01 +00:00
Whitewater
56e653140b fix: sort plugin list in test (#4289) 2023-09-08 22:36:48 +00:00
Alex Yang
5f0605a5d9 feat: page view storage with cloud support (#4238) 2023-09-08 22:02:22 +00:00
LongYinan
58a935b31d test(server): make testing more isolated (#4290) 2023-09-08 13:02:27 -07:00
DarkSky
a97fd486c3 feat: keep the multiline log in single log (#4281) 2023-09-08 17:33:46 +08:00
X1a0t
157ce7ac69 fix: edge case when upgrading page (#4283) 2023-09-08 07:43:18 +00:00
Qi
f9eea85577 fix: can not enable workspace if not sign in (#4265)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-08 07:41:07 +00:00
Qi
81a3bcee4f fix: modify back text (#4257) 2023-09-08 07:37:01 +00:00
LongYinan
05c27ed164 fix(core): temporary remove blockVersions assertion (#4285) 2023-09-08 00:45:07 -07:00
DarkSky
b261d97ed2 fix: revoke permission if failed to send email (#4279) 2023-09-07 23:21:38 -07:00
LongYinan
538c150950 fix(electron): api url mapping in electron (#4276) 2023-09-07 22:36:56 -07:00
LongYinan
aa025b0d46 fix(server): storage usage should be float rather than int (#4275) 2023-09-08 05:15:33 +00:00
Alex Yang
0bc3d9ebf5 build: fix internal release 2023-09-07 19:44:06 -07:00
Alex Yang
0ee0790295 ci: workflow dispatch 2023-09-07 16:52:10 -07:00
Alex Yang
211e960d58 chore: bump version (#4272) 2023-09-07 23:21:47 +00:00
Alex Yang
544dcde4c5 v0.9.0-canary.11 2023-09-07 15:22:44 -07:00
DarkSky
f4340da478 refactor(server): rate limit and permission (#4198)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2023-09-07 14:32:41 -07:00
Alex Yang
4c4bb65be8 fix(server): flaky test (#4271) 2023-09-07 20:55:04 +00:00
Peng Xiao
bf3a3379ae fix: disable set-db-location step (#4263) 2023-09-07 17:41:27 +00:00
Peng Xiao
d54f027f50 fix: sigin in different window may not refresh workspace list (#4270)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-07 17:30:21 +00:00
Qi
719917d445 fix: incorrect invitation email style (#4269) 2023-09-07 17:17:22 +00:00
Joooye_34
96545c97d0 feat(core): use double click to activate title renaming (#4214) 2023-09-07 17:12:07 +00:00
Peng Xiao
185546b8ea fix: logout 404 (#4253)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-07 16:14:02 +00:00
Qi
fcf041024f fix: incorrect toast after signed in (#4268) 2023-09-07 15:45:31 +00:00
Garfield Lee
4dbc5412a5 feat: add new locale key for not found page back home button (#4266) 2023-09-07 15:45:22 +00:00
Qi
4fe90ea3b3 fix: wrong content in invitation email (#4258) 2023-09-07 15:36:53 +00:00
Peng Xiao
0c4277e5b9 fix: better transition (#4267) 2023-09-07 15:20:48 +00:00
Peng Xiao
2813ad36b8 fix: disable simutanous updater download (#4254) 2023-09-07 06:54:51 +00:00
Alex Yang
0de6b748bb chore(i18n): fix nx config (#4249) 2023-09-07 06:54:28 +00:00
Alex Yang
a766279867 test: enhance 0.8.3 migration test (#4251) 2023-09-07 06:03:31 +00:00
Qi
498683ff4c feat: send email to owner after member accepted invitation / leave workspace (#4152)
Co-authored-by: DarkSky <darksky2048@gmail.com>
2023-09-07 05:08:23 +00:00
Peng Xiao
1301605db5 fix: workspace list dnd issues (#4219)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-07 05:06:03 +00:00
JimmFly
0992841673 style: add hover style to the setting button in workspace card (#4193) 2023-09-07 05:05:38 +00:00
Qi
f789e366f2 fix: wrong text in change password page (#4244)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-07 05:02:33 +00:00
Alex Yang
57c5f6cd2c fix(core): display user name dynamically (#4248) 2023-09-07 05:02:21 +00:00
Alex Yang
4d96d2dd02 feat(core): add share page error boundary (#4245)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-09-07 04:42:47 +00:00
Qi
10f2e9300a fix: wrong copywriting in change email modal (#4243) 2023-09-07 04:28:49 +00:00
Qi
798238df4d fix: can not close menu in all page (#4232)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-07 04:26:16 +00:00
Alex Yang
1d1fb6ca31 feat(core): await sync doc (#4247) 2023-09-07 03:54:39 +00:00
Garfield Lee
8656530049 fix: correct sign out display name (#4211) 2023-09-07 03:46:58 +00:00
Alex Yang
fc7919e172 v0.9.0-canary.10 2023-09-06 16:11:57 -07:00
Alex Yang
075986b2e3 fix(core): upload binary after migration (#4241) 2023-09-06 22:38:11 +00:00
Alex Yang
685db33b25 fix(core): data migration incorrect in cloud 2023-09-06 14:26:12 -07:00
Alex Yang
90013780e0 fix: display user on selection 2023-09-06 11:48:21 -07:00
LongYinan
97c201836f chore(server): redirect /api/auth/signin (#4237) 2023-09-06 11:11:57 -07:00
Qi
dd75062da7 feat: modify workspace setting description (#4234) 2023-09-06 10:16:24 -07:00
LongYinan
b8e3e4994e fix(core): location.state maybe null (#4236) 2023-09-06 10:15:46 -07:00
LongYinan
264d33823b chore(server): add log to early access redirect (#4235) 2023-09-06 10:11:31 -07:00
X1a0t
22d3411e6e fix: logger filter should pass graphql context (#4209) 2023-09-06 08:16:38 -07:00
Alex Yang
1b6a78cd00 feat!: unified migration logic in server electron, and browser (#4079)
Co-authored-by: Mirone <Saul-Mirone@outlook.com>
2023-09-06 07:44:53 +00:00
Garfield Lee
925c18300f refactor: change locale key (#3838) 2023-09-06 07:20:44 +00:00
Alex Yang
b3d902e33f fix(core): sign out jump to 404 (#4204) 2023-09-06 06:58:05 +00:00
Qi
d8c9f10bc1 feat: replace menu with new design (#4012)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-06 04:36:43 +00:00
X1a0t
ef3d3a34e2 feat: auth metric and trace (#4063) 2023-09-05 21:20:06 -07:00
Om Raut
d29514c995 fix: notification text cannot be selected when there is a modal (#4157) 2023-09-05 22:00:34 +00:00
Alex Yang
f5c2aaf28a v0.9.0-canary.9 2023-09-05 14:58:09 -07:00
Alex Yang
d2484d0b8d fix(core): remove write version (#4202) 2023-09-05 19:22:58 +00:00
LongYinan
6af7aefe00 fix(core): location.state maybe null (#4203) 2023-09-05 19:21:28 +00:00
Qi
ff808881ee fix: can not scroll in mermber list (#4182) 2023-09-05 18:17:17 +00:00
Peng Xiao
1dc94277c2 fix: use database session cookie for production (#4200) 2023-09-05 17:30:50 +00:00
Peng Xiao
8407b2dd7c fix: force reset callbackurl cookie in electron (#4199) 2023-09-05 12:54:17 +00:00
Yifeng Wang
d7838633e6 fix: preloading typo (#4197) 2023-09-05 10:18:28 +00:00
Peng Xiao
bf40d41c86 fix: cloud workspace sometimes converted to local workspace (#4194) 2023-09-05 09:53:22 +00:00
Peng Xiao
8b4f2a6c50 fix: sidebar ui fix (#4187) 2023-09-05 09:48:48 +00:00
LongYinan
dfa5fefa7f test(server): use mock PrismaService in tests (#4101) 2023-09-05 01:01:45 -07:00
LongYinan
3d5e1d2f3d chore(server): debug blob sizes limit (#4181) 2023-09-04 23:16:01 -07:00
Chi Zhang
14ae3cf6d6 docs: update BUG-REPORT.yml (#4174) 2023-09-05 02:45:59 +00:00
LongYinan
2da22e2fc2 chore(server): debug blob sizes limit (#4178) 2023-09-04 19:44:22 -07:00
LongYinan
ff66e2635a chore: upgrade yarn to 3.6.3 (#4173) 2023-09-05 01:37:38 +00:00
LongYinan
801d769877 fix(server): storage usage calculation issue (#4170) 2023-09-05 00:56:47 +00:00
Peng Xiao
d57f995e87 feat: add fav button (#4159)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-04 17:14:02 +00:00
dependabot[bot]
6609f712e7 chore: bump @toeverything/theme from 0.7.13 to 0.7.15 (#4146)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 17:04:03 +00:00
dependabot[bot]
3907802e09 chore: bump @electron/remote from 2.0.10 to 2.0.11 (#4147)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 17:03:27 +00:00
Peng Xiao
5288645e33 fix: header ui styles (#4156) 2023-09-04 10:13:20 +00:00
DarkSky
e72a10153e feat: enable https in production (#4154)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-09-04 17:20:11 +08:00
Peng Xiao
1066a8ca74 fix: style fixes to windows app control buttons (#4150) 2023-09-04 09:01:31 +00:00
Peng Xiao
f245b4f5ce fix: api url for electron (#4142) 2023-09-04 07:43:39 +00:00
X1a0t
ed76417ba3 feat: log all exceptions In HTTP context (#4149) 2023-09-04 07:42:55 +00:00
DarkSky
80195ca2a0 feat: remove yrs (#4153) 2023-09-04 07:31:00 +00:00
X1a0t
5b7d076a90 fix: add keyv types (#4148) 2023-09-04 05:24:51 +00:00
3720
e60e2236ea fix: filter of tags does not work (#4138) 2023-09-03 15:50:21 +00:00
DarkSky
2e97079a5e fix: server deps (#4140) 2023-09-03 14:33:56 +00:00
LongYinan
3211451b21 fix(server): missing nanoid dependency (#4139) 2023-09-03 14:30:00 +00:00
Alex Yang
d0e8a1f0df refactor(infra): move initEmptyPage (#4135) 2023-09-03 05:19:43 +00:00
TinsFox
c3cca85184 docs(i18n): update i18n usage (#4129)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-02 18:18:48 +00:00
Alex Yang
a2623e1352 test: email sending e2e (#4130) 2023-09-02 18:13:59 +00:00
Alex Yang
3d8a91aab0 docs: update README.md 2023-09-02 01:33:03 -05:00
Alex Yang
9f0e67a673 v0.9.0-canary.8 2023-09-02 01:22:50 -05:00
Qi
138aaed05d feat: add a reminder for early access in the invitation email (#4097)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-09-02 06:07:49 +00:00
Qi
3edfc46307 feat: optimize sign in experience (#4099) 2023-09-02 06:07:38 +00:00
Alex Yang
be9ae57a8e test: cover basic collaborative (#4127) 2023-09-02 01:06:47 -05:00
Alex Yang
4f97ea8a5d test: cover share page e2e (#4126) 2023-09-02 00:57:04 -05:00
Alex Yang
8825678ca9 test: add name change test (#4125) 2023-09-02 00:11:10 -05:00
Alex Yang
70b5a9deeb test: improve data migration suite (#4124) 2023-09-02 03:31:07 +00:00
Alex Yang
eb1a21265f refactor(server): use ava (#4120) 2023-09-01 19:41:29 +00:00
JimmFly
8845bb9b4b chore: optimized style (#4098) 2023-09-01 19:28:16 +00:00
Whitewater
189e91e6ca fix(core): sort tags by count (#4122) 2023-09-01 19:13:33 +00:00
Qi
442d06fc69 fix: error invitation url (#4110) 2023-09-01 18:22:26 +00:00
Peng Xiao
c9c76983de fix: cookie issues in Electron (#4115) 2023-09-01 17:34:08 +00:00
DarkSky
3c4f45bcb6 feat: add user info edit verify (#4117) 2023-09-01 16:59:33 +00:00
LongYinan
db3a6efaf3 build(core): fix non-canary assets bucket (#4116) 2023-09-02 00:32:11 +08:00
DarkSky
7d3b1ad2b9 feat: add user level blob quota (#4114) 2023-09-01 12:56:27 +00:00
LongYinan
e76cdf4d71 fix(server): set right AFFINE_SERVER_HOST env variable (#4108) 2023-09-01 18:37:48 +08:00
LongYinan
18ac355df3 chore(server): change the log level (#4106) 2023-09-01 18:37:31 +08:00
X1a0t
c0bf82d3ff fix: beta serverUrlPrefix (#4103) 2023-09-01 04:27:12 -05:00
Qi
a1f4cbc568 fix: error in @toeverything/components (#4102) 2023-09-01 09:00:37 +00:00
Yifeng Wang
10c609348f fix: preload typo (#4096) 2023-09-01 06:59:06 +00:00
Alex Yang
88f94d5b61 test(server): run test in single thread (#4095) 2023-09-01 01:25:18 -05:00
Alex Yang
92f0b31196 feat: support force sync by click (#4089)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-09-01 01:15:07 -05:00
LongYinan
83e7e9db8d fix(server): relax the rate limits (#4092) 2023-09-01 13:51:37 +08:00
LongYinan
3f21b0b45d fix(server): redirect logic in earlyAccessPreview (#4091) 2023-09-01 13:51:20 +08:00
Peng Xiao
d4a2b3f4d1 fix: not be able to login with Google in desktop (#4093) 2023-08-31 23:34:55 -05:00
X1a0t
d4a83c1c6f feat: exception logger (#4059) 2023-09-01 12:05:35 +08:00
Peng Xiao
b0024080bd fix: add back sourcemaps to electron build (#4090) 2023-09-01 03:34:18 +00:00
Alex Yang
c937b88978 test(server): fix flaky (#4088) 2023-09-01 01:03:46 +00:00
Ricardo Emanuel
0f2223ddf0 docs: fixed typo in README.md of the root (#4049) 2023-08-31 19:08:56 -05:00
Alex Yang
364fc517cc docs: update BUILDING.md (#4087) 2023-08-31 18:50:54 -05:00
Alex Yang
25671e2134 chore: bump version (#4083) 2023-08-31 18:50:03 -05:00
Alex Yang
1e30a3c7fe fix(core): forwardRef in count down render (#4086) 2023-08-31 18:37:56 -05:00
Alex Yang
06d5ecd597 docs: update indexeddb document (#4084) 2023-08-31 17:16:27 -05:00
KaranPant
b18596fc57 fix: show border around pagetitle when renaming (#4080) 2023-08-31 17:06:58 -05:00
Alex Yang
7082937b62 refactor(workspace): sync doc update in background using data source (#4081) 2023-08-31 16:20:57 -05:00
Pranay Prajapati
4091ff8e36 fix: corrected the preposition in "Save As New Collection" (#4070) 2023-08-31 13:35:10 -05:00
Alex Yang
0fa1bdf7d2 v0.9.0-canary.7 2023-08-31 13:18:25 -05:00
JimmFly
df4d71b0c8 feat: add worksapce type label (#4045)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-31 13:16:58 -05:00
Priyansh Gupta
18d5a99af5 feat(core): added code to handle keyboard inputs (#4006)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-31 13:15:55 -05:00
JimmFly
6be176b4e3 fix: the web version should not display client borders (#4040)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-31 13:15:22 -05:00
Alex Yang
97a0969583 fix(core): skip background syncing in the web (#4077) 2023-08-31 12:59:34 -05:00
Peng Xiao
a2e4ef904b refactor: remove hacky email login (#4075)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-31 12:49:22 -05:00
Qi
f99a7a5ecd fix: shortcut key style (#4072) 2023-08-31 08:08:10 -05:00
Alex Yang
f21426d23d fix(core): blockVersions check (#4073) 2023-08-31 08:07:21 -05:00
Qi
3f5e649295 fix: sign in issues (#4047)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-31 08:07:05 -05:00
Peng Xiao
13857d59dc fix: some style issues to sidebar and switch (#4046)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-31 20:46:06 +08:00
Qi
260c25acf3 feat: add storage panel in setting (#4069) 2023-08-31 07:36:05 -05:00
DarkSky
4ef1425299 feat: rate limiter (#4011) 2023-08-31 20:29:25 +08:00
Peng Xiao
8e48255ef8 fix: userinfo title (#4068) 2023-08-31 09:46:26 +00:00
liuyi
e10868cd20 fix(server): deal with unexpected updates (#4064) 2023-08-31 16:56:33 +08:00
LongYinan
9bffe3cf24 fix(server): do not override auth.privateKey (#4065) 2023-08-31 16:44:37 +08:00
DarkSky
0add43f8db feat: blob size api (#4060) 2023-08-31 16:39:19 +08:00
LongYinan
cc00da9325 chore(server): enable earlyAccessPreview for canary (#4061) 2023-08-31 14:41:43 +08:00
Alex Yang
49d203ac57 v0.9.0-canary.6 2023-08-31 00:48:00 -05:00
Alex Yang
55b3182799 feat(core): support syncing workspaces and blobs in the background (#4057) 2023-08-31 00:40:34 -05:00
Peng Xiao
4e45554585 feat: support google login on desktop (#4053)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-31 12:51:49 +08:00
X1a0t
ba735d8b57 chore: bump octobase (#4042) 2023-08-30 21:06:50 -05:00
Alex Yang
517f4afb31 fix(core): refresh metadata after refresh (#4054) 2023-08-30 21:22:34 +00:00
X1a0t
441e706746 fix: flaky unit test should be able to timer (#4043) 2023-08-30 13:58:25 -05:00
Alex Yang
7c4e65a5be ci: use 'pull_request' on publish-storybook.yml (#4051) 2023-08-30 11:09:04 -05:00
Alex Yang
e042152681 ci: update chromatic build (#4050) 2023-08-30 10:55:28 -05:00
Alex Yang
2e042e03b2 v0.9.0-canary.5 2023-08-30 09:51:35 -05:00
Peng Xiao
d6c0e67bf0 fix: electron white screen (#4048) 2023-08-30 09:41:06 -05:00
Alex Yang
e75ff52ec1 v0.9.0-canary.4 2023-08-30 00:13:22 -05:00
Alex Yang
00e7cf9a06 fix(core): incorrect blocksuite data format (#4039) 2023-08-30 00:04:16 -05:00
Peng Xiao
82f8ac50de fix: replace dmg bg (#4038) 2023-08-29 23:30:45 -05:00
Alex Yang
880375a6d1 chore: bump version (#4028) 2023-08-29 23:30:15 -05:00
Alex Yang
02bd9fc2d1 fix(core): find lost data (#4035) 2023-08-29 23:30:03 -05:00
Peng Xiao
cbb5b6e4a5 fix: crash on close (#4033) 2023-08-30 03:06:43 +00:00
DarkSky
d3bd369420 chore: add bump octobase script (#3931) 2023-08-29 19:37:12 -05:00
Alex Yang
4aabe2ea5e refactor(core): use element atom (#4026) 2023-08-29 18:59:39 -05:00
Alex Yang
591bfc3320 v0.9.0-canary.3 2023-08-29 18:21:49 -05:00
Alex Yang
60bdeebcc6 chore: bump version (#4025) 2023-08-29 18:21:18 -05:00
Peng Xiao
800f3c3cb6 feat: add open app route (#3899) 2023-08-29 22:40:25 +00:00
Alex Yang
71b195d9a9 feat(server): add compatibility field (#4022) 2023-08-29 21:30:59 +00:00
pdx
e3b105f1cd feat(core): use enter change workspace the name (#4007)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-29 21:20:30 +00:00
Alex Yang
5c0f73c535 ci: split server test (#4023) 2023-08-29 15:57:09 -05:00
Peng Xiao
849e225a2d fix: disable auto updater on dev (#4019) 2023-08-29 14:04:27 -05:00
Alex Yang
02026e0bb6 fix(server): invite user type might be null (#4020) 2023-08-29 14:04:12 -05:00
Alex Yang
12aef95ba2 chore: downgrade swr to 2.2.0 2023-08-29 12:07:15 -05:00
Alex Yang
6b206e59a5 v0.9.0-canary.2 2023-08-29 11:53:49 -05:00
Peng Xiao
7b0e5b89ed fix: electron whitescreen (#4013) 2023-08-29 11:51:57 -05:00
Peng Xiao
3ef0db7512 fix: add arch key to setup-node cached files (#4016) 2023-08-29 16:36:21 +00:00
Peng Xiao
92f9cb01f3 fix: enable electron sourcemap (#4014) 2023-08-29 23:42:50 +08:00
Peng Xiao
4abb407b5c fix: adjust email template font-family (#4015) 2023-08-29 23:27:20 +08:00
LongYinan
54574e5cc3 fix(server): signup/signin logic (#4008) 2023-08-29 19:22:56 +08:00
Alex Yang
a046cdafa3 chore: bump version (#4010) 2023-08-29 06:21:04 -05:00
Mirone
0d70d3727d fix: preloading issues caused by reference change in template (#4009) 2023-08-29 05:33:08 -05:00
Alex Yang
0ded20fcb7 ci: only deploy in the canary version 2023-08-29 05:27:14 -05:00
Alex Yang
06cec822f0 fix(core): options might undefined (#3999)
Co-authored-by: zuozijian3720 <zuozijian1994@gmail.com>
2023-08-29 05:19:21 -05:00
754 changed files with 42995 additions and 31693 deletions

View File

@@ -17,7 +17,6 @@
"cli",
"hooks",
"i18n",
"jotai",
"native",
"templates",
"y-indexeddb",

View File

@@ -37,11 +37,22 @@ const createPattern = packageName => [
// useSession is type unsafe
importNames: ['useSession'],
},
{
group: ['next-auth/react'],
message: "Import hooks from 'cloud-utils.ts'",
importNames: ['signIn', 'signOut'],
},
{
group: ['yjs'],
message: 'Do not use this API because it has a bug',
importNames: ['mergeUpdates'],
},
{
group: ['@affine/env/constant'],
message:
'Do not import from @affine/env/constant. Use `environment.isDesktop` instead',
importNames: ['isDesktop'],
},
];
const allPackages = [
@@ -52,7 +63,6 @@ const allPackages = [
'packages/graphql',
'packages/hooks',
'packages/i18n',
'packages/jotai',
'packages/native',
'packages/infra',
'packages/sdk',
@@ -122,6 +132,7 @@ const config = {
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
@@ -172,6 +183,11 @@ const config = {
// useSession is type unsafe
importNames: ['useSession'],
},
{
group: ['next-auth/react'],
message: "Import hooks from 'cloud-utils.ts'",
importNames: ['signIn', 'signOut'],
},
{
group: ['yjs'],
message: 'Do not use this API because it has a bug',
@@ -187,6 +203,7 @@ const config = {
ignore: ['^\\[[a-zA-Z0-9-_]+\\]\\.tsx$'],
},
],
'unicorn/no-unnecessary-await': 'error',
'sonarjs/no-all-duplicated-branches': 'error',
'sonarjs/no-element-overwrite': 'error',
'sonarjs/no-empty-collection': 'error',
@@ -237,6 +254,7 @@ const config = {
},
],
'@typescript-eslint/no-misused-promises': ['error'],
'i/no-extraneous-dependencies': ['error'],
},
})),
{
@@ -263,6 +281,7 @@ const config = {
],
'@typescript-eslint/no-floating-promises': 0,
'@typescript-eslint/no-misused-promises': 0,
'@typescript-eslint/no-restricted-imports': 0,
},
},
],

1
.github/CLA.md vendored
View File

@@ -61,3 +61,4 @@ Example:
- Shishu, @shishudesu, 2023/05/19
- Kushagra Singh, @kush002, 2023/06/28
- Sarvesh Kumar, @sarvesh521 2023/08/25
- 微扰理论 Qinghao Huang, @wfnuser 2023/09/29

View File

@@ -26,8 +26,8 @@ body:
- Windows x64
- Linux
- Web (app.affine.pro)
- Web (stage.affine.pro)
- Web (dev.affine.live)
- Web (affine.fail)
- Web (insider.affine.pro)
validations:
required: true
- type: dropdown

View File

@@ -34,7 +34,7 @@ runs:
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
shell: bash
run: |
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
env:
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
@@ -48,7 +48,7 @@ runs:
export CC=x86_64-unknown-linux-gnu-gcc
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc
export RUSTFLAGS="-C debuginfo=1"
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache
chmod -R 777 target
@@ -60,6 +60,6 @@ runs:
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: |
export RUSTFLAGS="-C debuginfo=1"
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache
chmod -R 777 target

View File

@@ -63,22 +63,24 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-json sync.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`,
]
: [];
const webReplicaCount = isProduction ? 3 : isBeta ? 2 : 1;
const graphqlReplicaCount = isProduction ? 3 : isBeta ? 2 : 1;
const syncReplicaCount = isProduction ? 6 : isBeta ? 3 : 1;
const webReplicaCount = isProduction ? 3 : isBeta ? 2 : 2;
const graphqlReplicaCount = isProduction ? 3 : isBeta ? 2 : 2;
const syncReplicaCount = isProduction ? 6 : isBeta ? 3 : 2;
const namespace = isProduction ? 'production' : isBeta ? 'beta' : 'dev';
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const host = DEPLOY_HOST || CANARY_DEPLOY_HOST;
const deployCommand = [
`helm upgrade --install affine .github/helm/affine`,
`--namespace ${namespace}`,
`--set global.ingress.enabled=true`,
`--set-json global.ingress.annotations=\"{ \\"kubernetes.io/ingress.class\\": \\"gce\\", \\"kubernetes.io/ingress.allow-http\\": \\"true\\", \\"kubernetes.io/ingress.global-static-ip-name\\": \\"${staticIpName}\\" }\"`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
`--set-string global.ingress.host="${DEPLOY_HOST || CANARY_DEPLOY_HOST}"`,
`--set-string global.ingress.host="${host}"`,
...redisAndPostgres,
`--set web.replicaCount=${webReplicaCount}`,
`--set-string web.image.tag="${imageTag}"`,
`--set graphql.replicaCount=${graphqlReplicaCount}`,
`--set-string graphql.image.tag="${imageTag}"`,
`--set graphql.app.host=${host}`,
`--set graphql.app.objectStorage.r2.enabled=true`,
`--set-string graphql.app.objectStorage.r2.accountId="${R2_ACCOUNT_ID}"`,
`--set-string graphql.app.objectStorage.r2.accessKeyId="${R2_ACCESS_KEY_ID}"`,

View File

@@ -7,8 +7,7 @@ runs:
- name: 'Install @electron-forge/maker-dmg'
if: runner.os == 'macos'
shell: bash
working-directory: ./apps/electron
run: yarn add @electron-forge/maker-dmg --dev
run: yarn workspace @affine/electron add @electron-forge/maker-dmg --dev
env:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'

View File

@@ -21,6 +21,14 @@ inputs:
description: 'set nmMode to hardlinks-local in .yarnrc.yml'
required: false
default: 'true'
build-infra:
description: 'Build infra'
required: false
default: 'true'
build-plugins:
description: 'Build plugins'
required: false
default: 'true'
runs:
using: 'composite'
@@ -42,7 +50,7 @@ runs:
if: ${{ inputs.package-install == 'true' }}
continue-on-error: true
shell: bash
run: yarn install ${{ inputs.extra-flags }}
run: yarn ${{ inputs.extra-flags }}
env:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
@@ -52,7 +60,7 @@ runs:
- name: yarn install (try again)
if: ${{ steps.install.outcome == 'failure' }}
shell: bash
run: yarn install ${{ inputs.extra-flags }}
run: yarn ${{ inputs.extra-flags }}
env:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
@@ -76,7 +84,7 @@ runs:
if: ${{ inputs.playwright-install == 'true' }}
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}'
key: '${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}'
# As a fallback, if the Playwright version has changed, try use the
# most recently cached version. There's a good chance that at least one
# of the browser binary versions haven't been updated, so Playwright can
@@ -86,14 +94,14 @@ runs:
# date cache, but still let Playwright decide if it needs to download
# new binaries or not.
restore-keys: |
${{ runner.os }}-playwright-
${{ runner.os }}-${{ runner.arch }}-playwright-
# If the Playwright browser binaries weren't able to be restored, we tell
# paywright to install everything for us.
# playwright to install everything for us.
- name: Install Playwright's dependencies
shell: bash
if: inputs.playwright-install == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps
if: inputs.playwright-install == 'true'
run: yarn playwright install --with-deps chromium
- name: Get installed Electron version
id: electron-version
@@ -107,21 +115,23 @@ runs:
if: ${{ inputs.electron-install == 'true' }}
with:
path: 'node_modules/.cache/electron'
key: '${{ runner.os }}-electron-${{ steps.electron-version.outputs.version }}'
key: '${{ runner.os }}-{{ runner.arch }}-electron-${{ steps.electron-version.outputs.version }}'
restore-keys: |
${{ runner.os }}-electron-
${{ runner.os }}-{{ runner.arch }}-electron-
- name: Install Electron binary
shell: bash
if: inputs.electron-install == 'true'
run: node apps/electron/node_modules/electron/install.js
run: node ./node_modules/electron/install.js
env:
ELECTRON_OVERRIDE_DIST_PATH: ./node_modules/.cache/electron
- name: Build Infra
shell: bash
if: inputs.build-infra == 'true'
run: yarn run build:infra
- name: Build Plugins
if: inputs.build-plugins == 'true'
shell: bash
run: yarn run build:plugins

1
.github/labeler.yml vendored
View File

@@ -46,7 +46,6 @@ mod:storage: 'packages/storage/**/*'
mod:native: 'packages/native/**/*'
mod:store:
- 'packages/jotai/**/*'
- '**/atoms/**/*'
rust:

View File

@@ -41,11 +41,11 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
run: yarn run build:plugins
with:
electron-install: false
- name: Build Core
run: yarn nx build @affine/core
- name: Upload core artifact
@@ -94,13 +94,15 @@ jobs:
}
needs: build-core
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
@@ -110,8 +112,7 @@ jobs:
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn vitest
working-directory: ./apps/electron
run: yarn workspace @affine/electron vitest
- name: Download core artifact
uses: actions/download-artifact@v3
@@ -119,27 +120,18 @@ jobs:
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Upload desktop dist
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: ./apps/electron/dist
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop e2e
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine/electron test
run: yarn workspace @affine-test/affine-desktop e2e
env:
COVERAGE: true
@@ -147,13 +139,13 @@ jobs:
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
run: yarn workspace @affine/electron make --platform=darwin --arch=arm64
- name: Bundle output check
- name: Output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
yarn ts-node-esm ./scripts/macos-arm64-output-check.mts
working-directory: apps/electron
yarn workspace @affine/electron ts-node-esm ./scripts/macos-arm64-output-check.mts
- name: Collect code coverage report
if: ${{ matrix.spec.test }}

315
.github/workflows/build-server.yml vendored Normal file
View File

@@ -0,0 +1,315 @@
name: Build(Server) & Test
on:
push:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-server.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/build-server.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
DISTRIBUTION: browser
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
build-storage:
name: Build Storage
runs-on: ubuntu-latest
env:
RUSTFLAGS: '-C debuginfo=1'
environment: development
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/storage
electron-install: false
build-infra: false
build-plugins: false
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
package: '@affine/storage'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/storage/storage.node
if-no-files-found: error
server-test:
name: Server Test
runs-on: ubuntu-latest
environment: development
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec ts-node-esm ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Run server tests
run: yarn workspace @affine/server test:coverage
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
server-e2e-test:
name: Server E2E Test
runs-on: ubuntu-latest
environment: development
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec ts-node-esm ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Run playwright tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-cloud e2e --forbid-only
env:
COVERAGE: true
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: server-e2etest
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore
server-desktop-e2e-test:
name: Server Desktop E2E Test
runs-on: ubuntu-latest
environment: development
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn workspace @affine/server exec ts-node-esm ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build:dev
- name: Run playwright tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" yarn workspace @affine-test/affine-desktop-cloud e2e
env:
COVERAGE: true
DEV_SERVER_URL: http://localhost:8080
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
ENABLE_LOCAL_EMAIL: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: server-e2etest
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore

View File

@@ -42,7 +42,7 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -61,224 +61,23 @@ jobs:
- name: Run Type Check
run: yarn typecheck
build-prototype:
name: Build Prototype
check-yarn-binary:
name: Check yarn binary
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Build Prototype
run: yarn nx build prototype
- name: Upload prototype artifact
uses: actions/upload-artifact@v3
with:
name: prototype
path: ./apps/prototype/dist
if-no-files-found: error
build-server:
name: Build Server
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Build Server
run: yarn nx build @affine/server
- name: Upload server dist
uses: actions/upload-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
if-no-files-found: error
build-docs:
name: Build Docs
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- run: yarn nx build @affine/docs
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
build-storage:
name: Build Storage
runs-on: ubuntu-latest
env:
RUSTFLAGS: '-C debuginfo=1'
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
package: '@affine/storage'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/storage/storage.node
if-no-files-found: error
server-test:
name: Server Test
runs-on: ubuntu-latest
environment: development
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Initialize database
- uses: actions/checkout@v4
- name: Run check
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn exec prisma generate
yarn exec prisma db push
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn exec ts-node-esm ./scripts/init-db.ts
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Run server tests
run: yarn test:coverage
working-directory: apps/server
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
server-e2e-test:
name: Server E2E Test
runs-on: ubuntu-latest
environment: development
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn exec prisma generate
yarn exec prisma db push
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn exec ts-node-esm ./scripts/init-db.ts
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: tests/affine-cloud
env:
COVERAGE: true
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: server-e2etest
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore
yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])")
git diff --exit-code
e2e-plugin-test:
name: E2E Plugin Test
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -309,49 +108,6 @@ jobs:
path: ./test-results
if-no-files-found: ignore
e2e-prototype-test:
name: E2E Prototype Test
runs-on: ubuntu-latest
environment: development
needs: build-prototype
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Download prototype artifact
uses: actions/download-artifact@v3
with:
name: prototype
path: ./apps/prototype/dist
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: tests/affine-prototype
env:
COVERAGE: true
# - name: Collect code coverage report
# run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
# - name: Upload e2e test coverage results
# uses: codecov/codecov-action@v3
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# files: ./.coverage/lcov.info
# flags: e2etest-prototype
# name: affine
# fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-prototype
path: ./test-results
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
@@ -361,7 +117,7 @@ jobs:
shard: [1, 2, 3, 4, 5]
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -398,33 +154,23 @@ jobs:
name: E2E Migration Test
runs-on: ubuntu-latest
environment: development
strategy:
matrix:
spec:
- { package: 0.7.0-canary.18 }
- { package: 0.8.0-canary.7 }
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Unzip
run: yarn unzip
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
run: yarn workspace @affine-test/affine-migration e2e --forbid-only
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-migration-${{ matrix.spec.package }}
path: ./tests/affine-legacy/${{ matrix.spec.package }}/test-results
name: test-results-e2e-migration
path: ./tests/affine-migration/test-results
if-no-files-found: ignore
unit-test:
@@ -432,7 +178,7 @@ jobs:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Cleanup
run: |

View File

@@ -37,7 +37,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -4,6 +4,8 @@ on:
push:
branches:
- master
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
workflow_dispatch:
inputs:
flavor:
@@ -22,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -41,7 +43,7 @@ jobs:
environment: production
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
@@ -68,7 +70,7 @@ jobs:
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
@@ -93,7 +95,7 @@ jobs:
- build-core
- build-storage
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Download core artifact
uses: actions/download-artifact@v3
with:
@@ -169,6 +171,7 @@ jobs:
deploy:
name: Deploy to cluster
if: ${{ github.event_name == 'workflow_dispatch' || github.ref_type == 'tag' }}
environment: ${{ github.event.inputs.flavor }}
permissions:
contents: 'write'
@@ -177,7 +180,7 @@ jobs:
- build-docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Deploy to dev
uses: ./.github/actions/deploy
with:

View File

@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout Helm chart repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: toeverything/helm-charts
path: .helm-chart-repo

View File

@@ -19,19 +19,17 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Check Language Key
if: github.ref != 'refs/heads/master'
working-directory: ./packages/i18n
run: yarn run sync-languages:check
run: yarn workspace @affine/i18n run sync-languages:check
env:
TOLGEE_API_KEY: ${{ secrets.TOLGEE_API_KEY }}
- name: Sync Languages
if: github.ref == 'refs/heads/master'
working-directory: ./packages/i18n
run: yarn run sync-languages
run: yarn workspace @affine/i18n run sync-languages
env:
TOLGEE_API_KEY: ${{ secrets.TOLGEE_API_KEY }}

View File

@@ -1,6 +1,7 @@
name: Build Canary Desktop App on Staging Branch
on:
workflow_dispatch:
push:
branches:
# 0.6.x-staging
@@ -35,7 +36,7 @@ jobs:
outputs:
version: 0.0.0-internal.${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: toeverything/set-build-version@latest
- id: version
run: echo ::set-output name=version::${{ env.BUILD_VERSION }}
@@ -46,7 +47,7 @@ jobs:
needs:
- set-build-version
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
@@ -102,7 +103,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -173,7 +174,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:

View File

@@ -1,54 +0,0 @@
name: NX
on:
push:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/nx.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/nx.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
jobs:
main:
name: Nx Cloud - Main Job
uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.13.0
with:
runs-on: macos-latest
main-branch-name: master
number-of-agents: 5
init-commands: |
yarn exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=5
environment-variables: |
BUILD_TYPE=canary
# parallel-commands: |
# yarn exec nx-cloud record -- yarn exec nx format:check
parallel-commands-on-agents: |
yarn exec nx affected --target=build --parallel=5
timeout: 60
agents:
name: Nx Cloud - Agents
uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.13.0
with:
runs-on: macos-latest
number-of-agents: 5
environment-variables: |
BUILD_TYPE=canary
timeout: 60

View File

@@ -17,7 +17,9 @@ jobs:
name: Check pull request title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- run: echo "${{ github.event.pull_request.title }}" | npx commitlint -g ./.commitlintrc.json
with:
electron-install: false
- run: echo "${{ github.event.pull_request.title }}" | yarn dlx commitlint -g ./.commitlintrc.json

View File

@@ -4,15 +4,19 @@ env:
NODE_OPTIONS: --max-old-space-size=4096
on:
workflow_dispatch:
push:
branches:
- master
pull_request_target:
pull_request:
branches:
- master
paths-ignore:
- README.md
- .github/**
- apps/server
- apps/docs
- apps/electron
- '!.github/workflows/publish-storybook.yml'
jobs:
@@ -21,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
# This is required to fetch all commits for chromatic
@@ -32,12 +36,20 @@ jobs:
electron-install: false
- name: Build Plugins
run: yarn run build:plugins
- name: Publish to Chromatic
uses: chromaui/action@v1
- uses: chromaui/action-next@v1
with:
workingDir: apps/storybook
buildScriptName: build
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
zip: true
exitOnceUploaded: true
onlyChanged: false
diagnostics: true
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
NODE_OPTIONS: ${{ env.NODE_OPTIONS }}
- uses: actions/upload-artifact@v2
if: always()
with:
name: chromatic-build-artifacts-${{ github.run_id }}
path: |
chromatic-diagnostics.json
**/build-storybook.log

View File

@@ -44,7 +44,7 @@ jobs:
outputs:
RELEASE_VERSION: ${{ steps.get-canary-version.outputs.RELEASE_VERSION }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
@@ -101,7 +101,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -119,9 +119,6 @@ jobs:
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
@@ -172,7 +169,7 @@ jobs:
env:
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -240,7 +237,7 @@ jobs:
outputs:
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -318,7 +315,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: core
path: web-static
- name: Zip web-static
run: zip -r web-static.zip web-static
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:

View File

@@ -17,7 +17,7 @@ jobs:
name: Try publishing npm@latest release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Try publishing to NPM
@@ -31,7 +31,7 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
@@ -50,7 +50,7 @@ jobs:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -72,7 +72,7 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
@@ -97,7 +97,7 @@ jobs:
- build-core
- build-storage
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Download core artifact
uses: actions/download-artifact@v3
with:

View File

@@ -8,7 +8,7 @@ packages/graphql/src/graphql/index.ts
out
dist
.yarn
tests/affine-legacy/0.7.0-canary.18/static
tests/affine-legacy/**/static
.github/helm
_next
storybook-static

View File

@@ -0,0 +1,15 @@
diff --git a/package.json b/package.json
index 26dcf8217f3e221e4c53722f14d29bb788332772..57a66dcb0943b9dd5cdaac2eaffccd9225a6b735 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,10 @@
"./adapters": {
"types": "./adapters.d.ts"
},
+ "./core": {
+ "types": "./core/index.d.ts",
+ "default": "./core/index.js"
+ },
"./jwt": {
"types": "./jwt/index.d.ts",
"default": "./jwt/index.js"

File diff suppressed because one or more lines are too long

View File

@@ -16,4 +16,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: '@yarnpkg/plugin-workspace-tools'
yarnPath: .yarn/releases/yarn-3.6.0.cjs
yarnPath: .yarn/releases/yarn-3.6.4.cjs

2232
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,3 @@ lto = true
codegen-units = 1
opt-level = 3
strip = "symbols"
[patch.crates-io]
lib0 = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" }
yrs = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" }

View File

@@ -2,9 +2,9 @@ Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates.
Portions of this software are licensed as follows:
* All content that resides under the "apps/server" directory of this repository, if that directory exists, is licensed under the license defined in "apps/server/LICENSE".
* All third party components incorporated into the AFFiNE Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "MPL2.0" license as defined in "LICENSE-MPL2.0".
- All content that resides under the "apps/server" directory of this repository, if that directory exists, is licensed under the license defined in "apps/server/LICENSE".
- All third party components incorporated into the AFFiNE Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined in "LICENSE-MIT".
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

21
LICENSE-MIT Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,384 +0,0 @@
# Mozilla Public License Version 2.0
Copyright (c) TOEVERYTHING PTE. LTD. and its affiliates.
1. Definitions
---
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
---
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
---
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
---
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
---
- *
- 6. Disclaimer of Warranty \*
- ------------------------- \*
- *
- Covered Software is provided under this License on an "as is" \*
- basis, without warranty of any kind, either expressed, implied, or \*
- statutory, including, without limitation, warranties that the \*
- Covered Software is free of defects, merchantable, fit for a \*
- particular purpose or non-infringing. The entire risk as to the \*
- quality and performance of the Covered Software is with You. \*
- Should any Covered Software prove defective in any respect, You \*
- (not any Contributor) assume the cost of any necessary servicing, \*
- repair, or correction. This disclaimer of warranty constitutes an \*
- essential part of this License. No use of any Covered Software is \*
- authorized under this License except under this disclaimer. \*
- *
---
---
- *
- 7. Limitation of Liability \*
- -------------------------- \*
- *
- Under no circumstances and under no legal theory, whether tort \*
- (including negligence), contract, or otherwise, shall any \*
- Contributor, or anyone who distributes Covered Software as \*
- permitted above, be liable to You for any direct, indirect, \*
- special, incidental, or consequential damages of any character \*
- including, without limitation, damages for lost profits, loss of \*
- goodwill, work stoppage, computer failure or malfunction, or any \*
- and all other commercial damages or losses, even if such party \*
- shall have been informed of the possibility of such damages. This \*
- limitation of liability shall not apply to liability for death or \*
- personal injury resulting from such party's negligence to the \*
- extent applicable law prohibits such limitation. Some \*
- jurisdictions do not allow the exclusion or limitation of \*
- incidental or consequential damages, so this exclusion and \*
- limitation may not apply to You. \*
- *
---
8. Litigation
---
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
---
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
## Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
## Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -31,6 +31,7 @@
[![blocksuite-icon]](https://github.com/toeverything/blocksuite)
[![Rust-version-icon]](https://www.rust-lang.org/)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE?ref=badge_shield)
[![Deploy](https://github.com/toeverything/AFFiNE/actions/workflows/deploy.yml/badge.svg)](https://github.com/toeverything/AFFiNE/actions/workflows/deploy.yml)
</div>
@@ -77,7 +78,7 @@ Star us, and you will receive all releases notifications from GitHub without any
- **Hyper merged** — Write, draw and plan all at once. Assemble any blocks you love on any canvas you like to enjoy seamless transitions between workflows with AFFiNE.
- **Privacy focussed** — AFFiNE is built with your privacy in mind and is one of our key concerns. We want you to keep control of your data, allowing you to store it as you like, where you like while still being able to freely edit and view your data on-demand.
- **Offline-first** - With your privacy in mind we also decided to go offline-first. This means that AFFiNE can be used offline, whether you want to view or edit, with support for conflict-free merging when you are back online.
- **Offline-first** With your privacy in mind we also decided to go offline-first. This means that AFFiNE can be used offline, whether you want to view or edit, with support for conflict-free merging when you are back online.
- **Clean, intuitive design** — With AFFiNE you can concentrate on editing with a clean and modern interface. Which is responsive, so it looks great on tablets too, and mobile support is coming in the future.
- **Modern Block Editor with Markdown support** — A modern block editor can help you not only for docs, but slides and tables as well. When you write in AFFiNE you can use Markdown syntax which helps create an easier editing experience, that can be experienced with just a keyboard. And this allows you to export your data cleanly into Markdown.
- **Collaboration** — Whether you want to collaborate with yourself across multiple devices, or work together with others, support for collaboration and multiplayer is out-of-the-box, which makes it easy for teams to get started with AFFiNE.
@@ -106,11 +107,12 @@ If you have questions, you are welcome to contact us. One of the best places to
## Ecosystem
| Name | | |
| --------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| [@affine/component](https://affine-storybook.vercel.app/) | AFFiNE Component Resources | [![](https://img.shields.io/codecov/c/github/toeverything/affine?style=flat-square)](https://affine-storybook.vercel.app/) |
| [@toeverything/y-indexeddb](packages/y-indexeddb) | IndexedDB database adapter for Yjs | [![](https://img.shields.io/npm/dm/@toeverything/y-indexeddb?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
| [@toeverything/theme](packages/theme) | AFFiNE theme | [![](https://img.shields.io/npm/dm/@toeverything/theme?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/theme) |
| Name | | |
| ----------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| [@toeverything/component](https://github.com/toeverything/design/tree/main/packages/components) | Toeverything Shared Component Resources | |
| [@affine/component](packages/component) | AFFiNE Component Resources | [![](https://img.shields.io/codecov/c/github/toeverything/affine?style=flat-square)](https://affine-storybook.vercel.app/) |
| [@toeverything/y-indexeddb](packages/y-indexeddb) | IndexedDB database adapter for Yjs | [![](https://img.shields.io/npm/dm/@toeverything/y-indexeddb?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
| [@toeverything/theme](packages/theme) | AFFiNE theme | [![](https://img.shields.io/npm/dm/@toeverything/theme?style=flat-square&color=eee)](https://www.npmjs.com/package/@toeverything/theme) |
## Plugins
@@ -121,11 +123,11 @@ If you have questions, you are welcome to contact us. One of the best places to
- [@affine/sdk](./packages/sdk) - SDK for developing plugins
- [@affine/plugin-cli](./packages/plugin-cli) - CLI for developing plugins
| Official Plugin | Description |
| ----------------------------------------------------- | ----------------------------------------- |
| [@affine/bookmark-plugin](plugins/bookmark) | A block for bookmarking a website |
| [@affine/copilot-plugin](plugins/copilot) | AI Copilot that help you document writing |
| [@affine/image-preview-plugin](plugins/image-preview) | Component for previewing an image |
| Official Plugin | Description | Status |
| ----------------------------------------------------- | ----------------------------------------- | ------ |
| [@affine/copilot-plugin](plugins/copilot) | AI Copilot that help you document writing | 🚧 |
| [@affine/image-preview-plugin](plugins/image-preview) | Component for previewing an image | ✅ |
| [@affine/outline](plugins/outline) | Outline for your document | ✅ |
## Upstreams
@@ -144,18 +146,7 @@ We would also like to give thanks to open-source projects that make AFFiNE possi
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
# Contributors
## Current Core members
Team members who are currently maintaining the project:
- [JimmFly](https://github.com/JimmFly) - Jinfei Yang <yangjinfei001@gmail.com> (he/him)
- [pengx17](https://github.com/pengx17) - Peng Xiao <pengxiao@outlook.com> (he/him)
- [QiShaoXuan](https://github.com/QiSHaoXuan) - Shaoxuan Qi <qishaoxuan777@gmail.com> (he/him)
- [himself65](https://github.com/himself65) - Zeyu "Alex" Yang <himself65@outlook.com> (he/him)
## All Contributors
## Contributors
We would like to express our gratitude to all the individuals who have already contributed to AFFiNE! If you have any AFFiNE-related project, documentation, tool or template, please feel free to contribute it by submitting a pull request to our curated list on GitHub: [awesome-affine](https://github.com/toeverything/awesome-affine).
@@ -169,12 +160,17 @@ Data compatibility is a very important issue for us. We will try our best to ens
If you encounter any problems when upgrading the version, please feel free to [contact us](mailto:developer@toeverything.info).
| AFFiNE Version | Export/Import workspace | Data auto migration |
| -------------- | ----------------------- | ------------------- |
| <= 0.5.4 | ❌️ | ❌ |
| 0.6.x | ✅️ | ✅ |
| 0.7.x | ✅️ | ✅ |
| 0.8.x | ✅ | ✅ |
| AFFiNE Version | Export/Import workspace | Data auto migration |
| --------------- | ----------------------- | ------------------- |
| <= 0.5.4 | ❌️ | ❌ |
| 0.6.x | ✅️ | ✅ |
| 0.7.x | ✅️ | ✅ |
| 0.8.x (current) | ✅ | ✅ |
| 0.9.x (next) | 🚧 | 🚧 |
- ❌️: Not compatible
- ✅: Compatible
- 🚧: Work in progress
## Self-Host

View File

@@ -23,6 +23,7 @@ import { WebpackS3Plugin, gitShortHash } from './s3-plugin.js';
const IN_CI = !!process.env.CI;
export const rootPath = fileURLToPath(new URL('..', import.meta.url));
const workspaceRoot = join(rootPath, '..', '..');
const require = createRequire(rootPath);
@@ -69,19 +70,20 @@ const OptimizeOptionOptions: (
},
});
export const publicPath = (function () {
export const getPublicPath = (buildFlags: BuildFlags) => {
const { BUILD_TYPE } = process.env;
const publicPath = process.env.PUBLIC_PATH ?? '/';
if (process.env.COVERAGE) {
if (process.env.COVERAGE || buildFlags.distribution === 'desktop') {
return publicPath;
}
if (BUILD_TYPE === 'canary') {
return `https://dev.affineassets.com/${gitShortHash()}/`;
} else if (BUILD_TYPE === 'beta' || BUILD_TYPE === 'stable') {
return `https://prod.affineassets.com/${gitShortHash()}/`;
}
return publicPath;
})();
};
export const createConfiguration: (
buildFlags: BuildFlags,
@@ -119,7 +121,7 @@ export const createConfiguration: (
path: join(rootPath, 'dist'),
clean: buildFlags.mode === 'production',
globalObject: 'globalThis',
publicPath,
publicPath: getPublicPath(buildFlags),
},
target: ['web', 'es2022'],
@@ -127,9 +129,7 @@ export const createConfiguration: (
devtool:
buildFlags.mode === 'production'
? buildFlags.distribution === 'desktop'
? 'nosources-source-map'
: 'source-map'
? 'source-map'
: 'eval-cheap-module-source-map',
resolve: {
@@ -145,66 +145,70 @@ export const createConfiguration: (
: {
events: false,
},
alias:
blocksuiteBaseDir === undefined
? undefined
: {
yjs: require.resolve('yjs'),
'@blocksuite/block-std': resolve(
blocksuiteBaseDir,
'packages',
'block-std',
'src'
),
'@blocksuite/blocks': resolve(
blocksuiteBaseDir,
'packages',
'blocks',
'src'
),
'@blocksuite/editor': resolve(
blocksuiteBaseDir,
'packages',
'editor',
'src'
),
'@blocksuite/global': resolve(
blocksuiteBaseDir,
'packages',
'global',
'src'
),
'@blocksuite/lit': resolve(
blocksuiteBaseDir,
'packages',
'lit',
'src'
),
'@blocksuite/phasor': resolve(
blocksuiteBaseDir,
'packages',
'phasor',
'src'
),
'@blocksuite/store/providers/broadcast-channel': resolve(
blocksuiteBaseDir,
'packages',
'store',
'src/providers/broadcast-channel'
),
'@blocksuite/store': resolve(
blocksuiteBaseDir,
'packages',
'store',
'src'
),
'@blocksuite/virgo': resolve(
blocksuiteBaseDir,
'packages',
'virgo',
'src'
),
},
alias: {
yjs: require.resolve('yjs'),
'@blocksuite/block-std': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'block-std', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'block-std',
'dist'
),
'@blocksuite/blocks': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'blocks', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'blocks',
'dist'
),
'@blocksuite/editor': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'editor', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'editor',
'dist'
),
'@blocksuite/global': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'global', 'src')
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'global',
'dist'
),
'@blocksuite/lit': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'lit', 'src')
: join(workspaceRoot, 'node_modules', '@blocksuite', 'lit', 'dist'),
'@blocksuite/store/providers/broadcast-channel': blocksuiteBaseDir
? join(
blocksuiteBaseDir,
'packages',
'store',
'src/providers/broadcast-channel'
)
: join(
workspaceRoot,
'node_modules',
'@blocksuite',
'store',
'dist',
'providers',
'broadcast-channel.js'
),
'@blocksuite/store': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'store', 'src')
: join(workspaceRoot, 'node_modules', '@blocksuite', 'store', 'dist'),
'@blocksuite/virgo': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'virgo', 'src')
: join(workspaceRoot, 'node_modules', '@blocksuite', 'virgo', 'dist'),
},
},
module: {
@@ -226,11 +230,8 @@ export const createConfiguration: (
{
loader: require.resolve('source-map-loader'),
options: {
filterSourceMappingUrl: (
_url: string,
resourcePath: string
) => {
return resourcePath.includes('@blocksuite');
filterSourceMappingUrl: (_url: string) => {
return false;
},
},
},
@@ -270,7 +271,6 @@ export const createConfiguration: (
useDefineForClassFields: false,
},
experimental: {
keepImportAssertions: true,
plugins: [
buildFlags.coverage && [
'swc-plugin-coverage-instrument',
@@ -295,7 +295,7 @@ export const createConfiguration: (
exclude: [/node_modules/],
},
{
test: /\.(png|jpg|gif|svg|webp)$/,
test: /\.(png|jpg|gif|svg|webp|mp4)$/,
type: 'asset/resource',
},
{
@@ -353,10 +353,12 @@ export const createConfiguration: (
'process.env': JSON.stringify({}),
'process.env.COVERAGE': JSON.stringify(!!buildFlags.coverage),
'process.env.NODE_ENV': JSON.stringify(buildFlags.mode),
'process.env.SHOULD_REPORT_TRACE': `${Boolean(
process.env.SHOULD_REPORT_TRACE
)}`,
'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`,
'process.env.SHOULD_REPORT_TRACE': JSON.stringify(
Boolean(process.env.SHOULD_REPORT_TRACE === 'true')
),
'process.env.TRACE_REPORT_ENDPOINT': JSON.stringify(
process.env.TRACE_REPORT_ENDPOINT
),
runtimeConfig: JSON.stringify(runtimeConfig),
}),
new CopyPlugin({

View File

@@ -6,21 +6,11 @@ const require = createRequire(import.meta.url);
const packageJson = require('../package.json');
const editorFlags: BlockSuiteFeatureFlags = {
enable_drag_handle: true,
enable_block_hub: true,
enable_surface: true,
enable_edgeless_toolbar: true,
enable_slash_menu: true,
enable_database: true,
enable_database_filter: false,
enable_data_view: false,
enable_page_tags: false,
enable_toggle_block: false,
enable_linked_page: true,
enable_bookmark_operation: false,
enable_note_index: false,
enable_attachment_block: true,
enable_set_remote_flag: false,
};
export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
@@ -48,13 +38,13 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
get beta() {
return {
...this.stable,
serverUrlPrefix: 'https://ambassador.affine.pro',
serverUrlPrefix: 'https://insider.affine.pro',
};
},
get internal() {
return {
...this.stable,
serverUrlPrefix: 'https://affine.fail',
serverUrlPrefix: 'https://insider.affine.pro',
};
},
// canary will be aggressive and enable all features

View File

@@ -1,12 +0,0 @@
import type { BuildFlags } from '@affine/cli/config';
export function computeCacheKey(buildFlags: BuildFlags) {
return [
'1',
'node' + process.version,
buildFlags.mode,
buildFlags.distribution,
buildFlags.channel,
...(buildFlags.localBlockSuite ? [buildFlags.localBlockSuite] : []),
].join('-');
}

View File

@@ -1,4 +1,4 @@
import { createConfiguration, rootPath, publicPath } from './config.js';
import { createConfiguration, rootPath, getPublicPath } from './config.js';
import { merge } from 'webpack-merge';
import { join, resolve } from 'node:path';
import type { BuildFlags } from '@affine/cli/config';
@@ -55,7 +55,7 @@ export default async function (cli_env: any, _: any) {
inject: 'body',
scriptLoading: 'module',
minify: false,
publicPath,
publicPath: getPublicPath(flags),
chunks: [
'_plugin/index.test',
'plugin',

View File

@@ -2,7 +2,7 @@
"name": "@affine/core",
"type": "module",
"private": true,
"version": "0.9.0-canary.1",
"version": "0.10.0-canary.1",
"scripts": {
"build": "yarn -T run build-core",
"dev": "yarn -T run dev-core",
@@ -12,7 +12,8 @@
"./app": "./src/app.tsx",
"./router": "./src/router.ts",
"./bootstrap/setup": "./src/bootstrap/setup.ts",
"./bootstrap/register-plugins": "./src/bootstrap/register-plugins.ts"
"./bootstrap/register-plugins": "./src/bootstrap/register-plugins.ts",
"./components/pure/*": "./src/components/pure/*/index.tsx"
},
"dependencies": {
"@affine-test/fixtures": "workspace:*",
@@ -21,64 +22,60 @@
"@affine/env": "workspace:*",
"@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230827224823-81f8728e-nightly",
"@blocksuite/blocks": "0.0.0-20230827224823-81f8728e-nightly",
"@blocksuite/editor": "0.0.0-20230827224823-81f8728e-nightly",
"@blocksuite/global": "0.0.0-20230827224823-81f8728e-nightly",
"@blocksuite/icons": "^2.1.31",
"@blocksuite/lit": "0.0.0-20230827224823-81f8728e-nightly",
"@blocksuite/store": "0.0.0-20230827224823-81f8728e-nightly",
"@blocksuite/block-std": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/blocks": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/editor": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/global": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/icons": "2.1.34",
"@blocksuite/lit": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/store": "0.0.0-20230926212737-6d4b1569-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.6",
"@radix-ui/react-select": "^1.2.2",
"@mui/material": "^5.14.13",
"@radix-ui/react-select": "^2.0.0",
"@react-hookz/web": "^23.1.0",
"@toeverything/components": "^0.0.19",
"@toeverything/components": "^0.0.45",
"async-call-rpc": "^6.3.1",
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
"cssnano": "^6.0.1",
"graphql": "^16.8.0",
"intl-segmenter-polyfill-rs": "^0.1.5",
"jotai": "^2.4.0",
"jotai-devtools": "^0.6.2",
"graphql": "^16.8.1",
"intl-segmenter-polyfill-rs": "^0.1.6",
"jotai": "^2.4.3",
"jotai-devtools": "^0.7.0",
"lit": "^2.8.0",
"lodash.debounce": "^4.0.8",
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.7.6",
"next-auth": "^4.22.1",
"next-auth": "^4.23.2",
"next-themes": "^0.2.1",
"postcss-loader": "^7.3.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-is": "18.2.0",
"react-resizable-panels": "^0.0.55",
"react-router-dom": "^6.15.0",
"react-router-dom": "^6.16.0",
"rxjs": "^7.8.1",
"ses": "^0.18.7",
"swr": "2.2.1",
"valtio": "^1.10.6",
"y-protocols": "^1.0.5",
"yjs": "^13.6.7",
"zod": "^3.22.2"
"ses": "^0.18.8",
"swr": "2.2.0",
"valtio": "^1.11.2",
"y-protocols": "^1.0.6",
"yjs": "^13.6.8",
"zod": "^3.22.4"
},
"devDependencies": {
"@aws-sdk/client-s3": "3.400.0",
"@aws-sdk/client-s3": "3.428.0",
"@perfsee/webpack": "^1.8.4",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@sentry/webpack-plugin": "^2.7.0",
"@sentry/webpack-plugin": "^2.8.0",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.3.80",
"@types/lodash-es": "^4.17.8",
"@types/lodash.debounce": "^4.0.7",
"@types/webpack-env": "^1.18.1",
"@swc/core": "^1.3.93",
"@types/lodash-es": "^4.17.9",
"@types/webpack-env": "^1.18.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"express": "^4.18.2",
@@ -91,7 +88,7 @@
"swc-loader": "^0.2.3",
"swc-plugin-coverage-instrument": "^0.0.20",
"thread-loader": "^4.0.2",
"webpack": "^5.88.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.9.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,11 +1,13 @@
import { initEmptyPage } from '@affine/env/blocksuite';
import { PageNotFoundError } from '@affine/env/constant';
import type {
WorkspaceFlavour,
WorkspaceUISchema,
} from '@affine/env/workspace';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { lazy, useCallback } from 'react';
import type { OnLoadEditor } from '../../components/page-detail-editor';
import { useCurrentUser } from '../../hooks/affine/use-current-user';
import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner';
import { useWorkspace } from '../../hooks/use-workspace';
import {
@@ -32,12 +34,28 @@ export const UI = {
if (!page) {
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
}
// this should be safe because we are under cloud workspace adapter
const currentUser = useCurrentUser();
const onLoad = useCallback<OnLoadEditor>(
(...args) => {
const dispose = onLoadEditor(...args);
workspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
'user',
{
name: currentUser.name,
}
);
return dispose;
},
[currentUser, workspace, onLoadEditor]
);
return (
<>
<PageDetailEditor
pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor}
onLoad={onLoad}
workspace={workspace.blockSuiteWorkspace}
/>
</>

View File

@@ -1,7 +1,5 @@
import { DebugLogger } from '@affine/debug';
import { initEmptyPage } from '@affine/env/blocksuite';
import {
DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX,
DEFAULT_WORKSPACE_NAME,
PageNotFoundError,
} from '@affine/env/constant';
@@ -16,12 +14,16 @@ import {
CRUD,
saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import {
getOrCreateWorkspace,
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { nanoid } from '@blocksuite/store';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import { getCurrentStore } from '@toeverything/infra/atom';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { setPageModeAtom } from '../../atoms';
@@ -49,6 +51,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
if (runtimeConfig.enablePreloading) {
buildShowcaseWorkspace(blockSuiteWorkspace, {
schema: globalBlockSuiteSchema,
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
@@ -57,8 +60,9 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
logger.error('init page with preloading failed', err);
});
} else {
const page = blockSuiteWorkspace.createPage({
id: `${blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`,
const page = blockSuiteWorkspace.createPage();
blockSuiteWorkspace.setPageMeta(page.id, {
jumpOnce: true,
});
initEmptyPage(page).catch(error => {
logger.error('init page with empty failed', error);

View File

@@ -1,7 +1,7 @@
import { initEmptyPage } from '@affine/env/blocksuite';
import { PageNotFoundError } from '@affine/env/constant';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { type WorkspaceUISchema } from '@affine/env/workspace';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { useCallback } from 'react';
import { useWorkspace } from '../../hooks/use-workspace';

View File

@@ -9,8 +9,9 @@ import {
ReleaseType,
WorkspaceFlavour,
} from '@affine/env/workspace';
import { CRUD as CloudCRUD } from '@affine/workspace/affine/crud';
import { startSync, stopSync } from '@affine/workspace/affine/sync';
import { CRUD as CloudCRUD } from './cloud/crud';
import { UI as CloudUI } from './cloud/ui';
import { LocalAdapter } from './local';
import { UI as PublicCloudUI } from './public-cloud/ui';
@@ -40,6 +41,8 @@ export const WorkspaceAdapters = {
return false;
}
},
'service:start': startSync,
'service:stop': stopSync,
} as Partial<AppEvents>,
CRUD: CloudCRUD,
UI: CloudUI,

View File

@@ -3,15 +3,16 @@ import '@affine/component/theme/theme.css';
import '@toeverything/components/style.css';
import { AffineContext } from '@affine/component/context';
import { NotificationCenter } from '@affine/component/notification-center';
import { WorkspaceFallback } from '@affine/component/workspace';
import { CacheProvider } from '@emotion/react';
import { getCurrentStore } from '@toeverything/infra/atom';
import { use } from 'foxact/use';
import { SessionProvider } from 'next-auth/react';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { CloudSessionProvider } from './providers/session-provider';
import { router } from './router';
import createEmotionCache from './utils/create-emotion-cache';
@@ -48,18 +49,19 @@ const languageLoadingPromise = loadLanguage().catch(console.error);
export const App = memo(function App() {
use(languageLoadingPromise);
return (
<SessionProvider refetchOnWindowFocus>
<CacheProvider value={cache}>
<AffineContext store={getCurrentStore()}>
<CacheProvider value={cache}>
<AffineContext store={getCurrentStore()}>
<CloudSessionProvider>
<DebugProvider>
{runtimeConfig.enableNotificationCenter && <NotificationCenter />}
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
router={router}
future={future}
/>
</DebugProvider>
</AffineContext>
</CacheProvider>
</SessionProvider>
</CloudSessionProvider>
</AffineContext>
</CacheProvider>
);
});

View File

@@ -9,7 +9,7 @@ import { describe, expect, test } from 'vitest';
import {
pageSettingFamily,
pageSettingsAtom,
recentPageSettingsAtom,
recentPageIdsBaseAtom,
} from '../index';
describe('page mode atom', () => {
@@ -26,20 +26,12 @@ describe('page mode atom', () => {
},
});
expect(store.get(recentPageSettingsAtom)).toEqual([
{
id: 'page0',
mode: 'page',
},
]);
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page0']);
const page1SettingAtom = pageSettingFamily('page1');
store.set(page1SettingAtom, {
mode: 'edgeless',
});
expect(store.get(recentPageSettingsAtom)).toEqual([
{ id: 'page1', mode: 'edgeless' },
{ id: 'page0', mode: 'page' },
]);
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page1', 'page0']);
});
});

View File

@@ -0,0 +1,4 @@
import { atom } from 'jotai';
import type { SessionContextValue } from 'next-auth/react';
export const sessionAtom = atom<SessionContextValue<true> | null>(null);

View File

@@ -0,0 +1,5 @@
import { atom } from 'jotai/vanilla';
export const appHeaderAtom = atom<HTMLDivElement | null>(null);
export const mainContainerAtom = atom<HTMLDivElement | null>(null);

View File

@@ -0,0 +1,25 @@
import { atom, useAtom } from 'jotai';
import { useCallback } from 'react';
export type OnceSignedInEvent = () => void;
export const onceSignedInEventsAtom = atom<OnceSignedInEvent[]>([]);
export const setOnceSignedInEventAtom = atom(
null,
(get, set, event: OnceSignedInEvent) => {
set(onceSignedInEventsAtom, [...get(onceSignedInEventsAtom), event]);
}
);
export const useOnceSignedInEvents = () => {
const [events, setEvents] = useAtom(onceSignedInEventsAtom);
return useCallback(async () => {
try {
await Promise.all(events.map(event => event()));
} catch (err) {
console.error('Error executing one of the events:', err);
}
setEvents([]);
}, [events, setEvents]);
};

View File

@@ -11,6 +11,7 @@ export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
export const openQuickSearchModalAtom = atom(false);
export const openOnboardingModalAtom = atom(false);
export const openSignOutModalAtom = atom(false);
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & {
open: boolean;
@@ -27,8 +28,6 @@ export type AuthAtom = {
state: AuthProps['state'];
email?: string;
emailType?: AuthProps['emailType'];
// Only used for sign in page callback, after called, it will be set to undefined
onceSignedIn?: () => void;
};
export const authAtom = atom<AuthAtom>({
@@ -45,10 +44,6 @@ type PageLocalSetting = {
mode: PageMode;
};
type PartialPageLocalSettingWithPageId = Partial<PageLocalSetting> & {
id: string;
};
const pageSettingsBaseAtom = atomWithStorage(
'pageSettings',
{} as Record<string, PageLocalSetting>
@@ -57,22 +52,11 @@ const pageSettingsBaseAtom = atomWithStorage(
// readonly atom by design
export const pageSettingsAtom = atom(get => get(pageSettingsBaseAtom));
const recentPageSettingsBaseAtom = atomWithStorage<string[]>(
export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
'recentPageSettings',
[]
);
export const recentPageSettingsAtom = atom<PartialPageLocalSettingWithPageId[]>(
get => {
const recentPageIDs = get(recentPageSettingsBaseAtom);
const pageSettings = get(pageSettingsAtom);
return recentPageIDs.map(id => ({
...pageSettings[id],
id,
}));
}
);
const defaultPageSetting = {
mode: 'page',
} satisfies PageLocalSetting;
@@ -87,7 +71,9 @@ export const pageSettingFamily: AtomFamily<
...defaultPageSetting,
},
(get, set, patch) => {
set(recentPageSettingsBaseAtom, ids => {
// fixme: this does not work when page reload,
// since atomWithStorage is async
set(recentPageIdsBaseAtom, ids => {
// pick 3 recent page ids
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
});
@@ -115,3 +101,5 @@ export const setPageModeAtom = atom(
export type PageModeOption = 'all' | 'page' | 'edgeless';
export const allPageModeSelectAtom = atom<PageModeOption>('all');
export const openWorkspaceListModalAtom = atom(false);

View File

@@ -49,7 +49,7 @@ export const fontStyleOptions = [
}[];
const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
clientBorder: globalThis.platform !== 'win32',
clientBorder: environment.isDesktop && globalThis.platform !== 'win32',
fullWidthLayout: false,
windowFrameStyle: 'frameless',
fontStyle: 'Sans',
@@ -63,7 +63,7 @@ const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
type SetStateAction<Value> = Value | ((prev: Value) => Value);
const appSettingAtom = atom<
export const appSettingAtom = atom<
AppSetting,
[SetStateAction<Partial<AppSetting>>],
void

View File

@@ -0,0 +1,13 @@
import { atom } from 'jotai';
export type TrashModal = {
open: boolean;
pageId: string;
pageTitle: string;
};
export const trashModalAtom = atom<TrashModal>({
open: false,
pageId: '',
pageTitle: '',
});

View File

@@ -16,7 +16,7 @@ import {
} from '@toeverything/infra/__internal__/plugin';
import {
contentLayoutAtom,
currentPageAtom,
currentPageIdAtom,
currentWorkspaceAtom,
} from '@toeverything/infra/atom';
import { atom } from 'jotai';
@@ -129,7 +129,7 @@ export function createSetup(rootStore: ReturnType<typeof createStore>) {
function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
// clean up plugin windows when switching to other pages
rootStore.sub(currentPageAtom, () => {
rootStore.sub(currentPageIdAtom, () => {
rootStore.set(contentLayoutAtom, 'editor');
});
@@ -149,7 +149,7 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
'@affine/sdk/entry': {
rootStore,
currentWorkspaceAtom: currentWorkspaceAtom,
currentPageAtom: currentPageAtom,
currentPageIdAtom: currentPageIdAtom,
pushLayoutAtom: pushLayoutAtom,
deleteLayoutAtom: deleteLayoutAtom,
},
@@ -169,7 +169,10 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
Map<string, Map<string, any>>
>();
const pluginImportsFunctionMap = new Map<string, (imports: any) => void>();
const pluginImportsFunctionMap = new Map<
string,
(newUpdaters: [string, [string, ((val: any) => void)[]][]][]) => void
>();
const createImports = (pluginName: string) => {
if (pluginImportsFunctionMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -355,9 +358,7 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
},
}
),
navigator: {
userAgent: navigator.userAgent,
},
navigator: globalThis.navigator,
MouseEvent: globalThis.MouseEvent,
KeyboardEvent: globalThis.KeyboardEvent,
@@ -548,7 +549,7 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
div,
widget.page,
() => {
return root.selectionManager.value;
return root.selection.value;
}
);
addCleanup(pluginName, () => {

View File

@@ -12,16 +12,15 @@ import {
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { assertExists } from '@blocksuite/global/utils';
import { nanoid } from '@blocksuite/store';
import {
migrateLocalBlobStorage,
migrateWorkspace,
WorkspaceVersion,
} from '@toeverything/infra/blocksuite';
import { downloadBinary } from '@toeverything/y-indexeddb';
import { downloadBinary, overwriteBinary } from '@toeverything/y-indexeddb';
import type { createStore } from 'jotai/vanilla';
import { Doc } from 'yjs';
import { applyUpdate } from 'yjs';
import { nanoid } from 'nanoid';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { WorkspaceAdapters } from '../adapters/workspace';
@@ -34,37 +33,39 @@ async function tryMigration() {
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (oldMeta.flavour === WorkspaceFlavour.LOCAL) {
let doc: YDoc;
const options = {
getCurrentRootDoc: async () => {
doc = new YDoc({
guid: oldMeta.id,
});
const downloadWorkspace = async (doc: YDoc): Promise<void> => {
const binary = await downloadBinary(doc.guid);
if (binary) {
applyUpdate(doc, binary);
}
await Promise.all(
[...doc.subdocs.values()].map(subdoc =>
downloadWorkspace(subdoc)
)
);
};
await downloadWorkspace(doc);
return doc;
},
createWorkspace: async () =>
getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL),
getSchema: () => globalBlockSuiteSchema,
};
promises.push(
migrateWorkspace(
'version' in oldMeta ? oldMeta.version : undefined,
{
getCurrentRootDoc: async () => {
const doc = new Doc({
guid: oldMeta.id,
});
const downloadWorkspace = async (doc: Doc): Promise<void> => {
const binary = await downloadBinary(doc.guid);
if (binary) {
applyUpdate(doc, binary);
}
return Promise.all(
[...doc.subdocs.values()].map(subdoc =>
downloadWorkspace(subdoc)
)
).then();
};
await downloadWorkspace(doc);
return doc;
},
createWorkspace: async () =>
getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL),
getSchema: () => globalBlockSuiteSchema,
}
).then(async workspace => {
if (typeof workspace !== 'boolean') {
options
).then(async status => {
if (typeof status !== 'boolean') {
const adapter = WorkspaceAdapters[oldMeta.flavour];
const oldWorkspace = await adapter.CRUD.get(oldMeta.id);
const newId = await adapter.CRUD.create(workspace);
const newId = await adapter.CRUD.create(status);
assertExists(
oldWorkspace,
'workspace should exist after migrate'
@@ -76,11 +77,25 @@ async function tryMigration() {
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.DatabaseV3,
version: WorkspaceVersion.Surface,
};
await migrateLocalBlobStorage(workspace.id, newId);
await migrateLocalBlobStorage(status.id, newId);
console.log('workspace migrated', oldMeta.id, newId);
} else if (workspace) {
} else if (status) {
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
newMetadata[index] = {
...oldMeta,
version: WorkspaceVersion.Surface,
};
const overWrite = async (doc: YDoc): Promise<void> => {
await overwriteBinary(doc.guid, encodeStateAsUpdate(doc));
return Promise.all(
[...doc.subdocs.values()].map(subdoc => overWrite(subdoc))
).then();
};
await overWrite(doc);
console.log('workspace migrated', oldMeta.id);
}
})
@@ -106,7 +121,7 @@ async function tryMigration() {
}
}
function createFirstAppData(store: ReturnType<typeof createStore>) {
export function createFirstAppData(store: ReturnType<typeof createStore>) {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
@@ -144,8 +159,8 @@ export async function setup(store: ReturnType<typeof createStore>) {
console.log('setup global');
setupGlobal();
createFirstAppData(store);
await tryMigration();
// do not read `rootWorkspacesMetadataAtom` before migration
await store.get(rootWorkspacesMetadataAtom);
console.log('setup done');
}

View File

@@ -0,0 +1,78 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ImportIcon, PlusIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
import { openCreateWorkspaceModalAtom } from '../atoms';
import type { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
export function registerAffineCreationCommands({
store,
pageHelper,
t,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
pageHelper: ReturnType<typeof usePageHelper>;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:new-page',
category: 'affine:creation',
label: t['com.affine.cmdk.affine.new-page'],
icon: <PlusIcon />,
keyBinding: environment.isDesktop
? {
binding: '$mod+N',
skipRegister: true,
}
: undefined,
run() {
pageHelper.createPage();
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:new-edgeless-page',
category: 'affine:creation',
icon: <PlusIcon />,
label: t['com.affine.cmdk.affine.new-edgeless-page'],
run() {
pageHelper.createEdgeless();
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:new-workspace',
category: 'affine:creation',
icon: <PlusIcon />,
label: t['com.affine.cmdk.affine.new-workspace'],
run() {
store.set(openCreateWorkspaceModalAtom, 'new');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:import-workspace',
category: 'affine:creation',
icon: <ImportIcon />,
label: t['com.affine.cmdk.affine.import-workspace'],
preconditionStrategy: () => {
return environment.isDesktop;
},
run() {
store.set(openCreateWorkspaceModalAtom, 'add');
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,58 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContactWithUsIcon, NewIcon, UserGuideIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
import { openOnboardingModalAtom, openSettingModalAtom } from '../atoms';
export function registerAffineHelpCommands({
t,
store,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:help-whats-new',
category: 'affine:help',
icon: <NewIcon />,
label: () => t['com.affine.cmdk.affine.whats-new'](),
run() {
window.open(runtimeConfig.changelogUrl, '_blank');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:help-contact-us',
category: 'affine:help',
icon: <ContactWithUsIcon />,
label: () => t['com.affine.cmdk.affine.contact-us'](),
run() {
store.set(openSettingModalAtom, {
open: true,
activeTab: 'about',
workspaceId: null,
});
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:help-getting-started',
category: 'affine:help',
icon: <UserGuideIcon />,
label: () => t['com.affine.cmdk.affine.getting-started'](),
preconditionStrategy: () => environment.isDesktop,
run() {
store.set(openOnboardingModalAtom, true);
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,40 @@
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SidebarIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
export function registerAffineLayoutCommands({
t,
store,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:toggle-left-sidebar',
category: 'affine:layout',
icon: <SidebarIcon />,
label: () => {
const open = store.get(appSidebarOpenAtom);
return t[
open
? 'com.affine.cmdk.affine.left-sidebar.collapse'
: 'com.affine.cmdk.affine.left-sidebar.expand'
]();
},
keyBinding: {
binding: '$mod+/',
},
run() {
store.set(appSidebarOpenAtom, v => !v);
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,120 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
import {
openSettingModalAtom,
openWorkspaceListModalAtom,
type PageModeOption,
} from '../atoms';
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../shared';
export function registerAffineNavigationCommands({
t,
store,
workspace,
navigationHelper,
pageListMode,
setPageListMode,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
navigationHelper: ReturnType<typeof useNavigateHelper>;
pageListMode: PageModeOption;
setPageListMode: React.Dispatch<React.SetStateAction<PageModeOption>>;
workspace: Workspace;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:goto-all-pages',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setPageListMode('all');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-page-list',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
preconditionStrategy: () => {
return pageListMode !== 'page';
},
label: () => t['com.affine.cmdk.affine.navigation.goto-page-list'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setPageListMode('page');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-edgeless-list',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
preconditionStrategy: () => {
return pageListMode !== 'edgeless';
},
label: () => t['com.affine.cmdk.affine.navigation.goto-edgeless-list'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setPageListMode('edgeless');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-workspace',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.goto-workspace'](),
run() {
store.set(openWorkspaceListModalAtom, true);
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:open-settings',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.open-settings'](),
run() {
store.set(openSettingModalAtom, {
activeTab: 'appearance',
workspaceId: null,
open: true,
});
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-trash',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.goto-trash'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
setPageListMode('all');
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,341 @@
import { Trans } from '@affine/i18n';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SettingsIcon } from '@blocksuite/icons';
import {
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra/command';
import { type createStore, useAtomValue } from 'jotai';
import type { useTheme } from 'next-themes';
import { openQuickSearchModalAtom } from '../atoms';
import { appSettingAtom } from '../atoms/settings';
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
// todo - find a better way to abstract the following translations components
const ClientBorderStyleLabel = () => {
const { clientBorder } = useAtomValue(appSettingAtom);
return (
<Trans
i18nKey="com.affine.cmdk.affine.client-border-style.to"
values={{
state: clientBorder ? 'OFF' : 'ON',
}}
>
Change Client Border Style to
<strong>state</strong>
</Trans>
);
};
const FullWidthLayoutLabel = () => {
const { fullWidthLayout } = useAtomValue(appSettingAtom);
return (
<Trans
i18nKey="com.affine.cmdk.affine.full-width-layout.to"
values={{
state: fullWidthLayout ? 'OFF' : 'ON',
}}
>
Change Full Width Layout to
<strong>state</strong>
</Trans>
);
};
const NoisyBackgroundLabel = () => {
const { enableNoisyBackground } = useAtomValue(appSettingAtom);
return (
<Trans
i18nKey="com.affine.cmdk.affine.noise-background-on-the-sidebar.to"
values={{
state: enableNoisyBackground ? 'OFF' : 'ON',
}}
>
Change Noise Background On The Sidebar to <strong>state</strong>
</Trans>
);
};
const BlurBackgroundLabel = () => {
const { enableBlurBackground } = useAtomValue(appSettingAtom);
return (
<Trans
i18nKey="com.affine.cmdk.affine.translucent-ui-on-the-sidebar.to"
values={{
state: enableBlurBackground ? 'OFF' : 'ON',
}}
>
Change Translucent UI On The Sidebar to <strong>state</strong>
</Trans>
);
};
export function registerAffineSettingsCommands({
t,
store,
theme,
languageHelper,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
theme: ReturnType<typeof useTheme>;
languageHelper: ReturnType<typeof useLanguageHelper>;
}) {
const unsubs: Array<() => void> = [];
const { onSelect, languagesList, currentLanguage } = languageHelper;
unsubs.push(
registerAffineCommand({
id: 'affine:show-quick-search',
preconditionStrategy: PreconditionStrategy.Never,
category: 'affine:general',
keyBinding: {
binding: '$mod+K',
},
icon: <SettingsIcon />,
run() {
const quickSearchModalState = store.get(openQuickSearchModalAtom);
store.set(openQuickSearchModalAtom, !quickSearchModalState);
},
})
);
// color schemes
unsubs.push(
registerAffineCommand({
id: 'affine:change-color-scheme-to-auto',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.color-scheme.to"
values={{ colour: 'Auto' }}
>
Change Colour Scheme to <strong>colour</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => theme.theme !== 'system',
run() {
theme.setTheme('system');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:change-color-scheme-to-dark',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.color-scheme.to"
values={{ colour: 'Dark' }}
>
Change Colour Scheme to <strong>colour</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => theme.theme !== 'dark',
run() {
theme.setTheme('dark');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:change-color-scheme-to-light',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.color-scheme.to"
values={{ colour: 'Light' }}
>
Change Colour Scheme to <strong>colour</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => theme.theme !== 'light',
run() {
theme.setTheme('light');
},
})
);
//Font styles
unsubs.push(
registerAffineCommand({
id: 'affine:change-font-style-to-sans',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.font-style.to"
values={{
fontFamily: t['com.affine.appearanceSettings.fontStyle.sans'](),
}}
>
Change Font Style to <strong>fontFamily</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () =>
store.get(appSettingAtom).fontStyle !== 'Sans',
run() {
store.set(appSettingAtom, prev => ({
...prev,
fontStyle: 'Sans',
}));
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:change-font-style-to-serif',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.font-style.to"
values={{
fontFamily: t['com.affine.appearanceSettings.fontStyle.serif'](),
}}
>
Change Font Style to
<strong style={{ fontFamily: 'var(--affine-font-serif-family)' }}>
fontFamily
</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () =>
store.get(appSettingAtom).fontStyle !== 'Serif',
run() {
store.set(appSettingAtom, prev => ({
...prev,
fontStyle: 'Serif',
}));
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:change-font-style-to-mono',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.font-style.to"
values={{
fontFamily: t['com.affine.appearanceSettings.fontStyle.mono'](),
}}
>
Change Font Style to
<strong style={{ fontFamily: 'var(--affine-font-mono-family)' }}>
fontFamily
</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () =>
store.get(appSettingAtom).fontStyle !== 'Mono',
run() {
store.set(appSettingAtom, prev => ({
...prev,
fontStyle: 'Mono',
}));
},
})
);
//Display Language
languagesList.forEach(language => {
unsubs.push(
registerAffineCommand({
id: `affine:change-display-language-to-${language.name}`,
label: (
<Trans
i18nKey="com.affine.cmdk.affine.display-language.to"
values={{
language: language.originalName,
}}
>
Change Display Language to
<strong>language</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => currentLanguage?.tag !== language.tag,
run() {
onSelect(language.tag);
},
})
);
});
//Layout Style
unsubs.push(
registerAffineCommand({
id: `affine:change-client-border-style`,
label: <ClientBorderStyleLabel />,
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => environment.isDesktop,
run() {
store.set(appSettingAtom, prev => ({
...prev,
clientBorder: !prev.clientBorder,
}));
},
})
);
unsubs.push(
registerAffineCommand({
id: `affine:change-full-width-layout`,
label: <FullWidthLayoutLabel />,
category: 'affine:settings',
icon: <SettingsIcon />,
run() {
store.set(appSettingAtom, prev => ({
...prev,
fullWidthLayout: !prev.fullWidthLayout,
}));
},
})
);
unsubs.push(
registerAffineCommand({
id: `affine:change-noise-background-on-the-sidebar`,
label: <NoisyBackgroundLabel />,
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => environment.isDesktop,
run() {
store.set(appSettingAtom, prev => ({
...prev,
enableNoisyBackground: !prev.enableNoisyBackground,
}));
},
})
);
unsubs.push(
registerAffineCommand({
id: `affine:change-translucent-ui-on-the-sidebar`,
label: <BlurBackgroundLabel />,
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => environment.isDesktop,
run() {
store.set(appSettingAtom, prev => ({
...prev,
enableBlurBackground: !prev.enableBlurBackground,
}));
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,35 @@
import { updateReadyAtom } from '@affine/component/app-sidebar/app-updater-button';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ResetIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
export function registerAffineUpdatesCommands({
t,
store,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:restart-to-upgrade',
category: 'affine:updates',
icon: <ResetIcon />,
label: () => t['com.affine.cmdk.affine.restart-to-upgrade'](),
preconditionStrategy: () => !!store.get(updateReadyAtom),
run() {
window.apis?.updater.quitAndInstall().catch(err => {
// TODO: add error toast here
console.error(err);
});
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,6 @@
export * from './affine-creation';
export * from './affine-help';
export * from './affine-layout';
export * from './affine-navigation';
export * from './affine-settings';
export * from './affine-updates';

View File

@@ -11,7 +11,11 @@ export const AppContainer = (props: WorkspaceRootProps) => {
return (
<AppContainerWithoutSettings
useNoisyBackground={appSettings.enableNoisyBackground}
useBlurBackground={!appSettings.enableBlurBackground}
useBlurBackground={
appSettings.enableBlurBackground &&
environment.isDesktop &&
environment.isMacOs
}
{...props}
/>
);

View File

@@ -1,23 +1,35 @@
import {
AuthContent,
BackButton,
CountDownRender,
ModalHeader,
ResendButton,
} from '@affine/component/auth-components';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { signIn } from 'next-auth/react';
import { type FC, useCallback } from 'react';
import { Button } from '@toeverything/components/button';
import { useCallback } from 'react';
import { buildCallbackUrl } from './callback-url';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
import { useAuth } from './use-auth';
export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
export const AfterSignInSendEmail = ({
setAuthState,
email,
}) => {
onSignedIn,
}: AuthPanelProps) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
const { resendCountDown, allowSendEmail, signIn } = useAuth();
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
const onResendClick = useCallback(async () => {
await signIn(email);
}, [email, signIn]);
return (
<>
@@ -31,15 +43,23 @@ export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
{t['com.affine.auth.sign.sent.email.message.end']()}
</AuthContent>
<ResendButton
onClick={useCallback(() => {
signIn('email', {
email,
callbackUrl: buildCallbackUrl('signIn'),
redirect: true,
}).catch(console.error);
}, [email])}
/>
<div className={style.resendWrapper}>
{allowSendEmail ? (
<Button type="plain" size="large" onClick={onResendClick}>
{t['com.affine.auth.sign.auth.code.resend.hint']()}
</Button>
) : (
<>
<span className="resend-code-hint">
{t['com.affine.auth.sign.auth.code.on.resend.hint']()}
</span>
<CountDownRender
className={style.resendCountdown}
timeLeft={resendCountDown}
/>
</>
)}
</div>
<div className={style.authMessage} style={{ marginTop: 20 }}>
{/*prettier-ignore*/}

View File

@@ -1,22 +1,35 @@
import {
AuthContent,
BackButton,
CountDownRender,
ModalHeader,
ResendButton,
} from '@affine/component/auth-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { signIn } from 'next-auth/react';
import { Button } from '@toeverything/components/button';
import { type FC, useCallback } from 'react';
import { buildCallbackUrl } from './callback-url';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
import { useAuth } from './use-auth';
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
setAuthState,
email,
onSignedIn,
}) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
const { resendCountDown, allowSendEmail, signUp } = useAuth();
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
const onResendClick = useCallback(async () => {
await signUp(email);
}, [email, signUp]);
return (
<>
@@ -30,15 +43,23 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
{t['com.affine.auth.sign.sent.email.message.end']()}
</AuthContent>
<ResendButton
onClick={useCallback(() => {
signIn('email', {
email: email,
callbackUrl: buildCallbackUrl('signUp'),
redirect: true,
}).catch(console.error);
}, [email])}
/>
<div className={style.resendWrapper}>
{allowSendEmail ? (
<Button type="plain" size="large" onClick={onResendClick}>
{t['com.affine.auth.sign.auth.code.resend.hint']()}
</Button>
) : (
<>
<span className="resend-code-hint">
{t['com.affine.auth.sign.auth.code.on.resend.hint']()}
</span>
<CountDownRender
className={style.resendCountdown}
timeLeft={resendCountDown}
/>
</>
)}
</div>
<div className={style.authMessage} style={{ marginTop: 20 }}>
{t['com.affine.auth.sign.auth.code.message']()}

View File

@@ -1,16 +0,0 @@
import { isDesktop } from '@affine/env/constant';
type Action = 'signUp' | 'changePassword' | 'signIn' | 'signUp';
export function buildCallbackUrl(action: Action) {
const callbackUrl = `/auth/${action}`;
const params: string[][] = [];
if (isDesktop && window.appInfo.schema) {
params.push(['schema', window.appInfo.schema]);
}
const query =
params.length > 0
? '?' + params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
: '';
return callbackUrl + query;
}

View File

@@ -2,12 +2,11 @@ import {
AuthModal as AuthModalBase,
type AuthModalProps as AuthModalBaseProps,
} from '@affine/component/auth-components';
import { isDesktop } from '@affine/env/constant';
import { atom, useAtom } from 'jotai';
import { type FC, useCallback, useEffect, useMemo } from 'react';
import { type FC, useCallback, useMemo } from 'react';
import { AfterSignInSendEmail } from './after-sign-in-send-email';
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
import { NoAccess } from './no-access';
import { SendEmail } from './send-email';
import { SignIn } from './sign-in';
import { SignInWithPassword } from './sign-in-with-password';
@@ -19,7 +18,8 @@ export type AuthProps = {
| 'afterSignInSendEmail'
// throw away
| 'signInWithPassword'
| 'sendEmail';
| 'sendEmail'
| 'noAccess';
setAuthState: (state: AuthProps['state']) => void;
setAuthEmail: (state: AuthProps['email']) => void;
setEmailType: (state: AuthProps['emailType']) => void;
@@ -35,8 +35,6 @@ export type AuthPanelProps = {
setEmailType: AuthProps['setEmailType'];
emailType: AuthProps['emailType'];
onSignedIn?: () => void;
authStore: AuthStoreAtom;
setAuthStore: (data: Partial<AuthStoreAtom>) => void;
};
const config: {
@@ -47,17 +45,9 @@ const config: {
afterSignInSendEmail: AfterSignInSendEmail,
signInWithPassword: SignInWithPassword,
sendEmail: SendEmail,
noAccess: NoAccess,
};
type AuthStoreAtom = {
hasSentEmail: boolean;
resendCountDown: number;
};
export const authStoreAtom = atom<AuthStoreAtom>({
hasSentEmail: false,
resendCountDown: 60,
});
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
open,
state,
@@ -68,27 +58,6 @@ export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
setEmailType,
emailType,
}) => {
const [, setAuthStore] = useAtom(authStoreAtom);
useEffect(() => {
if (!open) {
setAuthStore({
hasSentEmail: false,
resendCountDown: 60,
});
setAuthEmail('');
}
}, [open, setAuthEmail, setAuthStore]);
useEffect(() => {
if (isDesktop) {
return window.events?.ui.onFinishLogin(() => {
setOpen(false);
});
}
return;
}, [setOpen]);
const onSignedIn = useCallback(() => {
setOpen(false);
}, [setOpen]);
@@ -117,39 +86,18 @@ export const AuthPanel: FC<AuthProps> = ({
emailType,
onSignedIn,
}) => {
const [authStore, setAuthStore] = useAtom(authStoreAtom);
const CurrentPanel = useMemo(() => {
return config[state];
}, [state]);
useEffect(() => {
return () => {
setAuthStore({
hasSentEmail: false,
resendCountDown: 60,
});
};
}, [setAuthEmail, setAuthStore]);
return (
<CurrentPanel
email={email}
setAuthState={setAuthState}
setAuthEmail={setAuthEmail}
setEmailType={setEmailType}
authStore={authStore}
emailType={emailType}
onSignedIn={onSignedIn}
setAuthStore={useCallback(
(data: Partial<AuthStoreAtom>) => {
setAuthStore(prev => ({
...prev,
...data,
}));
},
[setAuthStore]
)}
/>
);
};

View File

@@ -0,0 +1,53 @@
import {
AuthContent,
BackButton,
ModalHeader,
} from '@affine/component/auth-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { NewIcon } from '@blocksuite/icons';
import { type FC, useCallback } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
export const NoAccess: FC<AuthPanelProps> = ({ setAuthState, onSignedIn }) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
return (
<>
<ModalHeader
title={t['com.affine.brand.affineCloud']()}
subTitle={t['Early Access Stage']()}
/>
<AuthContent style={{ height: 162 }}>
{t['com.affine.auth.sign.no.access.hint']()}
<a href="https://community.affine.pro/c/insider-general/">
{t['com.affine.auth.sign.no.access.link']()}
</a>
</AuthContent>
<div className={style.accessMessage}>
<NewIcon
style={{
fontSize: 16,
marginRight: 4,
color: 'var(--affine-icon-color)',
}}
/>
{t['com.affine.auth.sign.no.access.wait']()}
</div>
<BackButton
onClick={useCallback(() => {
setAuthState('signIn');
}, [setAuthState])}
/>
</>
);
};

View File

@@ -6,7 +6,6 @@ import {
ModalHeader,
} from '@affine/component/auth-components';
import { pushNotificationAtom } from '@affine/component/notification-center';
import { isDesktop } from '@affine/env/constant';
import {
sendChangeEmailMutation,
sendChangePasswordEmailMutation,
@@ -16,7 +15,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai/react';
import { type FC, useCallback } from 'react';
import { useCallback, useState } from 'react';
import type { AuthPanelProps } from './index';
@@ -32,6 +31,20 @@ const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
return t['com.affine.settings.email.action']();
}
};
const useContent = (emailType: AuthPanelProps['emailType'], email: string) => {
const t = useAFFiNEI18N();
switch (emailType) {
case 'setPassword':
return t['com.affine.auth.set.password.message']();
case 'changePassword':
return t['com.affine.auth.set.password.message']();
case 'changeEmail':
return t['com.affine.auth.change.email.message']({
email,
});
}
};
const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
const t = useAFFiNEI18N();
@@ -104,7 +117,7 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
return trigger({
email,
callbackUrl: `/auth/${callbackUrl}?isClient=${
isDesktop ? 'true' : 'false'
environment.isDesktop ? 'true' : 'false'
}`,
});
},
@@ -118,18 +131,18 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
};
};
export const SendEmail: FC<AuthPanelProps> = ({
export const SendEmail = ({
setAuthState,
setAuthStore,
email,
authStore: { hasSentEmail },
emailType,
}) => {
}: AuthPanelProps) => {
const t = useAFFiNEI18N();
const [hasSentEmail, setHasSentEmail] = useState(false);
const pushNotification = useSetAtom(pushNotificationAtom);
const title = useEmailTitle(emailType);
const hint = useNotificationHint(emailType);
const content = useContent(emailType, email);
const buttonContent = useButtonContent(emailType);
const { loading, sendEmail } = useSendEmail(emailType);
@@ -143,13 +156,16 @@ export const SendEmail: FC<AuthPanelProps> = ({
key: Date.now().toString(),
type: 'success',
});
setAuthStore({ hasSentEmail: true });
}, [email, hint, pushNotification, sendEmail, setAuthStore]);
setHasSentEmail(true);
}, [email, hint, pushNotification, sendEmail]);
return (
<>
<ModalHeader title={t['AFFiNE Cloud']()} subTitle={title} />
<AuthContent>{t['com.affine.auth.reset.password.message']()}</AuthContent>
<ModalHeader
title={t['com.affine.brand.affineCloud']()}
subTitle={title}
/>
<AuthContent>{content}</AuthContent>
<Wrapper
marginTop={30}

View File

@@ -4,15 +4,14 @@ import {
BackButton,
ModalHeader,
} from '@affine/component/auth-components';
import { pushNotificationAtom } from '@affine/component/notification-center';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { signIn, useSession } from 'next-auth/react';
import { useSession } from 'next-auth/react';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { signInCloud } from '../../../utils/cloud-utils';
import type { AuthPanelProps } from './index';
import { forgetPasswordButton } from './style.css';
@@ -24,13 +23,11 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
const t = useAFFiNEI18N();
const { update } = useSession();
const pushNotification = useSetAtom(pushNotificationAtom);
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState(false);
const onSignIn = useCallback(async () => {
const res = await signIn('credentials', {
const res = await signInCloud('credentials', {
redirect: false,
email,
password,
@@ -42,19 +39,13 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
await update();
onSignedIn?.();
pushNotification({
title: `${email}${t['com.affine.auth.has.signed']()}`,
message: '',
key: Date.now().toString(),
type: 'success',
});
}, [email, password, pushNotification, onSignedIn, t, update]);
}, [email, password, onSignedIn, update]);
return (
<>
<ModalHeader
title={t['com.affine.auth.sign.in']()}
subTitle={t['AFFiNE Cloud']()}
subTitle={t['com.affine.brand.affineCloud']()}
/>
<Wrapper

View File

@@ -1,51 +1,55 @@
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
import { pushNotificationAtom } from '@affine/component/notification-center';
import type { Notification } from '@affine/component/notification-center/index.jotai';
import { getUserQuery } from '@affine/graphql';
import {
AuthInput,
CountDownRender,
ModalHeader,
} from '@affine/component/auth-components';
import { type GetUserQuery, getUserQuery } from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai';
import { signIn, type SignInResponse } from 'next-auth/react';
import { GraphQLError } from 'graphql';
import { type FC, useState } from 'react';
import { useCallback } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import { emailRegex } from '../../../utils/email-regex';
import { buildCallbackUrl } from './callback-url';
import type { AuthPanelProps } from './index';
import * as style from './style.css';
import { INTERNAL_BETA_URL, useAuth } from './use-auth';
function validateEmail(email: string) {
return emailRegex.test(email);
}
function handleSendEmailError(
res: SignInResponse | undefined,
pushNotification: (notification: Notification) => void
) {
if (res?.error) {
pushNotification({
title: 'Send email error',
message: 'Please back to home and try again',
type: 'error',
});
}
}
export const SignIn: FC<AuthPanelProps> = ({
setAuthState,
setAuthEmail,
email,
onSignedIn,
}) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
const {
isMutating: isSigningIn,
resendCountDown,
allowSendEmail,
signIn,
signUp,
signInWithGoogle,
} = useAuth();
const { trigger: verifyUser, isMutating } = useMutation({
mutation: getUserQuery,
});
const [isValidEmail, setIsValidEmail] = useState(true);
const pushNotification = useSetAtom(pushNotificationAtom);
if (loginStatus === 'authenticated') {
onSignedIn?.();
}
const onContinue = useCallback(async () => {
if (!validateEmail(email)) {
setIsValidEmail(false);
@@ -53,35 +57,47 @@ export const SignIn: FC<AuthPanelProps> = ({
}
setIsValidEmail(true);
const { user } = await verifyUser({ email });
setAuthEmail(email);
if (user) {
signIn('email', {
email: email,
callbackUrl: buildCallbackUrl('signIn'),
redirect: false,
// 0 for no access for internal beta
let user: GetUserQuery['user'] | null | 0 = null;
await verifyUser({ email })
.then(({ user: u }) => {
user = u;
})
.then(res => handleSendEmailError(res, pushNotification))
.catch(console.error);
.catch(err => {
const e = err?.[0];
if (e instanceof GraphQLError && e.extensions?.code === 402) {
setAuthState('noAccess');
user = 0;
} else {
throw err;
}
});
if (user === 0) {
return;
}
setAuthEmail(email);
if (user) {
const res = await signIn(email);
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
return setAuthState('noAccess');
}
setAuthState('afterSignInSendEmail');
} else {
signIn('email', {
email: email,
callbackUrl: buildCallbackUrl('signUp'),
redirect: false,
})
.then(res => handleSendEmailError(res, pushNotification))
.catch(console.error);
const res = await signUp(email);
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
return setAuthState('noAccess');
}
setAuthState('afterSignUpSendEmail');
}
}, [email, setAuthEmail, setAuthState, verifyUser, pushNotification]);
}, [email, setAuthEmail, setAuthState, signIn, signUp, verifyUser]);
return (
<>
<ModalHeader
title={t['com.affine.auth.sign.in']()}
subTitle={t['AFFiNE Cloud']()}
subTitle={t['com.affine.brand.affineCloud']()}
/>
<Button
@@ -93,8 +109,8 @@ export const SignIn: FC<AuthPanelProps> = ({
}}
icon={<GoogleDuotoneIcon />}
onClick={useCallback(() => {
signIn('google').catch(console.error);
}, [])}
signInWithGoogle();
}, [signInWithGoogle])}
>
{t['Continue with Google']()}
</Button>
@@ -121,16 +137,24 @@ export const SignIn: FC<AuthPanelProps> = ({
size="extraLarge"
data-testid="continue-login-button"
block
loading={isMutating}
loading={isMutating || isSigningIn}
disabled={!allowSendEmail}
icon={
<ArrowDownBigIcon
width={20}
height={20}
style={{
transform: 'rotate(-90deg)',
color: 'var(--affine-blue)',
}}
/>
allowSendEmail || isMutating ? (
<ArrowDownBigIcon
width={20}
height={20}
style={{
transform: 'rotate(-90deg)',
color: 'var(--affine-blue)',
}}
/>
) : (
<CountDownRender
className={style.resendCountdownInButton}
timeLeft={resendCountDown}
/>
)
}
iconPosition="end"
onClick={onContinue}

View File

@@ -26,3 +26,32 @@ export const forgetPasswordButton = style({
bottom: 0,
display: 'none',
});
export const resendWrapper = style({
height: 32,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 30,
});
export const resendCountdown = style({ width: 45, textAlign: 'center' });
export const resendCountdownInButton = style({
width: 40,
textAlign: 'center',
fontSize: 'var(--affine-font-sm)',
marginLeft: 16,
color: 'var(--affine-blue)',
fontWeight: 400,
});
export const accessMessage = style({
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: 'var(--affine-font-xs)',
fontWeight: 500,
marginTop: 65,
marginBottom: 40,
});

View File

@@ -0,0 +1,137 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import type { Notification } from '@affine/component/notification-center/index.jotai';
import { atom, useAtom, useSetAtom } from 'jotai';
import { type SignInResponse } from 'next-auth/react';
import { useCallback } from 'react';
import { signInCloud } from '../../../utils/cloud-utils';
const COUNT_DOWN_TIME = 60;
export const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
function handleSendEmailError(
res: SignInResponse | undefined | void,
pushNotification: (notification: Notification) => void
) {
if (res?.error) {
pushNotification({
title: 'Send email error',
message: 'Please back to home and try again',
type: 'error',
});
}
}
type AuthStoreAtom = {
allowSendEmail: boolean;
resendCountDown: number;
isMutating: boolean;
};
export const authStoreAtom = atom<AuthStoreAtom>({
isMutating: false,
allowSendEmail: true,
resendCountDown: COUNT_DOWN_TIME,
});
const countDownAtom = atom(
null, // it's a convention to pass `null` for the first argument
(get, set) => {
const clearId = window.setInterval(() => {
const countDown = get(authStoreAtom).resendCountDown;
if (countDown === 0) {
set(authStoreAtom, {
isMutating: false,
allowSendEmail: true,
resendCountDown: COUNT_DOWN_TIME,
});
window.clearInterval(clearId);
return;
}
set(authStoreAtom, {
isMutating: false,
resendCountDown: countDown - 1,
allowSendEmail: false,
});
}, 1000);
}
);
export const useAuth = () => {
const pushNotification = useSetAtom(pushNotificationAtom);
const [authStore, setAuthStore] = useAtom(authStoreAtom);
const startResendCountDown = useSetAtom(countDownAtom);
const signIn = useCallback(
async (email: string) => {
setAuthStore(prev => {
return {
...prev,
isMutating: true,
};
});
const res = await signInCloud('email', {
email: email,
callbackUrl: '/auth/signIn',
redirect: false,
}).catch(console.error);
handleSendEmailError(res, pushNotification);
setAuthStore({
isMutating: false,
allowSendEmail: false,
resendCountDown: COUNT_DOWN_TIME,
});
startResendCountDown();
return res;
},
[pushNotification, setAuthStore, startResendCountDown]
);
const signUp = useCallback(
async (email: string) => {
setAuthStore(prev => {
return {
...prev,
isMutating: true,
};
});
const res = await signInCloud('email', {
email: email,
callbackUrl: '/auth/signUp',
redirect: false,
}).catch(console.error);
handleSendEmailError(res, pushNotification);
setAuthStore({
isMutating: false,
allowSendEmail: false,
resendCountDown: COUNT_DOWN_TIME,
});
startResendCountDown();
return res;
},
[pushNotification, setAuthStore, startResendCountDown]
);
const signInWithGoogle = useCallback(() => {
signInCloud('google').catch(console.error);
}, []);
return {
allowSendEmail: authStore.allowSendEmail,
resendCountDown: authStore.resendCountDown,
isMutating: authStore.isMutating,
signUp,
signIn,
signInWithGoogle,
};
};

View File

@@ -2,7 +2,7 @@ import { globalStyle, style } from '@vanilla-extract/css';
export const header = style({
position: 'relative',
height: '44px',
marginTop: '44px',
});
export const content = style({

View File

@@ -1,14 +1,13 @@
import {
Input,
Modal,
ModalCloseButton,
ModalWrapper,
toast,
} from '@affine/component';
import { Input, toast } from '@affine/component';
import { DebugLogger } from '@affine/debug';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { HelpIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import {
ConfirmModal,
type ConfirmModalProps,
Modal,
} from '@toeverything/components/modal';
import { Tooltip } from '@toeverything/components/tooltip';
import type {
LoadDBFileResult,
@@ -16,7 +15,7 @@ import type {
} from '@toeverything/infra/type';
import { useSetAtom } from 'jotai';
import type { KeyboardEvent } from 'react';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { useLayoutEffect } from 'react';
import { useCallback, useState } from 'react';
@@ -39,14 +38,13 @@ interface ModalProps {
onCreate: (id: string) => void;
}
interface NameWorkspaceContentProps {
onClose: () => void;
interface NameWorkspaceContentProps extends ConfirmModalProps {
onConfirmName: (name: string) => void;
}
const NameWorkspaceContent = ({
onConfirmName,
onClose,
...props
}: NameWorkspaceContentProps) => {
const [workspaceName, setWorkspaceName] = useState('');
@@ -64,9 +62,23 @@ const NameWorkspaceContent = ({
);
const t = useAFFiNEI18N();
return (
<div className={style.content}>
<div className={style.contentTitle}>{t['Name Your Workspace']()}</div>
<p>{t['Workspace description']()}</p>
<ConfirmModal
defaultOpen={true}
title={t['com.affine.nameWorkspace.title']()}
description={t['com.affine.nameWorkspace.description']()}
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
confirmButtonOptions={{
type: 'primary',
disabled: !workspaceName,
['data-testid' as string]: 'create-workspace-create-button',
children: t['com.affine.nameWorkspace.button.create'](),
}}
closeButtonOptions={{
['data-testid' as string]: 'create-workspace-close-button',
}}
onConfirm={handleCreateWorkspace}
{...props}
>
<Input
ref={ref => {
if (ref) {
@@ -75,28 +87,13 @@ const NameWorkspaceContent = ({
}}
data-testid="create-workspace-input"
onKeyDown={handleKeyDown}
placeholder={t['Set a Workspace name']()}
placeholder={t['com.affine.nameWorkspace.placeholder']()}
maxLength={64}
minLength={0}
onChange={setWorkspaceName}
size="large"
/>
<div className={style.buttonGroup}>
<Button data-testid="create-workspace-close-button" onClick={onClose}>
{t.Cancel()}
</Button>
<Button
data-testid="create-workspace-create-button"
disabled={!workspaceName}
style={{
opacity: !workspaceName ? 0.5 : 1,
}}
type="primary"
onClick={handleCreateWorkspace}
>
{t.Create()}
</Button>
</div>
</div>
</ConfirmModal>
);
};
@@ -125,7 +122,6 @@ const SetDBLocationContent = ({
onConfirmLocation,
}: SetDBLocationContentProps) => {
const t = useAFFiNEI18N();
const ref = useRef(null);
const defaultDBLocation = useDefaultDBLocation();
const [opening, setOpening] = useState(false);
@@ -150,8 +146,10 @@ const SetDBLocationContent = ({
return (
<div className={style.content}>
<div className={style.contentTitle}>{t['Set database location']()}</div>
<p>{t['Workspace database storage description']()}</p>
<div className={style.contentTitle}>
{t['com.affine.setDBLocation.title']()}
</div>
<p>{t['com.affine.setDBLocation.description']()}</p>
<div className={style.buttonGroup}>
<Button
disabled={opening}
@@ -159,15 +157,12 @@ const SetDBLocationContent = ({
type="primary"
onClick={handleSelectDBFileLocation}
>
{t['Customize']()}
{t['com.affine.setDBLocation.button.customize']()}
</Button>
<Tooltip
content={t['Default db location hint']({
content={t['com.affine.setDBLocation.tooltip.defaultLocation']({
location: defaultDBLocation,
})}
portalOptions={{
container: ref.current,
}}
>
<Button
data-testid="create-workspace-default-location-button"
@@ -177,9 +172,8 @@ const SetDBLocationContent = ({
}}
icon={<HelpIcon />}
iconPosition="end"
ref={ref}
>
{t['Default Location']()}
{t['com.affine.setDBLocation.button.defaultLocation']()}
</Button>
</Tooltip>
</div>
@@ -201,7 +195,9 @@ const SetSyncingModeContent = ({
return (
<div className={style.content}>
<div className={style.contentTitle}>
{t[mode === 'new' ? 'Created Successfully' : 'Added Successfully']()}
{mode === 'new'
? t['com.affine.setSyncingMode.title.created']()
: t['com.affine.setSyncingMode.title.added']()}
</div>
<div className={style.radioGroup}>
@@ -212,7 +208,7 @@ const SetSyncingModeContent = ({
readOnly
checked={!enableCloudSyncing}
/>
{t['Use on current device only']()}
{t['com.affine.setSyncingMode.deviceOnly']()}
</label>
<label onClick={() => setEnableCloudSyncing(true)}>
<input
@@ -221,7 +217,7 @@ const SetSyncingModeContent = ({
readOnly
checked={enableCloudSyncing}
/>
{t['Sync across devices with AFFiNE Cloud']()}
{t['com.affine.setSyncingMode.cloud']()}
</label>
</div>
@@ -233,7 +229,7 @@ const SetSyncingModeContent = ({
onConfirmMode(enableCloudSyncing);
}}
>
{t['Continue']()}
{t['com.affine.setSyncingMode.button.continue']()}
</Button>
</div>
</div>
@@ -273,7 +269,8 @@ export const CreateWorkspaceModal = ({
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
if (result.workspaceId && !canceled) {
setAddedId(result.workspaceId);
setStep('set-syncing-mode');
const newWorkspaceId = await addLocalWorkspace(result.workspaceId);
onCreate(newWorkspaceId);
} else if (result.error || result.canceled) {
if (result.error) {
toast(t[result.error]());
@@ -284,18 +281,14 @@ export const CreateWorkspaceModal = ({
console.error(err);
});
} else if (mode === 'new') {
setStep(
environment.isDesktop && runtimeConfig.enableSQLiteProvider
? 'set-db-location'
: 'name-workspace'
);
setStep('name-workspace');
} else {
setStep(undefined);
}
return () => {
canceled = true;
};
}, [mode, onClose, t]);
}, [addLocalWorkspace, mode, onClose, onCreate, t]);
const onConfirmEnableCloudSyncing = useCallback(
(enableCloudSyncing: boolean) => {
@@ -340,32 +333,15 @@ export const CreateWorkspaceModal = ({
const onConfirmName = useCallback(
(name: string) => {
setWorkspaceName(name);
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
setStep('set-syncing-mode');
} else {
// this will be the last step for web for now
// fix me later
createLocalWorkspace(name)
.then(id => {
onCreate(id);
})
.catch(err => {
logger.error(err);
});
}
// this will be the last step for web for now
// fix me later
createLocalWorkspace(name).then(id => {
onCreate(id);
});
},
[createLocalWorkspace, onCreate]
);
const nameWorkspaceNode =
step === 'name-workspace' ? (
<NameWorkspaceContent
// go to previous step instead?
onClose={onClose}
onConfirmName={onConfirmName}
/>
) : null;
const setDBLocationNode =
step === 'set-db-location' ? (
<SetDBLocationContent
@@ -384,16 +360,36 @@ export const CreateWorkspaceModal = ({
/>
) : null;
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) {
onClose();
}
},
[onClose]
);
if (step === 'name-workspace') {
return (
<NameWorkspaceContent
open={mode !== false && !!step}
onOpenChange={onOpenChange}
onConfirmName={onConfirmName}
/>
);
}
return (
<Modal open={mode !== false && !!step} onClose={onClose}>
<ModalWrapper width={560} style={{ padding: '10px' }}>
<div className={style.header}>
<ModalCloseButton top={6} right={6} onClick={onClose} />
</div>
{nameWorkspaceNode}
{setDBLocationNode}
{setSyncingModeNode}
</ModalWrapper>
<Modal
open={mode !== false && !!step}
width={560}
onOpenChange={onOpenChange}
contentOptions={{
style: { padding: '10px' },
}}
>
<div className={style.header}></div>
{setDBLocationNode}
{setSyncingModeNode}
</Modal>
);
};

View File

@@ -1,53 +1,59 @@
import { Modal, ModalWrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import { Button, IconButton } from '@toeverything/components/button';
import {
ConfirmModal,
type ConfirmModalProps,
} from '@toeverything/components/modal';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { ButtonContainer, Content, Header, StyleTips, Title } from './style';
interface EnableAffineCloudModalProps {
open: boolean;
onConfirm: () => void;
onClose: () => void;
}
import { authAtom } from '../../../atoms';
import { setOnceSignedInEventAtom } from '../../../atoms/event';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
export const EnableAffineCloudModal = ({
onConfirm,
open,
onClose,
}: EnableAffineCloudModalProps) => {
onConfirm: propsOnConfirm,
...props
}: ConfirmModalProps) => {
const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus();
const setAuthAtom = useSetAtom(authAtom);
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
const confirm = useCallback(async () => {
return propsOnConfirm?.();
}, [propsOnConfirm]);
const onConfirm = useCallback(() => {
if (loginStatus === 'unauthenticated') {
setAuthAtom(prev => ({
...prev,
openModal: true,
}));
setOnceSignedInEvent(confirm);
}
if (loginStatus === 'authenticated') {
return propsOnConfirm?.();
}
}, [confirm, loginStatus, propsOnConfirm, setAuthAtom, setOnceSignedInEvent]);
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={480}>
<Header>
<Title>{t['Enable AFFiNE Cloud']()}</Title>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</Header>
<Content>
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
<ButtonContainer>
<div>
<Button onClick={onClose} block>
{t['Cancel']()}
</Button>
</div>
<div>
<Button
data-testid="confirm-enable-affine-cloud-button"
type="primary"
block
onClick={onConfirm}
>
{t['Sign in and Enable']()}
</Button>
</div>
</ButtonContainer>
</Content>
</ModalWrapper>
</Modal>
<ConfirmModal
title={t['Enable AFFiNE Cloud']()}
description={t['Enable AFFiNE Cloud Description']()}
cancelText={t['com.affine.enableAffineCloudModal.button.cancel']()}
onConfirm={onConfirm}
confirmButtonOptions={{
type: 'primary',
['data-testid' as string]: 'confirm-enable-affine-cloud-button',
children:
loginStatus === 'authenticated'
? t['Enable']()
: t['Sign in and Enable'](),
}}
contentOptions={{
['data-testid' as string]: 'enable-cloud-modal',
}}
{...props}
/>
);
};

View File

@@ -1,35 +0,0 @@
import { styled } from '@affine/component';
export const Header = styled('div')({
display: 'flex',
justifyContent: 'space-between',
paddingRight: '20px',
paddingTop: '20px',
paddingLeft: '24px',
alignItems: 'center',
});
export const Content = styled('div')({
padding: '12px 24px 20px 24px',
});
export const Title = styled('div')({
fontSize: 'var(--affine-font-h6)',
lineHeight: '26px',
fontWeight: 600,
});
export const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
marginBottom: '20px',
};
});
export const ButtonContainer = styled('div')(() => {
return {
display: 'flex',
justifyContent: 'flex-end',
gap: '20px',
paddingTop: '20px',
};
});

View File

@@ -1,88 +1,47 @@
import {
Menu,
MenuItem,
type MenuProps,
MenuTrigger,
styled,
} from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useI18N } from '@affine/i18n';
import type { ButtonProps } from '@toeverything/components/button';
import type { ReactElement } from 'react';
import { useCallback } from 'react';
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
import { memo, type ReactElement } from 'react';
export const StyledListItem = styled(MenuItem)(() => ({
height: '38px',
textTransform: 'capitalize',
}));
interface LanguageMenuContentProps {
currentLanguage?: string;
}
const LanguageMenuContent = ({ currentLanguage }: LanguageMenuContentProps) => {
const i18n = useI18N();
const changeLanguage = useCallback(
(event: string) => {
return i18n.changeLanguage(event);
},
[i18n]
);
import { useLanguageHelper } from '../../../hooks/affine/use-language-helper';
// Fixme: keyboard focus should be supported by Menu component
const LanguageMenuContent = memo(function LanguageMenuContent() {
const { currentLanguage, languagesList, onSelect } = useLanguageHelper();
return (
<>
{LOCALES.map(option => {
{languagesList.map(option => {
return (
<StyledListItem
<MenuItem
key={option.name}
active={currentLanguage === option.originalName}
selected={currentLanguage?.originalName === option.originalName}
title={option.name}
onClick={() => {
changeLanguage(option.tag).catch(err => {
throw new Error('Failed to change language', err);
});
}}
onSelect={() => onSelect(option.tag)}
>
{option.originalName}
</StyledListItem>
</MenuItem>
);
})}
</>
);
};
interface LanguageMenuProps extends Omit<MenuProps, 'children'> {
triggerProps?: ButtonProps;
}
export const LanguageMenu = ({
triggerProps,
...menuProps
}: LanguageMenuProps) => {
const i18n = useI18N();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
});
export const LanguageMenu = () => {
const { currentLanguage } = useLanguageHelper();
return (
<Menu
content={
(
<LanguageMenuContent
currentLanguage={currentLanguage?.originalName}
/>
) as ReactElement
}
placement="bottom-end"
trigger="click"
disablePortal={true}
{...menuProps}
items={(<LanguageMenuContent />) as ReactElement}
contentOptions={{
style: {
background: 'var(--affine-white)',
},
align: 'end',
}}
>
<MenuTrigger
data-testid="language-menu-button"
style={{ textTransform: 'capitalize' }}
{...triggerProps}
style={{ textTransform: 'capitalize', fontWeight: 600 }}
block={true}
>
{currentLanguage?.originalName}
{currentLanguage?.originalName || ''}
</MenuTrigger>
</Menu>
);

View File

@@ -0,0 +1,5 @@
import { style } from '@vanilla-extract/css';
export const hoveredLanguageItem = style({
background: 'var(--affine-hover-color)',
});

View File

@@ -1,34 +1,26 @@
import { Input, Modal, ModalCloseButton } from '@affine/component';
import { Input } from '@affine/component';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useState } from 'react';
import {
StyledButtonContent,
StyledInputContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
StyledWorkspaceName,
} from './style';
ConfirmModal,
type ConfirmModalProps,
} from '@toeverything/components/modal';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useCallback, useState } from 'react';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
import * as styles from './style.css';
interface WorkspaceDeleteProps extends ConfirmModalProps {
workspace: AffineOfficialWorkspace;
onConfirm: () => void;
}
export const WorkspaceDeleteModal = ({
open,
onClose,
onConfirm,
workspace,
...props
}: WorkspaceDeleteProps) => {
const { onConfirm } = props;
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace
);
@@ -36,64 +28,57 @@ export const WorkspaceDeleteModal = ({
const allowDelete = deleteStr === workspaceName;
const t = useAFFiNEI18N();
const handleOnEnter = useCallback(() => {
if (allowDelete) {
return onConfirm?.();
}
}, [allowDelete, onConfirm]);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t['Delete Workspace']()}?</StyledModalHeader>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspaceName } as any}
</StyledWorkspaceName>
) cannot be undone, please proceed with caution. All contents will
be lost.
</Trans>
</StyledTextContent>
) : (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description2">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspaceName } as any}
</StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
</StyledTextContent>
)}
<StyledInputContent>
<Input
ref={ref => {
if (ref) {
window.setTimeout(() => ref.focus(), 0);
}
}}
onChange={setDeleteStr}
data-testid="delete-workspace-input"
placeholder={t['Placeholder of delete workspace']()}
width={315}
height={42}
/>
</StyledInputContent>
<StyledButtonContent>
<Button onClick={onClose} size="large">
{t['Cancel']()}
</Button>
<Button
data-testid="delete-workspace-confirm-button"
disabled={!allowDelete}
onClick={onConfirm}
size="large"
type="error"
style={{ marginLeft: '24px' }}
>
{t['Delete']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
<ConfirmModal
title={`${t['com.affine.workspaceDelete.title']()}?`}
cancelText={t['com.affine.workspaceDelete.button.cancel']()}
confirmButtonOptions={{
type: 'error',
disabled: !allowDelete,
['data-testid' as string]: 'delete-workspace-confirm-button',
children: t['com.affine.workspaceDelete.button.delete'](),
}}
{...props}
>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<Trans i18nKey="com.affine.workspaceDelete.description">
Deleting (
<span className={styles.workspaceName}>
{{ workspace: workspaceName } as any}
</span>
) cannot be undone, please proceed with caution. All contents will be
lost.
</Trans>
) : (
<Trans i18nKey="com.affine.workspaceDelete.description2">
Deleting (
<span className={styles.workspaceName}>
{{ workspace: workspaceName } as any}
</span>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
)}
<div className={styles.inputContent}>
<Input
ref={ref => {
if (ref) {
window.setTimeout(() => ref.focus(), 0);
}
}}
onChange={setDeleteStr}
data-testid="delete-workspace-input"
onEnter={handleOnEnter}
placeholder={t['com.affine.workspaceDelete.placeholder']()}
size="large"
/>
</div>
</ConfirmModal>
);
};

View File

@@ -0,0 +1,29 @@
import { style } from '@vanilla-extract/css';
export const modalWrapper = style({
position: 'relative',
padding: '0px',
width: '560px',
background: 'var(--affine-background-overlay-panel-color)',
borderRadius: '12px',
});
export const modalHeader = style({
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
});
export const inputContent = style({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '24px 0',
fontSize: 'var(--affine-font-base)',
});
export const workspaceName = style({
fontWeight: '600',
});

View File

@@ -1,75 +0,0 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '560px',
background: 'var(--affine-background-overlay-panel-color)',
borderRadius: '12px',
// height: '312px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
};
});
export const StyledInputContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '24px 0',
fontSize: 'var(--affine-font-base)',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
marginBottom: '42px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '600',
};
});
// export const StyledCancelButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });

View File

@@ -1,12 +1,12 @@
import { ConfirmModal } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { ConfirmModal } from '@toeverything/components/modal';
import { useCallback, useState } from 'react';
import type { WorkspaceSettingDetailProps } from '../index';
import type { WorkspaceSettingDetailProps } from '../types';
import { WorkspaceDeleteModal } from './delete';
export interface DeleteLeaveWorkspaceProps extends WorkspaceSettingDetailProps {
@@ -33,10 +33,6 @@ export const DeleteLeaveWorkspace = ({
}
}, [isOwner]);
const onCloseLeaveModal = useCallback(() => {
setShowLeave(false);
}, []);
const onLeaveConfirm = useCallback(() => {
return onLeaveWorkspace();
}, [onLeaveWorkspace]);
@@ -56,11 +52,11 @@ export const DeleteLeaveWorkspace = ({
name={
<span style={{ color: 'var(--affine-error-color)' }}>
{isOwner
? t['com.affine.settings.remove-workspace']()
: t['Leave Workspace']()}
? t['com.affine.workspaceDelete.title']()
: t['com.affine.deleteLeaveWorkspace.leave']()}
</span>
}
desc={t['com.affine.settings.remove-workspace-description']()}
desc={t['com.affine.deleteLeaveWorkspace.description']()}
style={{ cursor: 'pointer' }}
onClick={onLeaveOrDelete}
data-testid="delete-workspace-button"
@@ -71,21 +67,21 @@ export const DeleteLeaveWorkspace = ({
<WorkspaceDeleteModal
onConfirm={onDeleteConfirm}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
onOpenChange={setShowDelete}
workspace={workspace}
/>
) : (
<ConfirmModal
open={showLeave}
cancelText={t['com.affine.confirmModal.button.cancel']()}
onConfirm={onLeaveConfirm}
onCancel={onCloseLeaveModal}
onClose={onCloseLeaveModal}
title={`${t['Leave Workspace']()}?`}
content={t['Leave Workspace hint']()}
confirmType="warning"
confirmText={t['Leave']()}
onOpenChange={setShowLeave}
title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`}
description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}
confirmButtonOptions={{
type: 'warning',
children: t['Leave'](),
}}
/>
)}
</>

View File

@@ -1,11 +1,14 @@
import { toast } from '@affine/component';
import { pushNotificationAtom } from '@affine/component/notification-center';
import { SettingRow } from '@affine/component/setting-components';
import { isDesktop } from '@affine/env/constant';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { SaveDBFileResult } from '@toeverything/infra/type';
import { useCallback } from 'react';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react';
import type { Doc } from 'yjs';
import { encodeStateAsUpdate } from 'yjs';
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
if (window.apis && isDesktop) {
@@ -28,6 +31,22 @@ async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
}
}
async function syncDocsToSqliteDb(workspace: AffineOfficialWorkspace) {
if (window.apis && isDesktop) {
const workspaceId = workspace.blockSuiteWorkspace.doc.guid;
const syncDoc = async (doc: Doc) => {
await window.apis.db.applyDocUpdate(
workspace.id,
encodeStateAsUpdate(doc),
doc.guid === workspaceId ? undefined : doc.guid
);
await Promise.all([...doc.subdocs].map(subdoc => syncDoc(subdoc)));
};
return syncDoc(workspace.blockSuiteWorkspace.doc);
}
}
interface ExportPanelProps {
workspace: AffineOfficialWorkspace;
}
@@ -35,21 +54,45 @@ interface ExportPanelProps {
export const ExportPanel = ({ workspace }: ExportPanelProps) => {
const workspaceId = workspace.id;
const t = useAFFiNEI18N();
const [syncing, setSyncing] = useState(false);
const pushNotification = useSetAtom(pushNotificationAtom);
const onExport = useCallback(async () => {
await syncBlobsToSqliteDb(workspace);
const result: SaveDBFileResult =
await window.apis?.dialog.saveDBFileAs(workspaceId);
if (result?.error) {
toast(result.error);
} else if (!result?.canceled) {
toast(t['Export success']());
if (syncing) {
return;
}
}, [t, workspace, workspaceId]);
setSyncing(true);
try {
await syncBlobsToSqliteDb(workspace);
await syncDocsToSqliteDb(workspace);
const result: SaveDBFileResult =
await window.apis?.dialog.saveDBFileAs(workspaceId);
if (result?.error) {
throw new Error(result.error);
} else if (!result?.canceled) {
pushNotification({
type: 'success',
title: t['Export success'](),
});
}
} catch (e: any) {
pushNotification({
type: 'error',
title: t['Export failed'](),
message: e.message,
});
} finally {
setSyncing(false);
}
}, [pushNotification, syncing, t, workspace, workspaceId]);
return (
<>
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
<Button data-testid="export-affine-backup" onClick={onExport}>
<Button
data-testid="export-affine-backup"
onClick={onExport}
disabled={syncing}
>
{t['Export']()}
</Button>
</SettingRow>

View File

@@ -3,10 +3,6 @@ import {
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import type {
WorkspaceFlavour,
WorkspaceRegistry,
} from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useMemo } from 'react';
@@ -14,26 +10,12 @@ import { useMemo } from 'react';
import { useWorkspace } from '../../../hooks/use-workspace';
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { ExportPanel } from './export';
import { LabelsPanel } from './labels';
import { MembersPanel } from './members';
import { ProfilePanel } from './profile';
import { PublishPanel } from './publish';
import { StoragePanel } from './storage';
export interface WorkspaceSettingDetailProps {
workspaceId: string;
isOwner: boolean;
onDeleteLocalWorkspace: () => void;
onDeleteCloudWorkspace: () => void;
onLeaveWorkspace: () => void;
onTransferWorkspace: <
From extends WorkspaceFlavour,
To extends WorkspaceFlavour,
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
}
import type { WorkspaceSettingDetailProps } from './types';
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
const { workspaceId } = props;
@@ -69,9 +51,10 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
spreadCol={false}
>
<ProfilePanel workspace={workspace} {...props} />
<LabelsPanel workspace={workspace} {...props} />
</SettingRow>
</SettingWrapper>
<SettingWrapper title={t['AFFiNE Cloud']()}>
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
<PublishPanel workspace={workspace} {...props} />
<MembersPanel workspace={workspace} {...props} />
</SettingWrapper>

View File

@@ -0,0 +1,103 @@
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useMemo } from 'react';
import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types';
export interface LabelsPanelProps extends WorkspaceSettingDetailProps {
workspace: AffineOfficialWorkspace;
}
type WorkspaceStatus =
| 'local'
| 'syncCloud'
| 'syncDocker'
| 'selfHosted'
| 'joinedWorkspace'
| 'availableOffline'
| 'publishedToWeb';
type LabelProps = {
value: string;
background: string;
};
type LabelMap = {
[key in WorkspaceStatus]: LabelProps;
};
type labelConditionsProps = {
condition: boolean;
label: WorkspaceStatus;
};
const Label = ({ value, background }: LabelProps) => {
return (
<div>
<div className={style.workspaceLabel} style={{ background: background }}>
{value}
</div>
</div>
);
};
export const LabelsPanel = ({ workspace, isOwner }: LabelsPanelProps) => {
const labelMap: LabelMap = useMemo(
() => ({
local: {
value: 'Local',
background: 'var(--affine-tag-orange)',
},
syncCloud: {
value: 'Sync with AFFiNE Cloud',
background: 'var(--affine-tag-blue)',
},
syncDocker: {
value: 'Sync with AFFiNE Docker',
background: 'var(--affine-tag-green)',
},
selfHosted: {
value: 'Self-Hosted Server',
background: 'var(--affine-tag-purple)',
},
joinedWorkspace: {
value: 'Joined Workspace',
background: 'var(--affine-tag-yellow)',
},
availableOffline: {
value: 'Available Offline',
background: 'var(--affine-tag-green)',
},
publishedToWeb: {
value: 'Published to Web',
background: 'var(--affine-tag-blue)',
},
}),
[]
);
const labelConditions: labelConditionsProps[] = [
{ condition: !isOwner, label: 'joinedWorkspace' },
{ condition: workspace.flavour === 'local', label: 'local' },
{ condition: workspace.flavour === 'affine-cloud', label: 'syncCloud' },
{
condition: workspace.flavour === 'affine-public',
label: 'publishedToWeb',
},
//TODO: add these labels
// { status==="synced", label: 'availableOffline' }
// { workspace.flavour === 'affine-Docker', label: 'syncDocker' }
// { workspace.flavour === 'self-hosted', label: 'selfHosted' }
];
return (
<div className={style.labelWrapper}>
{labelConditions.map(
({ condition, label }) =>
condition && (
<Label
key={label}
value={labelMap[label].value}
background={labelMap[label].background}
/>
)
)}
</div>
);
};

View File

@@ -1,8 +1,11 @@
import { Menu, MenuItem } from '@affine/component';
import {
InviteModal,
type InviteModalProps,
} from '@affine/component/member-components';
import {
Pagination,
type PaginationProps,
} from '@affine/component/member-components';
import { pushNotificationAtom } from '@affine/component/notification-center';
import { SettingRow } from '@affine/component/setting-components';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
@@ -12,26 +15,37 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { MoreVerticalIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Button, IconButton } from '@toeverything/components/button';
import { Loading } from '@toeverything/components/loading';
import { Menu, MenuItem } from '@toeverything/components/menu';
import { Tooltip } from '@toeverything/components/tooltip';
import clsx from 'clsx';
import { useSetAtom } from 'jotai/react';
import { useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import { Suspense, useCallback, useMemo, useState } from 'react';
import {
Suspense,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
import { useInviteMember } from '../../../hooks/affine/use-invite-member';
import { useMemberCount } from '../../../hooks/affine/use-member-count';
import { type Member, useMembers } from '../../../hooks/affine/use-members';
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
import { AnyErrorBoundary } from '../any-error-boundary';
import { type WorkspaceSettingDetailProps } from './index';
import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types';
const COUNT_PER_PAGE = 8;
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
workspace: AffineOfficialWorkspace;
}
type OnRevoke = (memberId: string) => void;
const MembersPanelLocal = () => {
const t = useAFFiNEI18N();
return (
@@ -48,41 +62,27 @@ const MembersPanelLocal = () => {
export const CloudWorkspaceMembersPanel = ({
workspace,
isOwner,
}: MembersPanelProps): ReactElement => {
}: MembersPanelProps) => {
const workspaceId = workspace.id;
const members = useMembers(workspaceId);
const memberCount = useMemberCount(workspaceId);
const t = useAFFiNEI18N();
const currentUser = useCurrentUser();
const { invite, isMutating } = useInviteMember(workspaceId);
const [open, setOpen] = useState(false);
const pushNotification = useSetAtom(pushNotificationAtom);
const revokeMemberPermission = useRevokeMemberPermission(workspaceId);
const memberCount = members.length;
const memberList = useMemo(
() =>
members.sort((a, b) => {
if (
a.permission === Permission.Owner &&
b.permission !== Permission.Owner
) {
return -1;
}
if (
a.permission !== Permission.Owner &&
b.permission === Permission.Owner
) {
return 1;
}
return 0;
}),
[members]
);
const [open, setOpen] = useState(false);
const [memberSkip, setMemberSkip] = useState(0);
const pushNotification = useSetAtom(pushNotificationAtom);
const openModal = useCallback(() => {
setOpen(true);
}, []);
const onPageChange = useCallback<PaginationProps['onPageChange']>(offset => {
setMemberSkip(offset);
}, []);
const onInviteConfirm = useCallback<InviteModalProps['onConfirm']>(
async ({ email, permission }) => {
const success = await invite(
@@ -103,11 +103,39 @@ export const CloudWorkspaceMembersPanel = ({
[invite, pushNotification, t]
);
const listContainerRef = useRef<HTMLDivElement | null>(null);
const [memberListHeight, setMemberListHeight] = useState<number | null>(null);
useLayoutEffect(() => {
if (
memberCount > COUNT_PER_PAGE &&
listContainerRef.current &&
memberListHeight === null
) {
const rect = listContainerRef.current.getBoundingClientRect();
setMemberListHeight(rect.height);
}
}, [listContainerRef, memberCount, memberListHeight]);
const onRevoke = useCallback<OnRevoke>(
async memberId => {
const res = await revokeMemberPermission(memberId);
if (res?.revoke) {
pushNotification({
title: t['Removed successfully'](),
type: 'success',
});
}
},
[pushNotification, revokeMemberPermission, t]
);
return (
<>
<SettingRow
name={`${t['Members']()} (${memberCount})`}
desc={t['Members hint']()}
spreadCol={isOwner}
>
{isOwner ? (
<>
@@ -121,21 +149,84 @@ export const CloudWorkspaceMembersPanel = ({
</>
) : null}
</SettingRow>
<div className={style.membersList}>
{memberList.map(member => (
<MemberItem
key={member.id}
member={member}
<div
className={style.membersPanel}
ref={listContainerRef}
style={memberListHeight ? { height: memberListHeight } : {}}
>
<Suspense fallback={<MemberListFallback memberCount={memberCount} />}>
<MemberList
workspaceId={workspaceId}
isOwner={isOwner}
currentUser={currentUser}
onRevoke={revokeMemberPermission}
skip={memberSkip}
onRevoke={onRevoke}
/>
))}
</Suspense>
{memberCount > COUNT_PER_PAGE && (
<Pagination
totalCount={memberCount}
countPerPage={COUNT_PER_PAGE}
onPageChange={onPageChange}
/>
)}
</div>
</>
);
};
const MemberListFallback = ({ memberCount }: { memberCount: number }) => {
// prevent page jitter
const height = useMemo(() => {
if (memberCount > COUNT_PER_PAGE) {
// height and margin-bottom
return COUNT_PER_PAGE * 58 + (COUNT_PER_PAGE - 1) * 6;
}
return 'auto';
}, [memberCount]);
return (
<div
style={{
height,
}}
className={style.membersFallback}
>
<Loading size={40} />
</div>
);
};
const MemberList = ({
workspaceId,
isOwner,
skip,
onRevoke,
}: {
workspaceId: string;
isOwner: boolean;
skip: number;
onRevoke: OnRevoke;
}) => {
const members = useMembers(workspaceId, skip, COUNT_PER_PAGE);
const currentUser = useCurrentUser();
return (
<div className={style.memberList}>
{members.map(member => (
<MemberItem
key={member.id}
member={member}
isOwner={isOwner}
currentUser={currentUser}
onRevoke={onRevoke}
/>
))}
</div>
);
};
const MemberItem = ({
member,
isOwner,
@@ -145,7 +236,7 @@ const MemberItem = ({
member: Member;
isOwner: boolean;
currentUser: CheckedUser;
onRevoke: (memberId: string) => void;
onRevoke: OnRevoke;
}) => {
const t = useAFFiNEI18N();
@@ -161,56 +252,56 @@ const MemberItem = ({
}, [currentUser.id, isOwner, member.id, t]);
return (
<>
<div key={member.id} className={style.listItem}>
<Avatar
size={36}
url={member.avatarUrl}
name={(member.emailVerified ? member.name : member.email) as string}
/>
<div className={style.memberContainer}>
{member.emailVerified ? (
<>
<div className={style.memberName}>{member.name}</div>
<div className={style.memberEmail}>{member.email}</div>
</>
) : (
<div className={style.memberName}>{member.email}</div>
)}
</div>
<div
className={clsx(style.roleOrStatus, {
pending: !member.accepted,
})}
>
{member.accepted
? member.permission === Permission.Owner
? 'Workspace Owner'
: 'Member'
: 'Pending'}
</div>
<Menu
content={
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
{operationButtonInfo.leaveOrRevokeText}
</MenuItem>
}
placement="bottom"
disablePortal={true}
trigger="click"
>
<IconButton
disabled={!operationButtonInfo.show}
style={{
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
flexShrink: 0,
}}
>
<MoreVerticalIcon />
</IconButton>
</Menu>
<div
key={member.id}
className={style.memberListItem}
data-testid="member-item"
>
<Avatar
size={36}
url={member.avatarUrl}
name={(member.emailVerified ? member.name : member.email) as string}
/>
<div className={style.memberContainer}>
{member.emailVerified ? (
<>
<div className={style.memberName}>{member.name}</div>
<div className={style.memberEmail}>{member.email}</div>
</>
) : (
<div className={style.memberName}>{member.email}</div>
)}
</div>
</>
<div
className={clsx(style.roleOrStatus, {
pending: !member.accepted,
})}
>
{member.accepted
? member.permission === Permission.Owner
? 'Workspace Owner'
: 'Member'
: 'Pending'}
</div>
<Menu
items={
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
{operationButtonInfo.leaveOrRevokeText}
</MenuItem>
}
>
<IconButton
disabled={!operationButtonInfo.show}
type="plain"
style={{
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
flexShrink: 0,
}}
>
<MoreVerticalIcon />
</IconButton>
</Menu>
</div>
);
};

View File

@@ -1,17 +1,24 @@
import { FlexWrapper, Input, toast, Wrapper } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { FlexWrapper, Input, Wrapper } from '@affine/component';
import { pushNotificationAtom } from '@affine/component/notification-center';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CameraIcon, DoneIcon } from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { CameraIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Button } from '@toeverything/components/button';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import { useCallback, useState } from 'react';
import { useSetAtom } from 'jotai';
import {
type KeyboardEvent,
type MouseEvent,
startTransition,
useCallback,
useState,
} from 'react';
import { Upload } from '../../pure/file-upload';
import { type WorkspaceSettingDetailProps } from './index';
import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types';
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
workspace: AffineOfficialWorkspace;
@@ -19,8 +26,9 @@ export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
const t = useAFFiNEI18N();
const pushNotification = useSetAtom(pushNotificationAtom);
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
const [workspaceAvatar, update] = useBlockSuiteWorkspaceAvatarUrl(
workspace.blockSuiteWorkspace
);
@@ -33,30 +41,66 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
const handleUpdateWorkspaceName = useCallback(
(name: string) => {
setName(name);
toast(t['Update workspace name success']());
pushNotification({
title: t['Update workspace name success'](),
type: 'success',
});
},
[setName, t]
[pushNotification, setName, t]
);
const handleSetInput = useCallback((value: string) => {
startTransition(() => {
setInput(value);
});
}, []);
const handleKeyUp = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.code === 'Enter' && name !== input) {
handleUpdateWorkspaceName(input);
}
},
[handleUpdateWorkspaceName, input, name]
);
const handleClick = useCallback(() => {
handleUpdateWorkspaceName(input);
}, [handleUpdateWorkspaceName, input]);
const handleRemoveUserAvatar = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await update(null);
},
[update]
);
return (
<div className={style.profileWrapper}>
<div className={clsx(style.avatarWrapper, { disable: !isOwner })}>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
>
<>
<div className="camera-icon-wrapper">
<CameraIcon />
</div>
<WorkspaceAvatar
size={56}
workspace={workspace.blockSuiteWorkspace}
/>
</>
</Upload>
</div>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
disabled={!isOwner}
>
<Avatar
size={56}
url={workspaceAvatar}
name={name}
colorfulFallback
hoverIcon={isOwner ? <CameraIcon /> : undefined}
onRemove={
workspaceAvatar && isOwner ? handleRemoveUserAvatar : undefined
}
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
removeTooltipOptions={{ content: t['Remove photo']() }}
data-testid="workspace-setting-avatar"
removeButtonProps={{
['data-testid' as string]: 'workspace-setting-remove-avatar-button',
}}
/>
</Upload>
<Wrapper marginLeft={20}>
<div className={style.label}>{t['Workspace Name']()}</div>
<FlexWrapper alignItems="center" flexGrow="1">
@@ -69,21 +113,19 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
placeholder={t['Workspace Name']()}
maxLength={64}
minLength={0}
onChange={setInput}
onChange={handleSetInput}
onKeyUp={handleKeyUp}
/>
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
<IconButton
<Button
data-testid="save-workspace-name"
onClick={() => {
handleUpdateWorkspaceName(input);
}}
active={true}
onClick={handleClick}
style={{
marginLeft: '12px',
}}
>
<DoneIcon />
</IconButton>
{t['com.affine.editCollection.save']()}
</Button>
)}
</FlexWrapper>
</Wrapper>

View File

@@ -12,13 +12,14 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { noop } from 'foxact/noop';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from '../../../utils';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from './index';
import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types';
export interface PublishPanelProps
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
@@ -100,7 +101,7 @@ const FakePublishPanelAffine = (_props: FakePublishPanelAffineProps) => {
<Tooltip content={t['com.affine.settings.workspace.publish-tooltip']()}>
<div className={style.fakeWrapper}>
<SettingRow name={t['Publish']()} desc={t['Unpublished hint']()}>
<Switch checked={false} />
<Switch checked={false} onChange={noop} />
</SettingRow>
</div>
</Tooltip>
@@ -142,9 +143,7 @@ const PublishPanelLocal = ({
{runtimeConfig.enableCloud ? (
<EnableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
onOpenChange={setOpen}
onConfirm={() => {
onTransferWorkspace(
WorkspaceFlavour.LOCAL,
@@ -155,12 +154,7 @@ const PublishPanelLocal = ({
}}
/>
) : (
<TmpDisableAffineCloudModal
open={open}
onClose={() => {
setOpen(false);
}}
/>
<TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
)}
</>
);

View File

@@ -12,12 +12,20 @@ export const profileHandlerWrapper = style({
marginLeft: '20px',
});
export const labelWrapper = style({
width: '100%',
display: 'flex',
alignItems: 'center',
marginTop: '24px',
gap: '10px',
flexWrap: 'wrap',
});
export const avatarWrapper = style({
width: '56px',
height: '56px',
borderRadius: '50%',
position: 'relative',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
selectors: {
@@ -34,8 +42,9 @@ globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
display: 'flex',
});
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
width: '100%',
height: '100%',
width: '56px',
height: '56px',
borderRadius: '50%',
position: 'absolute',
display: 'none',
justifyContent: 'center',
@@ -77,24 +86,37 @@ export const fakeWrapper = style({
},
});
export const membersList = style({
marginTop: '24px',
export const membersFallback = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: 'var(--affine-primary-color)',
});
export const membersPanel = style({
padding: '4px',
borderRadius: '12px',
background: 'var(--affine-background-primary-color)',
maxHeight: '464px',
overflow: 'hidden',
border: '1px solid var(--affine-border-color)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
});
export const listItem = style({
export const memberList = style({});
export const memberListItem = style({
padding: '0 4px 0 16px',
height: '58px',
display: 'flex',
width: '100%',
alignItems: 'center',
':hover': {
background: 'var(--affine-hover-color)',
borderRadius: '8px',
selectors: {
'&:hover': {
background: 'var(--affine-hover-color)',
borderRadius: '8px',
},
'&:not(:last-of-type)': {
marginBottom: '6px',
},
},
});
export const memberContainer = style({
@@ -136,7 +158,7 @@ export const memberEmail = style({
});
export const iconButton = style({});
globalStyle(`${listItem}:hover ${iconButton}`, {
globalStyle(`${memberListItem}:hover ${iconButton}`, {
opacity: 1,
pointerEvents: 'all',
});
@@ -146,3 +168,17 @@ export const label = style({
color: 'var(--affine-text-secondary-color)',
marginBottom: '5px',
});
export const workspaceLabel = style({
width: '100%',
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '6px',
padding: '2px 10px',
border: '1px solid var(--affine-white-30)',
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-primary-color)',
lineHeight: '20px',
whiteSpace: 'nowrap',
});

View File

@@ -0,0 +1,20 @@
import type {
WorkspaceFlavour,
WorkspaceRegistry,
} from '@affine/env/workspace';
export interface WorkspaceSettingDetailProps {
workspaceId: string;
isOwner: boolean;
onDeleteLocalWorkspace: () => void;
onDeleteCloudWorkspace: () => void;
onLeaveWorkspace: () => void;
onTransferWorkspace: <
From extends WorkspaceFlavour,
To extends WorkspaceFlavour,
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
}

View File

@@ -8,12 +8,16 @@ import { guideOnboardingAtom } from '../../atoms/guide';
export const OnboardingModal = memo(function OnboardingModal() {
const [open, setOpen] = useAtom(openOnboardingModalAtom);
const [guideOpen, setShowOnboarding] = useAtom(guideOnboardingAtom);
const onCloseTourModal = useCallback(() => {
setShowOnboarding(false);
setOpen(false);
}, [setOpen, setShowOnboarding]);
const onOpenChange = useCallback(
(open: boolean) => {
if (open) return;
setShowOnboarding(false);
setOpen(false);
},
[setOpen, setShowOnboarding]
);
return (
<TourModal open={!open ? guideOpen : open} onClose={onCloseTourModal} />
<TourModal open={!open ? guideOpen : open} onOpenChange={onOpenChange} />
);
});

View File

@@ -2,43 +2,46 @@ import { FlexWrapper, Input } from '@affine/component';
import {
SettingHeader,
SettingRow,
StorageProgress,
} from '@affine/component/setting-components';
import { UserAvatar } from '@affine/component/user-avatar';
import { uploadAvatarMutation } from '@affine/graphql';
import {
allBlobSizesQuery,
removeAvatarMutation,
uploadAvatarMutation,
} from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon, CameraIcon, DoneIcon } from '@blocksuite/icons';
import { Button, IconButton } from '@toeverything/components/button';
import { useAtom } from 'jotai/index';
import { signOut } from 'next-auth/react';
import { type FC, useCallback, useState } from 'react';
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Button } from '@toeverything/components/button';
import { useSetAtom } from 'jotai';
import {
type FC,
type MouseEvent,
Suspense,
useCallback,
useState,
} from 'react';
import { authAtom } from '../../../../atoms';
import { authAtom, openSignOutModalAtom } from '../../../../atoms';
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
import { Upload } from '../../../pure/file-upload';
import * as style from './style.css';
export const AvatarAndName = () => {
export const UserAvatar = () => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
const [input, setInput] = useState<string>(user.name);
const { trigger: avatarTrigger } = useMutation({
mutation: uploadAvatarMutation,
});
const handleUpdateUserName = useCallback(
(newName: string) => {
user.update({ name: newName }).catch(console.error);
},
[user]
);
const { trigger: removeAvatarTrigger } = useMutation({
mutation: removeAvatarMutation,
});
const handleUpdateUserAvatar = useCallback(
async (file: File) => {
await avatarTrigger({
id: user.id,
avatar: file,
});
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
@@ -46,6 +49,51 @@ export const AvatarAndName = () => {
},
[avatarTrigger, user]
);
const handleRemoveUserAvatar = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await removeAvatarTrigger();
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
user.update({ name: user.name }).catch(console.error);
},
[removeAvatarTrigger, user]
);
return (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={handleUpdateUserAvatar}
data-testid="upload-user-avatar"
>
<Avatar
size={56}
name={user.name}
url={user.image}
hoverIcon={<CameraIcon />}
onRemove={user.image ? handleRemoveUserAvatar : undefined}
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
removeTooltipOptions={{ content: t['Remove photo']() }}
data-testid="user-setting-avatar"
removeButtonProps={{
['data-testid' as string]: 'user-setting-remove-avatar-button',
}}
/>
</Upload>
);
};
export const AvatarAndName = () => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
const [input, setInput] = useState<string>(user.name);
const allowUpdate = !!input && input !== user.name;
const handleUpdateUserName = useCallback(() => {
if (!allowUpdate) {
return;
}
user.update({ name: input }).catch(console.error);
}, [allowUpdate, input, user]);
return (
<>
<SettingRow
@@ -54,25 +102,9 @@ export const AvatarAndName = () => {
spreadCol={false}
>
<FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center">
<div className={style.avatarWrapper}>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={handleUpdateUserAvatar}
data-testid="upload-user-avatar"
>
<>
<div className="camera-icon-wrapper">
<CameraIcon />
</div>
<UserAvatar
size={56}
name={user.name}
url={user.image}
className="avatar"
/>
</>
</Upload>
</div>
<Suspense>
<UserAvatar />
</Suspense>
<div className={style.profileInputWrapper}>
<label>{t['com.affine.settings.profile.name']()}</label>
@@ -86,21 +118,19 @@ export const AvatarAndName = () => {
width={280}
height={28}
onChange={setInput}
onEnter={handleUpdateUserName}
/>
{input && input === user.name ? null : (
<IconButton
{allowUpdate ? (
<Button
data-testid="save-user-name"
onClick={() => {
handleUpdateUserName(input);
}}
onClick={handleUpdateUserName}
style={{
color: 'var(--affine-primary-color)',
marginLeft: '12px',
}}
>
<DoneIcon />
</IconButton>
)}
{t['com.affine.editCollection.save']()}
</Button>
) : null}
</FlexWrapper>
</div>
</FlexWrapper>
@@ -109,10 +139,35 @@ export const AvatarAndName = () => {
);
};
const StoragePanel = () => {
const t = useAFFiNEI18N();
const { data } = useQuery({
query: allBlobSizesQuery,
});
const onUpgrade = useCallback(() => {}, []);
return (
<SettingRow
name={t['com.affine.storage.title']()}
desc=""
spreadCol={false}
>
<StorageProgress
max={10737418240}
value={data.collectAllBlobSizes.size}
onUpgrade={onUpgrade}
/>
</SettingRow>
);
};
export const AccountSetting: FC = () => {
const t = useAFFiNEI18N();
const user = useCurrentUser();
const [, setAuthModal] = useAtom(authAtom);
const setAuthModal = useSetAtom(authAtom);
const setSignOutModal = useSetAtom(openSignOutModalAtom);
const onChangeEmail = useCallback(() => {
setAuthModal({
@@ -122,14 +177,19 @@ export const AccountSetting: FC = () => {
emailType: 'changeEmail',
});
}, [setAuthModal, user.email]);
const onChangePassword = useCallback(() => {
const onPasswordButtonClick = useCallback(() => {
setAuthModal({
openModal: true,
state: 'sendEmail',
email: user.email,
emailType: 'changePassword',
emailType: user.hasPassword ? 'changePassword' : 'setPassword',
});
}, [setAuthModal, user.email]);
}, [setAuthModal, user.email, user.hasPassword]);
const onOpenSignOutModal = useCallback(() => {
setSignOutModal(true);
}, [setSignOutModal]);
return (
<>
@@ -148,20 +208,21 @@ export const AccountSetting: FC = () => {
name={t['com.affine.settings.password']()}
desc={t['com.affine.settings.password.message']()}
>
<Button onClick={onChangePassword}>
<Button onClick={onPasswordButtonClick}>
{user.hasPassword
? t['com.affine.settings.password.action.change']()
: t['com.affine.settings.password.action.set']()}
</Button>
</SettingRow>
<Suspense>
<StoragePanel />
</Suspense>
<SettingRow
name={t[`Sign out`]()}
desc={t['com.affine.setting.sign.out.message']()}
style={{ cursor: 'pointer' }}
onClick={useCallback(() => {
signOut().catch(console.error);
}, [])}
data-testid="sign-out-button"
onClick={onOpenSignOutModal}
>
<ArrowRightSmallIcon />
</SettingRow>

View File

@@ -14,7 +14,6 @@ export const avatarWrapper = style({
height: '56px',
borderRadius: '50%',
position: 'relative',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
selectors: {
@@ -28,8 +27,9 @@ globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
display: 'flex',
});
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
width: '100%',
height: '100%',
width: '56px',
height: '56px',
borderRadius: '50%',
position: 'absolute',
display: 'none',
justifyContent: 'center',

View File

@@ -22,25 +22,28 @@ export const AboutAffine = () => {
return (
<>
<SettingHeader
title={t['About AFFiNE']()}
subtitle={t['com.affine.settings.about.message']()}
title={t['com.affine.aboutAFFiNE.title']()}
subtitle={t['com.affine.aboutAFFiNE.subtitle']()}
data-testid="about-title"
/>
<SettingWrapper title={t['Version']()}>
<SettingRow name={t['App Version']()} desc={runtimeConfig.appVersion} />
<SettingWrapper title={t['com.affine.aboutAFFiNE.version.title']()}>
<SettingRow
name={t['Editor Version']()}
name={t['com.affine.aboutAFFiNE.version.app']()}
desc={runtimeConfig.appVersion}
/>
<SettingRow
name={t['com.affine.aboutAFFiNE.version.editor.title']()}
desc={runtimeConfig.editorVersion}
/>
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
<>
<SettingRow
name={t['Check for updates']()}
desc={t['New version is ready']()}
></SettingRow>
name={t['com.affine.aboutAFFiNE.checkUpdate.title']()}
desc={t['com.affine.aboutAFFiNE.checkUpdate.description']()}
/>
<SettingRow
name={t['Check for updates automatically']()}
desc={t['com.affine.settings.about.update.check.message']()}
name={t['com.affine.aboutAFFiNE.autoCheckUpdate.title']()}
desc={t['com.affine.aboutAFFiNE.autoCheckUpdate.description']()}
>
<Switch
checked={appSettings.autoCheckUpdate}
@@ -48,8 +51,10 @@ export const AboutAffine = () => {
/>
</SettingRow>
<SettingRow
name={t['Download updates automatically']()}
desc={t['com.affine.settings.about.update.download.message']()}
name={t['com.affine.aboutAFFiNE.autoDownloadUpdate.title']()}
desc={t[
'com.affine.aboutAFFiNE.autoDownloadUpdate.description'
]()}
>
<Switch
checked={appSettings.autoCheckUpdate}
@@ -57,8 +62,8 @@ export const AboutAffine = () => {
/>
</SettingRow>
<SettingRow
name={t[`Discover what's new`]()}
desc={t['Changelog description']()}
name={t['com.affine.aboutAFFiNE.changelog.title']()}
desc={t['com.affine.aboutAFFiNE.changelog.description']()}
style={{ cursor: 'pointer' }}
onClick={() => {
window.open(runtimeConfig.changelogUrl, '_blank');
@@ -69,14 +74,14 @@ export const AboutAffine = () => {
</>
) : null}
</SettingWrapper>
<SettingWrapper title={t['Contact with us']()}>
<SettingWrapper title={t['com.affine.aboutAFFiNE.contact.title']()}>
<a
className={link}
rel="noreferrer"
href="https://affine.pro"
target="_blank"
>
{t['Official Website']()}
{t['com.affine.aboutAFFiNE.contact.website']()}
<OpenInNewIcon className="icon" />
</a>
<a
@@ -85,11 +90,11 @@ export const AboutAffine = () => {
href="https://community.affine.pro"
target="_blank"
>
{t['AFFiNE Community']()}
{t['com.affine.aboutAFFiNE.contact.community']()}
<OpenInNewIcon className="icon" />
</a>
</SettingWrapper>
<SettingWrapper title={t['Communities']()}>
<SettingWrapper title={t['com.affine.aboutAFFiNE.community.title']()}>
<div className={communityWrapper}>
{relatedLinks.map(({ icon, title, link }) => {
return (
@@ -107,14 +112,14 @@ export const AboutAffine = () => {
})}
</div>
</SettingWrapper>
<SettingWrapper title={t['Info of legal']()}>
<SettingWrapper title={t['com.affine.aboutAFFiNE.legal.title']()}>
<a
className={link}
rel="noreferrer"
href="https://affine.pro/privacy"
target="_blank"
>
{t['Privacy']()}
{t['com.affine.aboutAFFiNE.legal.privacy']()}
<OpenInNewIcon className="icon" />
</a>
<a
@@ -123,7 +128,7 @@ export const AboutAffine = () => {
href="https://affine.pro/terms"
target="_blank"
>
{t['Terms of Use']()}
{t['com.affine.aboutAFFiNE.legal.tos']()}
<OpenInNewIcon className="icon" />
</a>
</SettingWrapper>

View File

@@ -1,4 +1,4 @@
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
import dayjs from 'dayjs';
import { useCallback } from 'react';
@@ -23,10 +23,8 @@ const DateFormatMenuContent = ({
return (
<MenuItem
key={option}
active={currentOption === option}
onClick={() => {
onSelect(option);
}}
selected={currentOption === option}
onSelect={() => onSelect(option)}
>
{dayjs(new Date()).format(option)}
</MenuItem>
@@ -47,17 +45,14 @@ export const DateFormatSetting = () => {
return (
<Menu
content={
items={
<DateFormatMenuContent
onSelect={handleSelect}
currentOption={appearanceSettings.dateFormat}
/>
}
placement="bottom-end"
trigger="click"
disablePortal={true}
>
<MenuTrigger data-testid="date-format-menu-trigger">
<MenuTrigger data-testid="date-format-menu-trigger" block>
{dayjs(new Date()).format(appearanceSettings.dateFormat)}
</MenuTrigger>
</Menu>

View File

@@ -24,7 +24,7 @@ export const ThemeSettings = () => {
<RadioButtonGroup
width={250}
className={settingWrapper}
defaultValue={theme}
value={theme}
onValueChange={useCallback(
(value: string) => {
setTheme(value);
@@ -33,25 +33,26 @@ export const ThemeSettings = () => {
)}
>
<RadioButton value="system" data-testid="system-theme-trigger">
{t['system']()}
{t['com.affine.themeSettings.system']()}
</RadioButton>
<RadioButton value="light" data-testid="light-theme-trigger">
{t['light']()}
{t['com.affine.themeSettings.light']()}
</RadioButton>
<RadioButton value="dark" data-testid="dark-theme-trigger">
{t['dark']()}
{t['com.affine.themeSettings.dark']()}
</RadioButton>
</RadioButtonGroup>
);
};
const FontFamilySettings = () => {
const t = useAFFiNEI18N();
const [appSettings, setAppSettings] = useAppSetting();
return (
<RadioButtonGroup
width={250}
className={settingWrapper}
defaultValue={appSettings.fontStyle}
value={appSettings.fontStyle}
onValueChange={useCallback(
(key: AppSetting['fontStyle']) => {
setAppSettings({ fontStyle: key });
@@ -60,6 +61,20 @@ const FontFamilySettings = () => {
)}
>
{fontStyleOptions.map(({ key, value }) => {
let font = '';
switch (key) {
case 'Sans':
font = t['com.affine.appearanceSettings.fontStyle.sans']();
break;
case 'Serif':
font = t['com.affine.appearanceSettings.fontStyle.serif']();
break;
case 'Mono':
font = t[`com.affine.appearanceSettings.fontStyle.mono`]();
break;
default:
break;
}
return (
<RadioButton
key={key}
@@ -69,7 +84,7 @@ const FontFamilySettings = () => {
fontFamily: value,
}}
>
{key}
{font}
</RadioButton>
);
})}
@@ -91,47 +106,35 @@ export const AppearanceSettings = () => {
return (
<>
<SettingHeader
title={t['Appearance Settings']()}
subtitle={t['Customize your AFFiNE Appearance']()}
title={t['com.affine.appearanceSettings.title']()}
subtitle={t['com.affine.appearanceSettings.subtitle']()}
/>
<SettingWrapper title={t['Theme']()}>
<SettingWrapper title={t['com.affine.appearanceSettings.theme.title']()}>
<SettingRow
name={t['Color Scheme']()}
desc={t['Choose your color scheme']()}
name={t['com.affine.appearanceSettings.color.title']()}
desc={t['com.affine.appearanceSettings.color.description']()}
>
<ThemeSettings />
</SettingRow>
<SettingRow
name={t['Font Style']()}
desc={t['Choose your font style']()}
name={t['com.affine.appearanceSettings.font.title']()}
desc={t['com.affine.appearanceSettings.font.description']()}
>
<FontFamilySettings />
</SettingRow>
<SettingRow
name={t['Display Language']()}
desc={t['com.affine.settings.appearance.language-description']()}
name={t['com.affine.appearanceSettings.language.title']()}
desc={t['com.affine.appearanceSettings.language.description']()}
>
<div className={settingWrapper}>
<LanguageMenu
triggerContainerStyle={{ width: '100%' }}
triggerProps={{
style: {
width: '100%',
justifyContent: 'space-between',
fontWeight: 600,
padding: '0 10px',
},
}}
/>
<LanguageMenu />
</div>
</SettingRow>
{environment.isDesktop ? (
<SettingRow
name={t['Client Border Style']()}
desc={t[
'com.affine.settings.appearance.border-style-description'
]()}
name={t['com.affine.appearanceSettings.clientBorder.title']()}
desc={t['com.affine.appearanceSettings.clientBorder.description']()}
data-testid="client-border-style-trigger"
>
<Switch
@@ -142,8 +145,8 @@ export const AppearanceSettings = () => {
) : null}
<SettingRow
name={t['Full width Layout']()}
desc={t['com.affine.settings.appearance.full-width-description']()}
name={t['com.affine.appearanceSettings.fullWidth.title']()}
desc={t['com.affine.appearanceSettings.fullWidth.description']()}
>
<Switch
data-testid="full-width-layout-trigger"
@@ -153,10 +156,8 @@ export const AppearanceSettings = () => {
</SettingRow>
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
<SettingRow
name={t['Window frame style']()}
desc={t[
'com.affine.settings.appearance.window-frame-description'
]()}
name={t['com.affine.appearanceSettings.windowFrame.title']()}
desc={t['com.affine.appearanceSettings.windowFrame.description']()}
>
<RadioButtonGroup
className={settingWrapper}
@@ -169,7 +170,7 @@ export const AppearanceSettings = () => {
{windowFrameStyleOptions.map(option => {
return (
<RadioButton value={option} key={option}>
{t[option]()}
{t[`com.affine.appearanceSettings.windowFrame.${option}`]()}
</RadioButton>
);
})}
@@ -178,18 +179,18 @@ export const AppearanceSettings = () => {
) : null}
</SettingWrapper>
{runtimeConfig.enableNewSettingUnstableApi ? (
<SettingWrapper title={t['Date']()}>
<SettingWrapper title={t['com.affine.appearanceSettings.date.title']()}>
<SettingRow
name={t['Date Format']()}
desc={t['com.affine.settings.appearance.date-format-description']()}
name={t['com.affine.appearanceSettings.dateFormat.title']()}
desc={t['com.affine.appearanceSettings.dateFormat.description']()}
>
<div className={settingWrapper}>
<DateFormatSetting />
</div>
</SettingRow>
<SettingRow
name={t['Start Week On Monday']()}
desc={t['com.affine.settings.appearance.start-week-description']()}
name={t['com.affine.appearanceSettings.startWeek.title']()}
desc={t['com.affine.appearanceSettings.startWeek.description']()}
>
<Switch
checked={appSettings.startWeekOnMonday}
@@ -200,10 +201,14 @@ export const AppearanceSettings = () => {
) : null}
{environment.isDesktop ? (
<SettingWrapper title={t['Sidebar']()}>
<SettingWrapper
title={t['com.affine.appearanceSettings.sidebar.title']()}
>
<SettingRow
name={t['com.affine.settings.noise-style']()}
desc={t['com.affine.settings.noise-style-description']()}
name={t['com.affine.appearanceSettings.noisyBackground.title']()}
desc={t[
'com.affine.appearanceSettings.noisyBackground.description'
]()}
>
<Switch
checked={appSettings.enableNoisyBackground}
@@ -212,17 +217,21 @@ export const AppearanceSettings = () => {
}
/>
</SettingRow>
<SettingRow
name={t['com.affine.settings.translucent-style']()}
desc={t['com.affine.settings.translucent-style-description']()}
>
<Switch
checked={appSettings.enableBlurBackground}
onChange={checked =>
changeSwitch('enableBlurBackground', checked)
}
/>
</SettingRow>
{environment.isMacOs && (
<SettingRow
name={t['com.affine.appearanceSettings.translucentUI.title']()}
desc={t[
'com.affine.appearanceSettings.translucentUI.description'
]()}
>
<Switch
checked={appSettings.enableBlurBackground}
onChange={checked =>
changeSwitch('enableBlurBackground', checked)
}
/>
</SettingRow>
)}
</SettingWrapper>
) : null}
</>

View File

@@ -39,7 +39,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
},
{
key: 'shortcuts',
title: t['Keyboard Shortcuts'](),
title: t['com.affine.keyboardShortcuts.title'](),
icon: KeyboardIcon,
testId: 'shortcuts-panel-trigger',
},
@@ -51,7 +51,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
},
{
key: 'about',
title: t['About AFFiNE'](),
title: t['com.affine.aboutAFFiNE.title'](),
icon: InformationIcon,
testId: 'about-panel-trigger',
},

View File

@@ -49,8 +49,8 @@ export const Shortcuts = () => {
return (
<>
<SettingHeader
title={t['Keyboard Shortcuts']()}
subtitle={t['Check Keyboard Shortcuts quickly']()}
title={t['com.affine.keyboardShortcuts.title']()}
subtitle={t['com.affine.keyboardShortcuts.subtitle']()}
data-testid="keyboard-shortcuts-title"
/>
<ShortcutsPanel shortcutsInfo={generalShortcutsInfo} />

Some files were not shown because too many files have changed in this diff Show More