Compare commits
98 Commits
v0.8.0-can
...
v0.8.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8833584756 | ||
|
|
582059f6d6 | ||
|
|
1484818974 | ||
|
|
1d41687786 | ||
|
|
401fb48b86 | ||
|
|
91619b87db | ||
|
|
844e73ca29 | ||
|
|
ee91964998 | ||
|
|
1eaf228a30 | ||
|
|
9639143df4 | ||
|
|
e9f4912665 | ||
|
|
ce21ea78eb | ||
|
|
13ac9d18af | ||
|
|
1a8f849693 | ||
|
|
f0cbbc3a84 | ||
|
|
4834f99da9 | ||
|
|
394469a807 | ||
|
|
b7be91e04d | ||
|
|
278ffa3372 | ||
|
|
601cbd83ff | ||
|
|
43b35a77bb | ||
|
|
f06efd4d02 | ||
|
|
5887071319 | ||
|
|
fa111db91b | ||
|
|
b6cdabff36 | ||
|
|
af314dabfb | ||
|
|
11f6273a3a | ||
|
|
4b51eb7e06 | ||
|
|
671129bc32 | ||
|
|
358c3a5bb2 | ||
|
|
7c4b8866d3 | ||
|
|
dafd5619e6 | ||
|
|
05144abd6a | ||
|
|
629a3d11c6 | ||
|
|
bbd49252cf | ||
|
|
7c3a7513e6 | ||
|
|
e180398637 | ||
|
|
cbd4420fe9 | ||
|
|
f7408dede9 | ||
|
|
c324ccc12a | ||
|
|
84bbc2d7ce | ||
|
|
e22d6924cd | ||
|
|
4472a2a0b0 | ||
|
|
4d6e28c725 | ||
|
|
881424d03a | ||
|
|
5842619206 | ||
|
|
73272e266b | ||
|
|
4e84b9a121 | ||
|
|
7d16a8348c | ||
|
|
4d1692db7b | ||
|
|
3009d729fa | ||
|
|
ceeabfeb00 | ||
|
|
bfa5180c1a | ||
|
|
6efe29f7ef | ||
|
|
7826ecfa58 | ||
|
|
d615ac23ea | ||
|
|
4a0089dbfc | ||
|
|
d101db2a39 | ||
|
|
b147624f1c | ||
|
|
ec05bd3f53 | ||
|
|
952283fe16 | ||
|
|
47c0b1f680 | ||
|
|
cdef5f5826 | ||
|
|
6d01495bc7 | ||
|
|
d9f323874d | ||
|
|
45ffad45b0 | ||
|
|
dd4612bcef | ||
|
|
a0ce75b1ae | ||
|
|
031eb35b86 | ||
|
|
5bc923d58e | ||
|
|
a74a549b8c | ||
|
|
50df137c7f | ||
|
|
7bf77b566d | ||
|
|
48350d7654 | ||
|
|
72c4aa0078 | ||
|
|
c3c40225d6 | ||
|
|
b4acbb90b7 | ||
|
|
d1b2018700 | ||
|
|
d087bad9de | ||
|
|
2da6c4a0ec | ||
|
|
ede6637a14 | ||
|
|
36bd0b02d0 | ||
|
|
3a92c4f798 | ||
|
|
97de0ef5b4 | ||
|
|
bbf5f0efe0 | ||
|
|
ea76936508 | ||
|
|
f076cb0ead | ||
|
|
0f230253a8 | ||
|
|
13d2dde501 | ||
|
|
65fc0ed59c | ||
|
|
7ec4b8fb8c | ||
|
|
6415f0093b | ||
|
|
5795020403 | ||
|
|
f8e49ee3be | ||
|
|
1012807c65 | ||
|
|
1c7c27712e | ||
|
|
7f28c78d8c | ||
|
|
4bb874756d |
@@ -1,641 +0,0 @@
|
||||
{
|
||||
"projectName": "AFFiNE",
|
||||
"projectOwner": "toeverything",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 50,
|
||||
"commit": false,
|
||||
"commitConvention": "angular",
|
||||
"contributorsPerLine": 7,
|
||||
"badgeTemplate": "\n[all-contributors-badge]: https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square\n",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "doodlewind",
|
||||
"name": "Yifeng Wang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7312949?v=4",
|
||||
"profile": "https://github.com/doodlewind",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "darkskygit",
|
||||
"name": "DarkSky",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25152247?v=4",
|
||||
"profile": "https://darksky.eu.org/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tzhangchi",
|
||||
"name": "Chi Zhang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5910926?v=4",
|
||||
"profile": "http://zhangchi.page/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alt1o",
|
||||
"name": "wang xinglong",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/21084335?v=4",
|
||||
"profile": "https://github.com/alt1o",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Brooooooklyn",
|
||||
"name": "LongYinan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3468483?v=4",
|
||||
"profile": "https://github.com/Brooooooklyn",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hwangdev97",
|
||||
"name": "Hwang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24713927?v=4",
|
||||
"profile": "https://github.com/hwangdev97",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kobeshanks",
|
||||
"name": "kobeshanks",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82570088?v=4",
|
||||
"profile": "https://github.com/kobeshanks",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pengx17",
|
||||
"name": "Peng Xiao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/584378?v=4",
|
||||
"profile": "https://pengx17.vercel.app/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Saul-Mirone",
|
||||
"name": "Mirone",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10047788?v=4",
|
||||
"profile": "https://mirone.me/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zqran",
|
||||
"name": "zqran",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15389209?v=4",
|
||||
"profile": "https://github.com/zqran",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "SuneBear",
|
||||
"name": "Shule Hsiung",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7693264?v=4",
|
||||
"profile": "https://sunebear.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fundon",
|
||||
"name": "Fangdun Tsai",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/27926?v=4",
|
||||
"profile": "https://fundon.viz.rs/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lawvs",
|
||||
"name": "Whitewater",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18554747?v=4",
|
||||
"profile": "https://lawvs.github.io/profile/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zuoxiaodong0815",
|
||||
"name": "xiaodong zuo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53252747?v=4",
|
||||
"profile": "https://github.com/zuoxiaodong0815",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Himself65",
|
||||
"name": "Himself65",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/14026360?v=4",
|
||||
"profile": "https://github.com/Himself65",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "DiamondThree",
|
||||
"name": "DiamondThree",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24630517?v=4",
|
||||
"profile": "https://github.com/DiamondThree",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "QiShaoXuan",
|
||||
"name": "Qi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/22772830?v=4",
|
||||
"profile": "https://github.com/QiShaoXuan",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "colelawrence",
|
||||
"name": "Cole Lawrence",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2925395?v=4",
|
||||
"profile": "https://colelawrence.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "linonetwo",
|
||||
"name": "lin onetwo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3746270?v=4",
|
||||
"profile": "https://onetwo.ren/wiki",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "thorseraq",
|
||||
"name": "x1a0t",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20554850?v=4",
|
||||
"profile": "https://github.com/thorseraq",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "HeJiachen-PM",
|
||||
"name": "HeJiachen-PM",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/79301703?v=4",
|
||||
"profile": "https://github.com/HeJiachen-PM",
|
||||
"contributions": [
|
||||
"research",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joebeijing",
|
||||
"name": "houjoe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/22443345?v=4",
|
||||
"profile": "https://www.notion.so/houjoe/Joe-2a85f5be01004cd2b6a5ad26fbb948b1",
|
||||
"contributions": [
|
||||
"research",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Yipei-Operation",
|
||||
"name": "Yipei Wei",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/79373028?v=4",
|
||||
"profile": "https://github.com/Yipei-Operation",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "VelikaHF",
|
||||
"name": "Velika",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/121547898?v=4",
|
||||
"profile": "https://github.com/VelikaHF",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Svaney-ssman",
|
||||
"name": "Svaney",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/110808979?v=4",
|
||||
"profile": "https://github.com/Svaney-ssman",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "xell",
|
||||
"name": "Guozhu Liu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/132558?v=4",
|
||||
"profile": "http://xell.me/",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fyZheng07",
|
||||
"name": "fyZheng07",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/63830919?v=4",
|
||||
"profile": "https://github.com/fyZheng07",
|
||||
"contributions": [
|
||||
"eventOrganizing",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "CJSS",
|
||||
"name": "CJSS",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4605025?v=4",
|
||||
"profile": "https://github.com/CJSS",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JimmFly",
|
||||
"name": "JimmFly",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/102217452?v=4",
|
||||
"profile": "https://github.com/JimmFly",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mitsuhatu",
|
||||
"name": "mitsuhatu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/110213079?v=4",
|
||||
"profile": "https://github.com/mitsuhatu",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Austaras",
|
||||
"name": "Austaras",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15013925?v=4",
|
||||
"profile": "https://shockwave.me/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "uptonking",
|
||||
"name": "Jin Yao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11391549?v=4",
|
||||
"profile": "https://github.com/uptonking",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "CarlosZoft",
|
||||
"name": "Carlos Rafael ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/62192072?v=4",
|
||||
"profile": "https://github.com/CarlosZoft",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "caleboleary",
|
||||
"name": "Caleb OLeary",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12816579?v=4",
|
||||
"profile": "https://github.com/caleboleary",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "westongraham",
|
||||
"name": "Weston Graham",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/89493023?v=4",
|
||||
"profile": "https://github.com/westongraham",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "SaikaSakura",
|
||||
"name": "MingLIang Wang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11530942?v=4",
|
||||
"profile": "https://github.com/SaikaSakura",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fanjing22",
|
||||
"name": "fanjing22",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/109729699?v=4",
|
||||
"profile": "https://github.com/fanjing22",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pointmax",
|
||||
"name": "pointmax",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/49361135?v=4",
|
||||
"profile": "https://github.com/pointmax",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "liby",
|
||||
"name": "Bryan Lee",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/38807139?v=4",
|
||||
"profile": "https://liby.github.io/notes",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chenmoonmo",
|
||||
"name": "Simon Li",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/36295999?v=4",
|
||||
"profile": "https://github.com/chenmoonmo",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "githbq",
|
||||
"name": "Bob Hu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10009709?v=4",
|
||||
"profile": "https://github.com/githbq",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lucky-chap",
|
||||
"name": "Quavo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/67266933?v=4",
|
||||
"profile": "https://quavo.vercel.app/",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "LuciNyan",
|
||||
"name": "子瞻 Luci",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/22126563?v=4",
|
||||
"profile": "https://github.com/LuciNyan",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "m1911star",
|
||||
"name": "Horus",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4948120?v=4",
|
||||
"profile": "http://blog.ipili.me/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"platform"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fanshyiis",
|
||||
"name": "Super.x",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15103283?v=4",
|
||||
"profile": "https://segmentfault.com/u/qzuser_584786517d31a",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wangyu-1999",
|
||||
"name": "Wang Yu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/80874770?v=4",
|
||||
"profile": "https://wangyu-1999.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "felixonmars",
|
||||
"name": "Felix Yan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1006477?v=4",
|
||||
"profile": "https://felixc.at/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lynettelopez",
|
||||
"name": "Lynette Lopez",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32908859?v=4",
|
||||
"profile": "https://github.com/lynettelopez",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Zheaoli",
|
||||
"name": "Manjusaka",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7054676?v=4",
|
||||
"profile": "http://manjusaka.itscoder.com/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sudongyuer",
|
||||
"name": "Frozen FIsh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/76603360?v=4",
|
||||
"profile": "https://juejin.cn/user/2867982785579102/posts?sort=popular",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MuhammedFaraz",
|
||||
"name": "Mohammed Faraz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/92734739?v=4",
|
||||
"profile": "https://github.com/MuhammedFaraz",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Pranav4399",
|
||||
"name": "Pranav Sriram ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28348429?v=4",
|
||||
"profile": "https://pranavsriram.dev/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Reson-a",
|
||||
"name": "Reson-a",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20806266?v=4",
|
||||
"profile": "https://github.com/Reson-a",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hezhizhen",
|
||||
"name": "Zhizhen He",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7611700?v=4",
|
||||
"profile": "https://t.me/littlepoint",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "AkaraChen",
|
||||
"name": "AkaraChen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/85140972?v=4",
|
||||
"profile": "https://akr.moe/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "suyanhanx",
|
||||
"name": "Suyan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24221472?v=4",
|
||||
"profile": "https://github.com/suyanhanx",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hehex9",
|
||||
"name": "hehe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9209882?v=4",
|
||||
"profile": "https://github.com/hehex9",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "albertodlc",
|
||||
"name": "Alberto de la Cruz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32411964?v=4",
|
||||
"profile": "https://github.com/albertodlc",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "AlessioGr",
|
||||
"name": "Alessio Gravili",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/70709113?v=4",
|
||||
"profile": "https://github.com/AlessioGr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lzlme",
|
||||
"name": "Zhilin Liu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/117659326?v=4",
|
||||
"profile": "https://github.com/lzlme",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "suica",
|
||||
"name": "Sg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8041462?v=4",
|
||||
"profile": "https://github.com/suica",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sinchang",
|
||||
"name": "Jeff Wen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3297859?v=4",
|
||||
"profile": "https://sinchang.me/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "m1212e",
|
||||
"name": "m1212e",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/14091540?v=4",
|
||||
"profile": "https://m1212e.github.io/portfolio/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "adityash1",
|
||||
"name": "Aditya Sharma",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/65771169?v=4",
|
||||
"profile": "https://adityash1.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sheben404",
|
||||
"name": "Kehan Wang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/61317160?v=4",
|
||||
"profile": "https://github.com/sheben404",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "VictorNanka",
|
||||
"name": "VictorNanka",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/30154366?v=4",
|
||||
"profile": "https://github.com/VictorNanka",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
"storage",
|
||||
"infra",
|
||||
"plugin-cli",
|
||||
"sdk"
|
||||
"sdk",
|
||||
"plugin"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,3 +12,5 @@ e2e-dist-*
|
||||
static
|
||||
web-static
|
||||
public
|
||||
packages/sdk/src/*.d.ts
|
||||
packages/sdk/src/*.js
|
||||
|
||||
14
.eslintrc.js
@@ -22,7 +22,7 @@ const createPattern = packageName => [
|
||||
allowTypeImports: false,
|
||||
},
|
||||
{
|
||||
group: ['@blocksuite /store'],
|
||||
group: ['@blocksuite/store'],
|
||||
message: "Import from '@blocksuite/global/utils'",
|
||||
importNames: ['assertExists', 'assertEquals'],
|
||||
},
|
||||
@@ -31,6 +31,11 @@ const createPattern = packageName => [
|
||||
message: 'Use `useNavigateHelper` instead',
|
||||
importNames: ['useNavigate'],
|
||||
},
|
||||
{
|
||||
group: ['yjs'],
|
||||
message: 'Do not use this API because it has a bug',
|
||||
importNames: ['mergeUpdates'],
|
||||
},
|
||||
];
|
||||
|
||||
const allPackages = [
|
||||
@@ -155,6 +160,11 @@ const config = {
|
||||
message: 'Use `useNavigateHelper` instead',
|
||||
importNames: ['useNavigate'],
|
||||
},
|
||||
{
|
||||
group: ['yjs'],
|
||||
message: 'Do not use this API because it has a bug',
|
||||
importNames: ['mergeUpdates'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -214,6 +224,7 @@ const config = {
|
||||
ignoreIIFE: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-misused-promises': ['error'],
|
||||
},
|
||||
})),
|
||||
{
|
||||
@@ -239,6 +250,7 @@ const config = {
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-floating-promises': 0,
|
||||
'@typescript-eslint/no-misused-promises': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
14
.github/actions/setup-maker/action.yml
vendored
@@ -1,12 +1,16 @@
|
||||
name: Setup maker
|
||||
description: 'Setup maker dmg for electron'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Install create-dmg from brew'
|
||||
- name: 'Install @electron-forge/maker-dmg'
|
||||
if: runner.os == 'macos'
|
||||
shell: bash
|
||||
run: brew install create-dmg
|
||||
- name: 'Build @affine/makder-dmg'
|
||||
shell: bash
|
||||
run: yarn nx build @affine/maker-dmg
|
||||
working-directory: ./apps/electron
|
||||
run: yarn add @electron-forge/maker-dmg --dev
|
||||
env:
|
||||
HUSKY: '0'
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||
SENTRYCLI_SKIP_DOWNLOAD: '1'
|
||||
|
||||
6
.github/actions/setup-node/action.yml
vendored
@@ -17,10 +17,6 @@ inputs:
|
||||
description: 'Download the Electron binary'
|
||||
required: false
|
||||
default: 'true'
|
||||
npm-token:
|
||||
description: 'The NPM token to use for private packages.'
|
||||
required: false
|
||||
default: ''
|
||||
hard-link-nm:
|
||||
description: 'set nmMode to hardlinks-local in .yarnrc.yml'
|
||||
required: false
|
||||
@@ -48,7 +44,6 @@ runs:
|
||||
shell: bash
|
||||
run: yarn install ${{ inputs.extra-flags }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
|
||||
HUSKY: '0'
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||
@@ -59,7 +54,6 @@ runs:
|
||||
shell: bash
|
||||
run: yarn install ${{ inputs.extra-flags }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
|
||||
HUSKY: '0'
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||
|
||||
@@ -3,7 +3,7 @@ updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
interval: 'weekly'
|
||||
versioning-strategy: increase
|
||||
commit-message:
|
||||
prefix: 'chore'
|
||||
64
.github/workflows/build.yml
vendored
@@ -59,6 +59,25 @@ jobs:
|
||||
- name: Run Type Check
|
||||
run: yarn typecheck
|
||||
|
||||
build-prototype:
|
||||
name: Build Prototype
|
||||
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
|
||||
@@ -280,6 +299,49 @@ 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
|
||||
@@ -415,8 +477,6 @@ jobs:
|
||||
with:
|
||||
playwright-install: true
|
||||
hard-link-nm: false
|
||||
- name: Setup Maker
|
||||
uses: ./.github/actions/setup-maker
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
|
||||
2
.github/workflows/cancel.yml
vendored
@@ -14,5 +14,5 @@ jobs:
|
||||
- uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
# See https://api.github.com/repos/toeverything/AFFiNE/actions/workflows
|
||||
workflow_id: 44038251, 61883931
|
||||
workflow_id: 44038251, 61883931, 65188160
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
2
.github/workflows/nightly-build.yml
vendored
@@ -112,8 +112,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Maker
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-maker
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
|
||||
2
.github/workflows/release-desktop-app.yml
vendored
@@ -114,8 +114,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Maker
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-maker
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
|
||||
41
README.md
@@ -14,15 +14,6 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<!--
|
||||
Make New Badge Pattern badges inline
|
||||
See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
-->
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
|
||||
[all-contributors-badge]: https://img.shields.io/badge/all_contributors-66-orange.svg?style=flat-square
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[?style=flat-square&logoColor=white&logo=affine>)](https://app.affine.pro)
|
||||
[](https://affine.pro/download)
|
||||
@@ -39,6 +30,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
[![React-version-icon]](https://reactjs.org/)
|
||||
[![blocksuite-icon]](https://github.com/toeverything/blocksuite)
|
||||
[![Rust-version-icon]](https://www.rust-lang.org/)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE?ref=badge_shield)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -122,9 +114,12 @@ If you have questions, you are welcome to contact us. One of the best places to
|
||||
|
||||
## Plugins
|
||||
|
||||
> Plugins are a way to extend the functionality of AFFiNE.
|
||||
> Plugins are a way to extend the functionality of AFFiNE. You can use plugins to add new blocks, new features, and even new ways to edit content.
|
||||
>
|
||||
> (Currently, plugins are under heavy development, and the SDK is not yet available.)
|
||||
> (Currently, the plugin system is under heavy development. You will see the plugin system in the canary release.)
|
||||
|
||||
- [@affine/sdk](./packages/sdk) - SDK for developing plugins
|
||||
- [@affine/plugin-cli](./packages/plugin-cli) - CLI for developing plugins
|
||||
|
||||
| Official Plugin | Description |
|
||||
| ----------------------------------------------------- | ----------------------------------------- |
|
||||
@@ -136,15 +131,15 @@ If you have questions, you are welcome to contact us. One of the best places to
|
||||
|
||||
We would also like to give thanks to open-source projects that make AFFiNE possible:
|
||||
|
||||
- [BlockSuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE.
|
||||
- [blocksuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE.
|
||||
- [OctoBase](https://github.com/toeverything/OctoBase) - 🐙 OctoBase is the open-source database behind AFFiNE, local-first, yet collaborative. A light-weight, scalable, data engine written in Rust.
|
||||
- [Yjs](https://github.com/yjs/yjs) - Fundamental support of CRDTs for our implementation on state management and data sync.
|
||||
- [Electron](https://github.com/electron/electron) - Build cross-platform desktop apps with JavaScript, HTML, and CSS.
|
||||
- [React](https://github.com/facebook/react) - View layer support and web GUI framework.
|
||||
- [Rust](https://github.com/rust-lang/rust) - High performance language that extends the ability and availability of our real-time backend, OctoBase.
|
||||
- [yjs](https://github.com/yjs/yjs) - Fundamental support of CRDTs for our implementation on state management and data sync.
|
||||
- [electron](https://github.com/electron/electron) - Build cross-platform desktop apps with JavaScript, HTML, and CSS.
|
||||
- [React](https://github.com/facebook/react) - The library for web and native user interfaces.
|
||||
- [napi-rs](https://github.com/napi-rs/napi-rs) - A framework for building compiled Node.js add-ons in Rust via Node-API.
|
||||
- [Jotai](https://github.com/pmndrs/jotai) - Primitive and flexible state management for React.
|
||||
- [MUI](https://github.com/mui/material-ui) - Our most used graphic UI component library.
|
||||
- [async-call-rpc](https://github.com/Jack-Works/async-call-rpc) - A lightweight JSON RPC client & server.
|
||||
- [Vite](https://github.com/vitejs/vite) - Next generation frontend tooling.
|
||||
- Other upstream [dependencies](https://github.com/toeverything/AFFiNE/network/dependencies).
|
||||
|
||||
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.
|
||||
@@ -168,15 +163,11 @@ Some amazing companies including AFFiNE are looking for developers! Are you inte
|
||||
|
||||
## Upgrading
|
||||
|
||||
For upgrading information please see our [update page].
|
||||
For upgrading information, please see our [update page].
|
||||
|
||||
## Feature Request
|
||||
|
||||
For feature request please see [community.affine.pro](https://community.affine.pro/c/feature-requests/).
|
||||
|
||||
## Is it awesome?
|
||||
|
||||
[These people] seem to like it.
|
||||
For feature request, please see [community.affine.pro](https://community.affine.pro/c/feature-requests/).
|
||||
|
||||
## Building
|
||||
|
||||
@@ -191,9 +182,9 @@ See [docs/contributing/tutorial.md](./docs/contributing/tutorial.md) for details
|
||||
|
||||
See [LICENSE] for details.
|
||||
|
||||
[all-contributors-badge]: https://img.shields.io/github/all-contributors/toeverything/AFFiNE/master?color=orange
|
||||
[license]: ./LICENSE
|
||||
[building.md]: ./docs/BUILDING.md
|
||||
[these people]: https://twitter.com/AffineOfficial/followers
|
||||
[update page]: https://affine.pro/blog?tag=Release%20Note
|
||||
[jobs available]: ./docs/jobs.md
|
||||
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
|
||||
@@ -205,3 +196,5 @@ See [LICENSE] for details.
|
||||
[typescript-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/typescript
|
||||
[react-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/react?filename=apps%2Fcore%2Fpackage.json&color=rgb(97%2C228%2C251)
|
||||
[blocksuite-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/@blocksuite/store?color=6880ff&filename=apps%2Fcore%2Fpackage.json&label=blocksuite
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftoeverything%2FAFFiNE?ref=badge_large)
|
||||
|
||||
@@ -8,7 +8,7 @@ AFFiNE Developer Documentation using [waku](https://github.com/dai-shi/waku).
|
||||
|
||||
## electron
|
||||
|
||||
> `web` needs to be built before electron.
|
||||
> `core` needs to be built before electron.
|
||||
|
||||
AFFiNE Desktop (macOS, Linux and Windows Distribution) using [Electron](https://www.electronjs.org/).
|
||||
|
||||
@@ -20,6 +20,10 @@ Server using [Nest.js](https://nestjs.com/).
|
||||
|
||||
Storybook using [Storybook](https://storybook.js.org/).
|
||||
|
||||
## Core
|
||||
## prototype
|
||||
|
||||
AFFiNE Core Application using [React.js](https://reactjs.org/).
|
||||
AFFiNE Prototype using [React.js](https://reactjs.org/) + [Vite](https://vitejs.dev/).
|
||||
|
||||
## core
|
||||
|
||||
AFFiNE Core Application using [React.js](https://reactjs.org/) + [Webpack](https://webpack.js.org/).
|
||||
|
||||
@@ -73,6 +73,8 @@ export const createConfiguration: (
|
||||
) => webpack.Configuration = (buildFlags, runtimeConfig) => {
|
||||
let publicPath = process.env.PUBLIC_PATH ?? '/';
|
||||
|
||||
const blocksuiteBaseDir = buildFlags.localBlockSuite;
|
||||
|
||||
const cacheKey = computeCacheKey(buildFlags);
|
||||
|
||||
const config = {
|
||||
@@ -82,6 +84,7 @@ export const createConfiguration: (
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
outputModule: false,
|
||||
syncWebAssembly: true,
|
||||
},
|
||||
output: {
|
||||
environment: {
|
||||
@@ -121,6 +124,60 @@ export const createConfiguration: (
|
||||
'.mjs': ['.mjs', '.mts'],
|
||||
},
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
fallback:
|
||||
blocksuiteBaseDir === undefined
|
||||
? undefined
|
||||
: {
|
||||
events: false,
|
||||
},
|
||||
alias:
|
||||
blocksuiteBaseDir === undefined
|
||||
? undefined
|
||||
: {
|
||||
yjs: require.resolve('yjs'),
|
||||
'@blocksuite/block-std': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'block-std'
|
||||
),
|
||||
'@blocksuite/blocks': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'blocks'
|
||||
),
|
||||
'@blocksuite/editor': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'editor'
|
||||
),
|
||||
'@blocksuite/global': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'global'
|
||||
),
|
||||
'@blocksuite/lit': resolve(blocksuiteBaseDir, 'packages', 'lit'),
|
||||
'@blocksuite/phasor': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'phasor'
|
||||
),
|
||||
'@blocksuite/store/providers/broadcast-channel': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'store',
|
||||
'src/providers/broadcast-channel'
|
||||
),
|
||||
'@blocksuite/store': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'store'
|
||||
),
|
||||
'@blocksuite/virgo': resolve(
|
||||
blocksuiteBaseDir,
|
||||
'packages',
|
||||
'virgo'
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
cache: {
|
||||
@@ -167,8 +224,7 @@ export const createConfiguration: (
|
||||
oneOf: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
// Compile all ts files in the workspace
|
||||
include: resolve(rootPath, '..', '..'),
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('swc-loader'),
|
||||
options: {
|
||||
// https://swc.rs/docs/configuring-swc/
|
||||
@@ -179,9 +235,10 @@ export const createConfiguration: (
|
||||
dynamicImport: true,
|
||||
topLevelAwait: false,
|
||||
tsx: true,
|
||||
decorators: true,
|
||||
},
|
||||
target: 'es2022',
|
||||
externalHelpers: true,
|
||||
externalHelpers: false,
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
@@ -191,6 +248,7 @@ export const createConfiguration: (
|
||||
emitFullSignatures: true,
|
||||
},
|
||||
},
|
||||
useDefineForClassFields: false,
|
||||
},
|
||||
experimental: {
|
||||
keepImportAssertions: true,
|
||||
|
||||
@@ -7,5 +7,6 @@ export function computeCacheKey(buildFlags: BuildFlags) {
|
||||
buildFlags.mode,
|
||||
buildFlags.distribution,
|
||||
buildFlags.channel,
|
||||
...(buildFlags.localBlockSuite ? [buildFlags.localBlockSuite] : []),
|
||||
].join('-');
|
||||
}
|
||||
|
||||
@@ -15,21 +15,24 @@ export default async function (cli_env: any, _: any) {
|
||||
const config = createConfiguration(flags, runtimeConfig);
|
||||
return merge(config, {
|
||||
entry: {
|
||||
'polyfill/intl-segmenter': {
|
||||
import: resolve(rootPath, 'src/polyfill/intl-segmenter.ts'),
|
||||
},
|
||||
'polyfill/ses': {
|
||||
import: resolve(rootPath, 'src/polyfill/ses.ts'),
|
||||
},
|
||||
plugin: {
|
||||
dependOn: ['polyfill/ses'],
|
||||
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses'],
|
||||
import: resolve(rootPath, 'src/bootstrap/register-plugins.ts'),
|
||||
},
|
||||
app: {
|
||||
chunkLoading: 'import',
|
||||
dependOn: ['polyfill/ses', 'plugin'],
|
||||
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses', 'plugin'],
|
||||
import: resolve(rootPath, 'src/index.tsx'),
|
||||
},
|
||||
'_plugin/index.test': {
|
||||
chunkLoading: 'import',
|
||||
dependOn: ['polyfill/ses', 'plugin'],
|
||||
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses', 'plugin'],
|
||||
import: resolve(rootPath, 'src/_plugin/index.test.tsx'),
|
||||
},
|
||||
},
|
||||
@@ -39,7 +42,7 @@ export default async function (cli_env: any, _: any) {
|
||||
inject: 'body',
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
chunks: ['app', 'plugin', 'polyfill/ses'],
|
||||
chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'],
|
||||
filename: 'index.html',
|
||||
}),
|
||||
new HTMLPlugin({
|
||||
@@ -47,7 +50,12 @@ export default async function (cli_env: any, _: any) {
|
||||
inject: 'body',
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
chunks: ['_plugin/index.test', 'plugin', 'polyfill/ses'],
|
||||
chunks: [
|
||||
'_plugin/index.test',
|
||||
'plugin',
|
||||
'polyfill/intl-segmenter',
|
||||
'polyfill/ses',
|
||||
],
|
||||
filename: '_plugin/index.html',
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@affine/core",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "0.8.0-canary.9",
|
||||
"version": "0.8.0-canary.20",
|
||||
"scripts": {
|
||||
"build": "yarn -T run build-core",
|
||||
"dev": "yarn -T run dev-core",
|
||||
@@ -18,29 +18,33 @@
|
||||
"@affine/jotai": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/icons": "^2.1.29",
|
||||
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230811201552-f37162ea-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230811201552-f37162ea-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230811201552-f37162ea-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230811201552-f37162ea-nightly",
|
||||
"@blocksuite/icons": "^2.1.31",
|
||||
"@blocksuite/lit": "0.0.0-20230811201552-f37162ea-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230811201552-f37162ea-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.2",
|
||||
"@mui/material": "^5.14.4",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@toeverything/components": "^0.0.10",
|
||||
"@types/lodash.throttle": "^4.1.7",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
"css-spring": "^4.1.0",
|
||||
"cssnano": "^6.0.1",
|
||||
"graphql": "^16.7.1",
|
||||
"jotai": "^2.2.2",
|
||||
"jotai-devtools": "^0.6.0",
|
||||
"lit": "^2.7.6",
|
||||
"intl-segmenter-polyfill-rs": "^0.1.5",
|
||||
"jotai": "^2.3.1",
|
||||
"jotai-devtools": "^0.6.1",
|
||||
"lit": "^2.8.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lottie-web": "^5.12.2",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"next-themes": "^0.2.1",
|
||||
@@ -49,20 +53,20 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-is": "18.2.0",
|
||||
"react-resizable-panels": "^0.0.54",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"ses": "^0.18.5",
|
||||
"swr": "2.1.5",
|
||||
"ses": "^0.18.7",
|
||||
"swr": "2.2.1",
|
||||
"y-protocols": "^1.0.5",
|
||||
"yjs": "^13.6.7",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@perfsee/webpack": "^1.8.2",
|
||||
"@perfsee/webpack": "^1.8.4",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@sentry/webpack-plugin": "^2.5.0",
|
||||
"@sentry/webpack-plugin": "^2.6.2",
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"@swc/core": "^1.3.71",
|
||||
"@swc/core": "^1.3.76",
|
||||
"@types/webpack-env": "^1.18.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
@@ -72,7 +76,7 @@
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-plugin-coverage-instrument": "^0.0.19",
|
||||
"swc-plugin-coverage-instrument": "^0.0.20",
|
||||
"thread-loader": "^4.0.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"webpack": "^5.88.2",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"{projectRoot}/.webpack/**/*",
|
||||
"{projectRoot}/**/*",
|
||||
"{projectRoot}/public/**/*",
|
||||
"{workspaceRoot}/packages/env/src/**/*",
|
||||
"{workspaceRoot}/packages/component/src/**/*",
|
||||
"{workspaceRoot}/packages/debug/src/**/*",
|
||||
"{workspaceRoot}/packages/graphql/src/**/*",
|
||||
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 723 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 971 KiB |
@@ -1,5 +1,5 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import { loadedPluginNameAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import { use } from 'foxact/use';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Provider } from 'jotai/react';
|
||||
@@ -17,7 +17,7 @@ async function main() {
|
||||
|
||||
const App = () => {
|
||||
use(pluginRegisterPromise);
|
||||
const plugins = useAtomValue(registeredPluginAtom);
|
||||
const plugins = useAtomValue(loadedPluginNameAtom);
|
||||
_pluginNestedImportsMap.forEach(value => {
|
||||
const exports = value.get('index.js');
|
||||
assertExists(exports);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { initEmptyPage, initPageWithPreloading } from '@affine/env/blocksuite';
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import {
|
||||
DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX,
|
||||
DEFAULT_WORKSPACE_NAME,
|
||||
@@ -20,6 +20,7 @@ import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
|
||||
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
@@ -41,21 +42,18 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||
const page = blockSuiteWorkspace.createPage({
|
||||
id: `${blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`,
|
||||
});
|
||||
if (runtimeConfig.enablePreloading) {
|
||||
initPageWithPreloading(page).catch(err => {
|
||||
buildShowcaseWorkspace(blockSuiteWorkspace).catch(err => {
|
||||
logger.error('init page with preloading failed', err);
|
||||
});
|
||||
} else {
|
||||
const page = blockSuiteWorkspace.createPage({
|
||||
id: `${blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`,
|
||||
});
|
||||
initEmptyPage(page).catch(error => {
|
||||
logger.error('init page with empty failed', error);
|
||||
});
|
||||
}
|
||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
jumpOnce: true,
|
||||
});
|
||||
const provider = createIndexedDBDownloadProvider(
|
||||
blockSuiteWorkspace.id,
|
||||
blockSuiteWorkspace.doc,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
import '@toeverything/components/style.css';
|
||||
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
|
||||
@@ -70,12 +70,16 @@ export const guideOnboardingAtom = atom<
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
export const guideDownloadClientTipAtom = atom<
|
||||
Guide['downloadClientTip'],
|
||||
[open: boolean],
|
||||
void
|
||||
>(
|
||||
get => {
|
||||
if (environment.isDesktop) {
|
||||
return false;
|
||||
}
|
||||
return get(guidePrimitiveAtom).downloadClientTip;
|
||||
},
|
||||
(_, set, open) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { PrimitiveAtom } from 'jotai';
|
||||
import { atom } from 'jotai';
|
||||
import { atomFamily, atomWithStorage } from 'jotai/utils';
|
||||
import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
|
||||
|
||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||
import type { SettingProps } from '../components/affine/setting-modal';
|
||||
@@ -59,19 +61,16 @@ const defaultPageSetting = {
|
||||
mode: 'page',
|
||||
} satisfies PageLocalSetting;
|
||||
|
||||
export const pageSettingFamily = atomFamily((pageId: string) =>
|
||||
export const pageSettingFamily: AtomFamily<
|
||||
string,
|
||||
PrimitiveAtom<PageLocalSetting>
|
||||
> = atomFamily((pageId: string) =>
|
||||
atom(
|
||||
get =>
|
||||
get(pageSettingsBaseAtom)[pageId] ?? {
|
||||
...defaultPageSetting,
|
||||
},
|
||||
(
|
||||
get,
|
||||
set,
|
||||
patch:
|
||||
| Partial<PageLocalSetting>
|
||||
| ((prevSetting: PageLocalSetting | undefined) => void)
|
||||
) => {
|
||||
(get, set, patch) => {
|
||||
set(recentPageSettingsBaseAtom, ids => {
|
||||
// pick 3 recent page ids
|
||||
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
|
||||
|
||||
22
apps/core/src/bootstrap/plugins/setup-imports-map.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
type ExportsPromiseOrExports =
|
||||
| Promise<{ [key: string]: any }>
|
||||
| { [key: string]: any };
|
||||
|
||||
export async function setupImportsMap(
|
||||
map: Map<string, Map<string, any>>,
|
||||
imports: Record<string, ExportsPromiseOrExports>
|
||||
) {
|
||||
for (const [key, value] of Object.entries(imports)) {
|
||||
let module: { [key: string]: any };
|
||||
if (value instanceof Promise) {
|
||||
module = await value;
|
||||
} else {
|
||||
module = value;
|
||||
}
|
||||
const moduleMap = new Map();
|
||||
map.set(key, moduleMap);
|
||||
for (const [exportName, exportValue] of Object.entries(module)) {
|
||||
moduleMap.set(exportName, exportValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,140 @@
|
||||
import * as AFFiNEComponent from '@affine/component';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { CallbackMap, PluginContext } from '@affine/sdk/entry';
|
||||
import { FormatQuickBar } from '@blocksuite/blocks';
|
||||
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
||||
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
|
||||
import type {
|
||||
CallbackMap,
|
||||
ExpectedLayout,
|
||||
LayoutNode,
|
||||
PluginContext,
|
||||
} from '@affine/sdk/entry';
|
||||
import { AffineFormatBarWidget } from '@blocksuite/blocks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import * as Icons from '@blocksuite/icons';
|
||||
import {
|
||||
addCleanup,
|
||||
pluginEditorAtom,
|
||||
pluginHeaderItemAtom,
|
||||
pluginSettingAtom,
|
||||
pluginWindowAtom,
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
import {
|
||||
contentLayoutAtom,
|
||||
currentPageAtom,
|
||||
currentWorkspaceAtom,
|
||||
editorItemsAtom,
|
||||
headerItemsAtom,
|
||||
rootStore,
|
||||
settingItemsAtom,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import * as Jotai from 'jotai/index';
|
||||
import { atom } from 'jotai';
|
||||
import { Provider } from 'jotai/react';
|
||||
import * as JotaiUtils from 'jotai/utils';
|
||||
import * as React from 'react';
|
||||
import { createElement, type PropsWithChildren } from 'react';
|
||||
import * as ReactJSXRuntime from 'react/jsx-runtime';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import * as ReactDomClient from 'react-dom/client';
|
||||
import * as SWR from 'swr';
|
||||
|
||||
import { createFetch } from './endowments/fercher';
|
||||
import { createTimers } from './endowments/timer';
|
||||
import { setupImportsMap } from './setup-imports-map';
|
||||
|
||||
const dynamicImportKey = '$h_import';
|
||||
const dynamicImportKey = '$h_import';
|
||||
|
||||
const permissionLogger = new DebugLogger('plugins:permission');
|
||||
const importLogger = new DebugLogger('plugins:import');
|
||||
|
||||
const setupRootImportsMap = () => {
|
||||
_rootImportsMap.set('react', new Map(Object.entries(React)));
|
||||
_rootImportsMap.set(
|
||||
'react/jsx-runtime',
|
||||
new Map(Object.entries(ReactJSXRuntime))
|
||||
);
|
||||
_rootImportsMap.set('react-dom', new Map(Object.entries(ReactDom)));
|
||||
_rootImportsMap.set(
|
||||
'react-dom/client',
|
||||
new Map(Object.entries(ReactDomClient))
|
||||
);
|
||||
_rootImportsMap.set('@blocksuite/icons', new Map(Object.entries(Icons)));
|
||||
_rootImportsMap.set(
|
||||
'@affine/component',
|
||||
new Map(Object.entries(AFFiNEComponent))
|
||||
);
|
||||
_rootImportsMap.set(
|
||||
'@blocksuite/blocks/std',
|
||||
new Map(Object.entries(BlockSuiteBlocksStd))
|
||||
);
|
||||
_rootImportsMap.set(
|
||||
'@blocksuite/global/utils',
|
||||
new Map(Object.entries(BlockSuiteGlobalUtils))
|
||||
);
|
||||
_rootImportsMap.set('jotai', new Map(Object.entries(Jotai)));
|
||||
_rootImportsMap.set('jotai/utils', new Map(Object.entries(JotaiUtils)));
|
||||
_rootImportsMap.set(
|
||||
'@affine/sdk/entry',
|
||||
new Map(
|
||||
Object.entries({
|
||||
rootStore: rootStore,
|
||||
currentWorkspaceAtom: currentWorkspaceAtom,
|
||||
currentPageAtom: currentPageAtom,
|
||||
contentLayoutAtom: contentLayoutAtom,
|
||||
})
|
||||
)
|
||||
);
|
||||
_rootImportsMap.set('swr', new Map(Object.entries(SWR)));
|
||||
};
|
||||
const pushLayoutAtom = atom<
|
||||
null,
|
||||
// fixme: check plugin name here
|
||||
[pluginName: string, create: (root: HTMLElement) => () => void],
|
||||
void
|
||||
>(null, (_, set, pluginName, callback) => {
|
||||
set(pluginWindowAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback,
|
||||
}));
|
||||
set(contentLayoutAtom, layout => {
|
||||
if (layout === 'editor') {
|
||||
return {
|
||||
direction: 'horizontal',
|
||||
first: 'editor',
|
||||
second: pluginName,
|
||||
splitPercentage: 70,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...layout,
|
||||
direction: 'horizontal',
|
||||
first: 'editor',
|
||||
second: {
|
||||
direction: 'horizontal',
|
||||
// fixme: incorrect type here
|
||||
first: layout.second,
|
||||
second: pluginName,
|
||||
splitPercentage: 70,
|
||||
},
|
||||
} as ExpectedLayout;
|
||||
}
|
||||
});
|
||||
addCleanup(pluginName, () => {
|
||||
set(deleteLayoutAtom, pluginName);
|
||||
});
|
||||
});
|
||||
|
||||
const deleteLayoutAtom = atom<null, [string], void>(null, (_, set, id) => {
|
||||
set(pluginWindowAtom, items => {
|
||||
const newItems = { ...items };
|
||||
delete newItems[id];
|
||||
return newItems;
|
||||
});
|
||||
const removeLayout = (layout: LayoutNode): LayoutNode => {
|
||||
if (layout === 'editor') {
|
||||
return 'editor';
|
||||
} else {
|
||||
if (typeof layout === 'string') {
|
||||
return layout as ExpectedLayout;
|
||||
}
|
||||
if (layout.first === id) {
|
||||
return layout.second;
|
||||
} else if (layout.second === id) {
|
||||
return layout.first;
|
||||
} else {
|
||||
return removeLayout(layout.second);
|
||||
}
|
||||
}
|
||||
};
|
||||
set(contentLayoutAtom, layout => {
|
||||
if (layout === 'editor') {
|
||||
return 'editor';
|
||||
} else {
|
||||
if (typeof layout === 'string') {
|
||||
return layout as ExpectedLayout;
|
||||
}
|
||||
if (layout.first === id) {
|
||||
return layout.second as ExpectedLayout;
|
||||
} else if (layout.second === id) {
|
||||
return layout.first as ExpectedLayout;
|
||||
} else {
|
||||
return removeLayout(layout.second) as ExpectedLayout;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// module -> importName -> updater[]
|
||||
export const _rootImportsMap = new Map<string, Map<string, any>>();
|
||||
setupRootImportsMap();
|
||||
const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
|
||||
react: import('react'),
|
||||
'react/jsx-runtime': import('react/jsx-runtime'),
|
||||
'react-dom': import('react-dom'),
|
||||
'react-dom/client': import('react-dom/client'),
|
||||
jotai: import('jotai'),
|
||||
'jotai/utils': import('jotai/utils'),
|
||||
swr: import('swr'),
|
||||
'@affine/component': import('@affine/component'),
|
||||
'@blocksuite/icons': import('@blocksuite/icons'),
|
||||
'@affine/sdk/entry': {
|
||||
rootStore: rootStore,
|
||||
currentWorkspaceAtom: currentWorkspaceAtom,
|
||||
currentPageAtom: currentPageAtom,
|
||||
pushLayoutAtom: pushLayoutAtom,
|
||||
deleteLayoutAtom: deleteLayoutAtom,
|
||||
},
|
||||
'@blocksuite/global/utils': import('@blocksuite/global/utils'),
|
||||
'@toeverything/infra/atom': import('@toeverything/infra/atom'),
|
||||
'@toeverything/components/button': import('@toeverything/components/button'),
|
||||
});
|
||||
|
||||
// pluginName -> module -> importName -> updater[]
|
||||
export const _pluginNestedImportsMap = new Map<
|
||||
string,
|
||||
@@ -135,7 +192,15 @@ const pluginFetch = createFetch({});
|
||||
const timer = createTimers(abortController.signal);
|
||||
|
||||
const sharedGlobalThis = Object.assign(Object.create(null), timer, {
|
||||
Object: globalThis.Object,
|
||||
fetch: pluginFetch,
|
||||
ReadableStream: globalThis.ReadableStream,
|
||||
Symbol: globalThis.Symbol,
|
||||
Error: globalThis.Error,
|
||||
TypeError: globalThis.TypeError,
|
||||
RangeError: globalThis.RangeError,
|
||||
console: globalThis.console,
|
||||
crypto: globalThis.crypto,
|
||||
});
|
||||
|
||||
const dynamicImportMap = new Map<
|
||||
@@ -206,6 +271,7 @@ export const createOrGetGlobalThis = (
|
||||
Object.create(null),
|
||||
sharedGlobalThis,
|
||||
{
|
||||
// fixme: vite build output bundle will have this, we should remove it
|
||||
process: Object.freeze({
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
@@ -222,6 +288,9 @@ export const createOrGetGlobalThis = (
|
||||
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
|
||||
const result = Reflect.get(window, key);
|
||||
if (typeof result === 'function') {
|
||||
if (result === ShadowRoot) {
|
||||
return result;
|
||||
}
|
||||
return function (...args: any[]) {
|
||||
permissionLogger.debug(
|
||||
`${pluginName} is calling window`,
|
||||
@@ -262,15 +331,11 @@ export const createOrGetGlobalThis = (
|
||||
userAgent: navigator.userAgent,
|
||||
},
|
||||
|
||||
// safe to use for all plugins
|
||||
Error: globalThis.Error,
|
||||
TypeError: globalThis.TypeError,
|
||||
RangeError: globalThis.RangeError,
|
||||
console: globalThis.console,
|
||||
crypto: globalThis.crypto,
|
||||
MouseEvent: globalThis.MouseEvent,
|
||||
KeyboardEvent: globalThis.KeyboardEvent,
|
||||
CustomEvent: globalThis.CustomEvent,
|
||||
|
||||
// copilot uses these
|
||||
CustomEvent: globalThis.CustomEvent,
|
||||
Date: globalThis.Date,
|
||||
Math: globalThis.Math,
|
||||
URL: globalThis.URL,
|
||||
@@ -284,6 +349,10 @@ export const createOrGetGlobalThis = (
|
||||
Blob: globalThis.Blob,
|
||||
ClipboardItem: globalThis.ClipboardItem,
|
||||
|
||||
// vue uses these
|
||||
Element: globalThis.Element,
|
||||
SVGElement: globalThis.SVGElement,
|
||||
|
||||
// fixme: use our own db api
|
||||
indexedDB: globalThis.indexedDB,
|
||||
IDBRequest: globalThis.IDBRequest,
|
||||
@@ -299,6 +368,7 @@ export const createOrGetGlobalThis = (
|
||||
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
|
||||
}
|
||||
);
|
||||
pluginGlobalThis.global = pluginGlobalThis;
|
||||
globalThisMap.set(pluginName, pluginGlobalThis);
|
||||
return pluginGlobalThis;
|
||||
};
|
||||
@@ -308,6 +378,7 @@ export const setupPluginCode = async (
|
||||
pluginName: string,
|
||||
filename: string
|
||||
) => {
|
||||
await rootImportsMapSetupPromise;
|
||||
if (!_pluginNestedImportsMap.has(pluginName)) {
|
||||
_pluginNestedImportsMap.set(pluginName, new Map());
|
||||
}
|
||||
@@ -396,7 +467,6 @@ const PluginProvider = ({ children }: PropsWithChildren) =>
|
||||
children
|
||||
);
|
||||
|
||||
const group = new DisposableGroup();
|
||||
const entryLogger = new DebugLogger('plugin:entry');
|
||||
|
||||
export const evaluatePluginEntry = (pluginName: string) => {
|
||||
@@ -409,31 +479,59 @@ export const evaluatePluginEntry = (pluginName: string) => {
|
||||
register: (part, callback) => {
|
||||
entryLogger.info(`Registering ${pluginName} to ${part}`);
|
||||
if (part === 'headerItem') {
|
||||
rootStore.set(headerItemsAtom, items => ({
|
||||
rootStore.set(pluginHeaderItemAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['headerItem'],
|
||||
}));
|
||||
addCleanup(pluginName, () => {
|
||||
rootStore.set(pluginHeaderItemAtom, items => {
|
||||
const newItems = { ...items };
|
||||
delete newItems[pluginName];
|
||||
return newItems;
|
||||
});
|
||||
});
|
||||
} else if (part === 'editor') {
|
||||
rootStore.set(editorItemsAtom, items => ({
|
||||
rootStore.set(pluginEditorAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['editor'],
|
||||
}));
|
||||
} else if (part === 'window') {
|
||||
rootStore.set(windowItemsAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['window'],
|
||||
}));
|
||||
addCleanup(pluginName, () => {
|
||||
rootStore.set(pluginEditorAtom, items => {
|
||||
const newItems = { ...items };
|
||||
delete newItems[pluginName];
|
||||
return newItems;
|
||||
});
|
||||
});
|
||||
} else if (part === 'setting') {
|
||||
rootStore.set(settingItemsAtom, items => ({
|
||||
rootStore.set(pluginSettingAtom, items => ({
|
||||
...items,
|
||||
[pluginName]: callback as CallbackMap['setting'],
|
||||
}));
|
||||
} else if (part === 'formatBar') {
|
||||
FormatQuickBar.customElements.push((page, getBlockRange) => {
|
||||
const div = document.createElement('div');
|
||||
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
|
||||
return div;
|
||||
addCleanup(pluginName, () => {
|
||||
rootStore.set(pluginSettingAtom, items => {
|
||||
const newItems = { ...items };
|
||||
delete newItems[pluginName];
|
||||
return newItems;
|
||||
});
|
||||
});
|
||||
} else if (part === 'formatBar') {
|
||||
const register = (widget: AffineFormatBarWidget) => {
|
||||
const div = document.createElement('div');
|
||||
const root = widget.root;
|
||||
const cleanup = (callback as CallbackMap['formatBar'])(
|
||||
div,
|
||||
widget.page,
|
||||
() => {
|
||||
return root.selectionManager.value;
|
||||
}
|
||||
);
|
||||
addCleanup(pluginName, () => {
|
||||
AffineFormatBarWidget.customElements.delete(register);
|
||||
cleanup();
|
||||
});
|
||||
return div;
|
||||
};
|
||||
AffineFormatBarWidget.customElements.add(register);
|
||||
} else {
|
||||
throw new Error(`Unknown part: ${part}`);
|
||||
}
|
||||
@@ -445,5 +543,5 @@ export const evaluatePluginEntry = (pluginName: string) => {
|
||||
if (typeof cleanup !== 'function') {
|
||||
throw new Error('Plugin entry must return a function');
|
||||
}
|
||||
group.add(cleanup);
|
||||
addCleanup(pluginName, cleanup);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import {
|
||||
builtinPluginPaths,
|
||||
enabledPluginAtom,
|
||||
invokeCleanup,
|
||||
pluginPackageJson,
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
import { loadedPluginNameAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import { packageJsonOutputSchema } from '@toeverything/infra/type';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
|
||||
|
||||
const builtinPluginUrl = new Set([
|
||||
'/plugins/bookmark',
|
||||
'/plugins/copilot',
|
||||
'/plugins/hello-world',
|
||||
'/plugins/image-preview',
|
||||
]);
|
||||
|
||||
const logger = new DebugLogger('register-plugins');
|
||||
|
||||
declare global {
|
||||
@@ -17,13 +18,50 @@ declare global {
|
||||
var __pluginPackageJson__: unknown[];
|
||||
}
|
||||
|
||||
globalThis.__pluginPackageJson__ = [];
|
||||
Object.defineProperty(globalThis, '__pluginPackageJson__', {
|
||||
get() {
|
||||
return rootStore.get(pluginPackageJson);
|
||||
},
|
||||
});
|
||||
|
||||
rootStore.sub(enabledPluginAtom, () => {
|
||||
const added = new Set<string>();
|
||||
const removed = new Set<string>();
|
||||
const enabledPlugin = new Set(rootStore.get(enabledPluginAtom));
|
||||
enabledPlugin.forEach(pluginName => {
|
||||
if (!enabledPluginSet.has(pluginName)) {
|
||||
added.add(pluginName);
|
||||
}
|
||||
});
|
||||
enabledPluginSet.forEach(pluginName => {
|
||||
if (!enabledPlugin.has(pluginName)) {
|
||||
removed.add(pluginName);
|
||||
}
|
||||
});
|
||||
// update plugins
|
||||
enabledPluginSet.clear();
|
||||
enabledPlugin.forEach(pluginName => {
|
||||
enabledPluginSet.add(pluginName);
|
||||
});
|
||||
added.forEach(pluginName => {
|
||||
evaluatePluginEntry(pluginName);
|
||||
});
|
||||
removed.forEach(pluginName => {
|
||||
invokeCleanup(pluginName);
|
||||
});
|
||||
});
|
||||
const enabledPluginSet = new Set(rootStore.get(enabledPluginAtom));
|
||||
const loadedAssets = new Set<string>();
|
||||
|
||||
// we will load all plugins in parallel from builtinPlugins
|
||||
export const pluginRegisterPromise = Promise.all(
|
||||
[...builtinPluginUrl].map(url => {
|
||||
[...builtinPluginPaths].map(url => {
|
||||
return fetch(`${url}/package.json`)
|
||||
.then(async res => {
|
||||
const packageJson = await res.json();
|
||||
const packageJson = (await res.json()) as z.infer<
|
||||
typeof packageJsonOutputSchema
|
||||
>;
|
||||
packageJsonOutputSchema.parse(packageJson);
|
||||
const {
|
||||
name: pluginName,
|
||||
affinePlugin: {
|
||||
@@ -32,7 +70,7 @@ export const pluginRegisterPromise = Promise.all(
|
||||
assets,
|
||||
},
|
||||
} = packageJson;
|
||||
globalThis.__pluginPackageJson__.push(packageJson);
|
||||
rootStore.set(pluginPackageJson, json => [...json, packageJson]);
|
||||
logger.debug(`registering plugin ${pluginName}`);
|
||||
logger.debug(`package.json: ${packageJson}`);
|
||||
if (!release && !runtimeConfig.enablePlugin) {
|
||||
@@ -40,14 +78,19 @@ export const pluginRegisterPromise = Promise.all(
|
||||
}
|
||||
const baseURL = url;
|
||||
const entryURL = `${baseURL}/${core}`;
|
||||
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
|
||||
rootStore.set(loadedPluginNameAtom, prev => [...prev, pluginName]);
|
||||
await setupPluginCode(baseURL, pluginName, core);
|
||||
console.log(`prepareImports for ${pluginName} done`);
|
||||
await fetch(entryURL).then(async () => {
|
||||
if (assets.length > 0) {
|
||||
await Promise.all(
|
||||
assets.map(async (asset: string) => {
|
||||
// todo(himself65): add assets into shadow dom
|
||||
if (loadedAssets.has(asset)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (asset.endsWith('.css')) {
|
||||
loadedAssets.add(asset);
|
||||
const res = await fetch(`${baseURL}/${asset}`);
|
||||
if (res.ok) {
|
||||
// todo: how to put css file into sandbox?
|
||||
@@ -65,7 +108,12 @@ export const pluginRegisterPromise = Promise.all(
|
||||
})
|
||||
);
|
||||
}
|
||||
evaluatePluginEntry(pluginName);
|
||||
if (!enabledPluginSet.has(pluginName)) {
|
||||
logger.debug(`plugin ${pluginName} is not enabled`);
|
||||
} else {
|
||||
logger.debug(`plugin ${pluginName} is enabled`);
|
||||
evaluatePluginEntry(pluginName);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
||||
import {
|
||||
migrateLocalBlobStorage,
|
||||
upgradeV1ToV2,
|
||||
@@ -36,15 +37,15 @@ async function tryMigration() {
|
||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
||||
assertExists(adapter);
|
||||
const upgrade = async () => {
|
||||
if (oldMeta.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
console.warn('not supported');
|
||||
return;
|
||||
}
|
||||
const workspace = await adapter.CRUD.get(oldMeta.id);
|
||||
if (!workspace) {
|
||||
console.warn('cannot find workspace', oldMeta.id);
|
||||
return;
|
||||
}
|
||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
console.warn('not supported');
|
||||
return;
|
||||
}
|
||||
const doc = workspace.blockSuiteWorkspace.doc;
|
||||
const provider = createIndexedDBDownloadProvider(
|
||||
workspace.id,
|
||||
@@ -62,7 +63,10 @@ async function tryMigration() {
|
||||
return;
|
||||
}
|
||||
const newWorkspace = upgradeV1ToV2(workspace);
|
||||
await migrateDatabaseBlockTo3(newWorkspace.blockSuiteWorkspace.doc);
|
||||
await migrateDatabaseBlockTo3(
|
||||
newWorkspace.blockSuiteWorkspace.doc,
|
||||
globalBlockSuiteSchema
|
||||
);
|
||||
|
||||
const newId = await adapter.CRUD.create(
|
||||
newWorkspace.blockSuiteWorkspace
|
||||
@@ -83,11 +87,14 @@ async function tryMigration() {
|
||||
// create a new workspace and push it to metadata
|
||||
promises.push(upgrade());
|
||||
} else if (oldMeta.version < WorkspaceVersion.DatabaseV3) {
|
||||
console.log('migrate to v3');
|
||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
||||
assertExists(adapter);
|
||||
promises.push(
|
||||
(async () => {
|
||||
if (oldMeta.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
console.warn('not supported');
|
||||
return;
|
||||
}
|
||||
const workspace = await adapter.CRUD.get(oldMeta.id);
|
||||
if (workspace) {
|
||||
const provider = createIndexedDBDownloadProvider(
|
||||
@@ -101,17 +108,18 @@ async function tryMigration() {
|
||||
provider.sync();
|
||||
await provider.whenReady;
|
||||
await migrateDatabaseBlockTo3(
|
||||
workspace.blockSuiteWorkspace.doc
|
||||
workspace.blockSuiteWorkspace.doc,
|
||||
globalBlockSuiteSchema
|
||||
);
|
||||
}
|
||||
const index = newMetadata.findIndex(
|
||||
meta => meta.id === oldMeta.id
|
||||
);
|
||||
console.log('migrate to v3');
|
||||
newMetadata[index] = {
|
||||
...oldMeta,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
};
|
||||
console.log('migrate to v3');
|
||||
})()
|
||||
);
|
||||
}
|
||||
@@ -121,8 +129,8 @@ async function tryMigration() {
|
||||
.then(() => {
|
||||
console.log('migration done');
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('migration failed');
|
||||
.catch(e => {
|
||||
console.error('migration failed', e);
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
|
||||
|
||||
@@ -3,7 +3,6 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
|
||||
@@ -15,7 +14,7 @@ const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
|
||||
const page = blockSuiteWorkspace.createPage({ id: 'page0' });
|
||||
|
||||
const Editor: React.FC = () => {
|
||||
const Editor = () => {
|
||||
const onLoad = useCallback((page: Page, editor: EditorContainer) => {
|
||||
// @ts-expect-error
|
||||
globalThis.page = page;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
@@ -10,6 +9,11 @@ import {
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { HelpIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type {
|
||||
LoadDBFileResult,
|
||||
SelectDBFileLocationResult,
|
||||
} from '@toeverything/infra/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
@@ -134,12 +138,12 @@ const SetDBLocationContent = ({
|
||||
}
|
||||
setOpening(true);
|
||||
(async function () {
|
||||
const result = await window.apis?.dialog.selectDBFileLocation();
|
||||
const result: SelectDBFileLocationResult =
|
||||
await window.apis?.dialog.selectDBFileLocation();
|
||||
setOpening(false);
|
||||
if (result?.filePath) {
|
||||
onConfirmLocation(result.filePath);
|
||||
} else if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic so the type is unknown
|
||||
toast(t[result.error]());
|
||||
}
|
||||
})().catch(err => {
|
||||
@@ -267,13 +271,12 @@ export const CreateWorkspaceModal = ({
|
||||
}
|
||||
logger.info('load db file');
|
||||
setStep(undefined);
|
||||
const result = await window.apis.dialog.loadDBFile();
|
||||
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
|
||||
if (result.workspaceId && !canceled) {
|
||||
setAddedId(result.workspaceId);
|
||||
setStep('set-syncing-mode');
|
||||
} else if (result.error || result.canceled) {
|
||||
if (result.error) {
|
||||
// @ts-expect-error: result.error is dynamic so the type is unknown
|
||||
toast(t[result.error]());
|
||||
}
|
||||
onClose();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IconButton, Modal, ModalWrapper } from '@affine/component';
|
||||
import { Modal, ModalWrapper, Wrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
|
||||
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
|
||||
import { Content, ContentTitle, Header, StyleTips } from './style';
|
||||
|
||||
interface EnableAffineCloudModalProps {
|
||||
open: boolean;
|
||||
@@ -11,11 +11,11 @@ interface EnableAffineCloudModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
|
||||
export const EnableAffineCloudModal = ({
|
||||
onConfirm,
|
||||
open,
|
||||
onClose,
|
||||
}) => {
|
||||
}: EnableAffineCloudModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
@@ -30,19 +30,22 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
|
||||
<ContentTitle>{t['Enable AFFiNE Cloud']()}?</ContentTitle>
|
||||
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
|
||||
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
|
||||
<div>
|
||||
<StyleButton
|
||||
<Wrapper width={284} margin="auto">
|
||||
<Button
|
||||
data-testid="confirm-enable-affine-cloud-button"
|
||||
shape="round"
|
||||
type="primary"
|
||||
block
|
||||
onClick={onConfirm}
|
||||
style={{
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{t['Sign in and Enable']()}
|
||||
</StyleButton>
|
||||
<StyleButton shape="round" onClick={onClose}>
|
||||
</Button>
|
||||
<Button onClick={onClose} block>
|
||||
{t['Not now']()}
|
||||
</StyleButton>
|
||||
</div>
|
||||
</Button>
|
||||
</Wrapper>
|
||||
</Content>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, styled } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
|
||||
export const Header = styled('div')({
|
||||
height: '44px',
|
||||
@@ -29,12 +29,3 @@ export const StyleTips = styled('div')(() => {
|
||||
marginTop: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleButton = styled(Button)(() => {
|
||||
return {
|
||||
width: '284px',
|
||||
display: 'block',
|
||||
margin: 'auto',
|
||||
marginTop: '16px',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import {
|
||||
type ButtonProps,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
styled,
|
||||
} from '@affine/component';
|
||||
import { Menu, MenuItem, MenuTrigger, styled } from '@affine/component';
|
||||
import { LOCALES } from '@affine/i18n';
|
||||
import { useI18N } from '@affine/i18n';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import type { ButtonProps } from '@toeverything/components/button';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const StyledListItem = styled(MenuItem)(() => ({
|
||||
@@ -16,9 +11,11 @@ export const StyledListItem = styled(MenuItem)(() => ({
|
||||
textTransform: 'capitalize',
|
||||
}));
|
||||
|
||||
const LanguageMenuContent: FC<{
|
||||
interface LanguageMenuContentProps {
|
||||
currentLanguage?: string;
|
||||
}> = ({ currentLanguage }) => {
|
||||
}
|
||||
|
||||
const LanguageMenuContent = ({ currentLanguage }: LanguageMenuContentProps) => {
|
||||
const i18n = useI18N();
|
||||
const changeLanguage = useCallback(
|
||||
(event: string) => {
|
||||
@@ -26,6 +23,7 @@ const LanguageMenuContent: FC<{
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{LOCALES.map(option => {
|
||||
@@ -47,9 +45,12 @@ const LanguageMenuContent: FC<{
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const LanguageMenu: FC<{ triggerProps?: ButtonProps }> = ({
|
||||
triggerProps,
|
||||
}) => {
|
||||
|
||||
interface LanguageMenuProps {
|
||||
triggerProps?: ButtonProps;
|
||||
}
|
||||
|
||||
export const LanguageMenu = ({ triggerProps }: LanguageMenuProps) => {
|
||||
const i18n = useI18N();
|
||||
|
||||
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
|
||||
import { Input, Modal, ModalCloseButton } from '@affine/component';
|
||||
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 { useCallback, useState } from 'react';
|
||||
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import type { WorkspaceSettingDetailProps } from '../index';
|
||||
import { WorkspaceDeleteModal } from './delete';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
|
||||
export const DeleteLeaveWorkspace: FC<{
|
||||
interface DeleteLeaveWorkspaceProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
|
||||
}> = ({ workspace, onDeleteWorkspace }) => {
|
||||
}
|
||||
|
||||
export const DeleteLeaveWorkspace = ({
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
}: DeleteLeaveWorkspaceProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
// fixme: cloud regression
|
||||
const isOwner = true;
|
||||
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [showLeave, setShowLeave] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { ModalCloseButton } from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
import {
|
||||
StyledButtonContent,
|
||||
@@ -38,7 +38,6 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
|
||||
<Button
|
||||
onClick={handleLeave}
|
||||
type="error"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t['Leave']()}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Button, toast } from '@affine/component';
|
||||
import { toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { type FC, useCallback } from 'react';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
|
||||
@@ -27,21 +29,25 @@ async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
}
|
||||
}
|
||||
|
||||
export const ExportPanel: FC<{
|
||||
interface ExportPanelProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}> = ({ workspace }) => {
|
||||
}
|
||||
|
||||
export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
||||
const workspaceId = workspace.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const onExport = useCallback(async () => {
|
||||
await syncBlobsToSqliteDb(workspace);
|
||||
const result = await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||
const result: SaveDBFileResult = await window.apis?.dialog.saveDBFileAs(
|
||||
workspaceId
|
||||
);
|
||||
if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic
|
||||
toast(t[result.error]());
|
||||
} else if (!result?.canceled) {
|
||||
toast(t['Export success']());
|
||||
}
|
||||
}, [t, workspace, workspaceId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
@@ -18,7 +18,7 @@ import { ProfilePanel } from './profile';
|
||||
import { PublishPanel } from './publish';
|
||||
import { StoragePanel } from './storage';
|
||||
|
||||
export type WorkspaceSettingDetailProps = {
|
||||
export interface WorkspaceSettingDetailProps {
|
||||
workspaceId: string;
|
||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
||||
onTransferWorkspace: <
|
||||
@@ -29,13 +29,13 @@ export type WorkspaceSettingDetailProps = {
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
|
||||
export const WorkspaceSettingDetail = ({
|
||||
workspaceId,
|
||||
onDeleteWorkspace,
|
||||
...props
|
||||
}) => {
|
||||
}: WorkspaceSettingDetailProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IconButton, Input, toast } from '@affine/component';
|
||||
import { Input, toast } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DoneIcon } from '@blocksuite/icons';
|
||||
import { IconButton } 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 { type FC, useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { Upload } from '../../pure/file-upload';
|
||||
@@ -29,9 +30,11 @@ const CameraIcon = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ProfilePanel: FC<{
|
||||
interface ProfilePanelProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}> = ({ workspace }) => {
|
||||
}
|
||||
|
||||
export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, FlexWrapper, Switch, Tooltip } from '@affine/component';
|
||||
import { FlexWrapper, Switch, Tooltip } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
@@ -18,26 +18,20 @@ import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||
import type { WorkspaceSettingDetailProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export type PublishPanelProps = Omit<
|
||||
WorkspaceSettingDetailProps,
|
||||
'workspaceId'
|
||||
> & {
|
||||
export interface PublishPanelProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
};
|
||||
export type PublishPanelLocalProps = Omit<
|
||||
WorkspaceSettingDetailProps,
|
||||
'workspaceId'
|
||||
> & {
|
||||
}
|
||||
export interface PublishPanelLocalProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: LocalWorkspace;
|
||||
};
|
||||
export type PublishPanelAffineProps = Omit<
|
||||
WorkspaceSettingDetailProps,
|
||||
'workspaceId'
|
||||
> & {
|
||||
}
|
||||
export interface PublishPanelAffineProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: AffineCloudWorkspace;
|
||||
};
|
||||
}
|
||||
|
||||
const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||
const { workspace } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
||||
@@ -57,6 +51,7 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
toast(t['Copied link to clipboard']());
|
||||
}, [shareUrl, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
@@ -90,10 +85,13 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const FakePublishPanelAffine: FC<{
|
||||
interface FakePublishPanelAffineProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}> = () => {
|
||||
}
|
||||
|
||||
const FakePublishPanelAffine = (_props: FakePublishPanelAffineProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={t['com.affine.settings.workspace.publish.local-tooltip']()}
|
||||
@@ -107,10 +105,11 @@ const FakePublishPanelAffine: FC<{
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
|
||||
|
||||
const PublishPanelLocal = ({
|
||||
workspace,
|
||||
onTransferWorkspace,
|
||||
}) => {
|
||||
}: PublishPanelLocalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
|
||||
@@ -166,7 +165,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const PublishPanel: FC<PublishPanelProps> = props => {
|
||||
export const PublishPanel = (props: PublishPanelProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Button, FlexWrapper, toast, Tooltip } from '@affine/component';
|
||||
import { FlexWrapper, toast, Tooltip } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { MoveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useMemo } from 'react';
|
||||
import { type FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import * as style from './style.css';
|
||||
@@ -30,9 +32,11 @@ const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
export const StoragePanel: FC<{
|
||||
interface StoragePanelProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}> = ({ workspace }) => {
|
||||
}
|
||||
|
||||
export const StoragePanel = ({ workspace }: StoragePanelProps) => {
|
||||
const workspaceId = workspace.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const secondaryPath = useDBFileSecondaryPath(workspaceId);
|
||||
@@ -51,11 +55,10 @@ export const StoragePanel: FC<{
|
||||
setMoveToInProgress(true);
|
||||
window.apis?.dialog
|
||||
.moveDBFile(workspaceId)
|
||||
.then(result => {
|
||||
.then((result: MoveDBFileResult) => {
|
||||
if (!result?.error && !result?.canceled) {
|
||||
toast(t['Move folder success']());
|
||||
} else if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic
|
||||
toast(t[result.error]());
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { TourModal } from '@affine/component/tour-modal';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import { openOnboardingModalAtom } from '../../atoms';
|
||||
import { guideOnboardingAtom } from '../../atoms/guide';
|
||||
|
||||
export const OnboardingModal: FC = memo(function OnboardingModal() {
|
||||
export const OnboardingModal = memo(function OnboardingModal() {
|
||||
const [open, setOpen] = useAtom(openOnboardingModalAtom);
|
||||
const [guideOpen, setShowOnboarding] = useAtom(guideOnboardingAtom);
|
||||
const onCloseTourModal = useCallback(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
|
||||
import dayjs from 'dayjs';
|
||||
import { type FC, useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
dateFormatOptions,
|
||||
@@ -8,10 +8,15 @@ import {
|
||||
useAppSetting,
|
||||
} from '../../../../../atoms/settings';
|
||||
|
||||
const DateFormatMenuContent: FC<{
|
||||
interface DateFormatMenuContentProps {
|
||||
currentOption: DateFormats;
|
||||
onSelect: (option: DateFormats) => void;
|
||||
}> = ({ onSelect, currentOption }) => {
|
||||
}
|
||||
|
||||
const DateFormatMenuContent = ({
|
||||
onSelect,
|
||||
currentOption,
|
||||
}: DateFormatMenuContentProps) => {
|
||||
return (
|
||||
<>
|
||||
{dateFormatOptions.map(option => {
|
||||
@@ -30,6 +35,7 @@ const DateFormatMenuContent: FC<{
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DateFormatSetting = () => {
|
||||
const [appearanceSettings, setAppSettings] = useAppSetting();
|
||||
const handleSelect = useCallback(
|
||||
@@ -38,6 +44,7 @@ export const DateFormatSetting = () => {
|
||||
},
|
||||
[setAppSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
content={
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
AiIcon,
|
||||
AppearanceIcon,
|
||||
InformationIcon,
|
||||
KeyboardIcon,
|
||||
PluginIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { FC, SVGProps } from 'react';
|
||||
import type { ReactElement, SVGProps } from 'react';
|
||||
|
||||
import { AboutAffine } from './about';
|
||||
import { AppearanceSettings } from './appearance';
|
||||
@@ -18,15 +18,18 @@ export type GeneralSettingKeys =
|
||||
| 'plugins'
|
||||
| 'about';
|
||||
|
||||
export type GeneralSettingList = {
|
||||
interface GeneralSettingListItem {
|
||||
key: GeneralSettingKeys;
|
||||
title: string;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
icon: (props: SVGProps<SVGSVGElement>) => ReactElement;
|
||||
testId: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type GeneralSettingList = GeneralSettingListItem[];
|
||||
|
||||
export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'appearance',
|
||||
@@ -43,7 +46,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
{
|
||||
key: 'plugins',
|
||||
title: 'Plugins',
|
||||
icon: AiIcon,
|
||||
icon: PluginIcon,
|
||||
testId: 'plugins-panel-trigger',
|
||||
},
|
||||
{
|
||||
@@ -55,11 +58,11 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
||||
];
|
||||
};
|
||||
|
||||
export const GeneralSetting = ({
|
||||
generalKey,
|
||||
}: {
|
||||
interface GeneralSettingProps {
|
||||
generalKey: GeneralSettingKeys;
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const GeneralSetting = ({ generalKey }: GeneralSettingProps) => {
|
||||
switch (generalKey) {
|
||||
case 'shortcuts':
|
||||
return <Shortcuts />;
|
||||
|
||||
@@ -1,55 +1,96 @@
|
||||
import { Switch } from '@affine/component';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { CallbackMap } from '@affine/sdk/entry';
|
||||
import {
|
||||
registeredPluginAtom,
|
||||
settingItemsAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { useRef } from 'react';
|
||||
addCleanup,
|
||||
enabledPluginAtom,
|
||||
pluginPackageJson,
|
||||
pluginSettingAtom,
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
import { loadedPluginNameAtom } from '@toeverything/infra/atom';
|
||||
import type { packageJsonOutputSchema } from '@toeverything/infra/type';
|
||||
import { useAtom, useAtomValue } from 'jotai/react';
|
||||
import { startTransition, useCallback, useMemo } from 'react';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { pluginItem } from './style.css';
|
||||
import { pluginItemStyle } from './style.css';
|
||||
|
||||
const PluginSettingWrapper: FC<{
|
||||
id: string;
|
||||
title?: ReactNode;
|
||||
}> = ({ title, id }) => {
|
||||
const Setting = useAtomValue(settingItemsAtom)[id];
|
||||
const disposeRef = useRef<(() => void) | null>(null);
|
||||
type PluginItemProps = {
|
||||
json: z.infer<typeof packageJsonOutputSchema>;
|
||||
};
|
||||
|
||||
type PluginSettingDetailProps = {
|
||||
pluginName: string;
|
||||
create: CallbackMap['setting'];
|
||||
};
|
||||
|
||||
const PluginSettingDetail = ({
|
||||
pluginName,
|
||||
create,
|
||||
}: PluginSettingDetailProps) => {
|
||||
return (
|
||||
<div>
|
||||
{title ? <div className="title">{title}</div> : null}
|
||||
<div
|
||||
ref={ref => {
|
||||
if (ref && Setting) {
|
||||
setTimeout(() => {
|
||||
disposeRef.current = Setting(ref);
|
||||
});
|
||||
} else if (ref === null) {
|
||||
setTimeout(() => {
|
||||
disposeRef.current?.();
|
||||
});
|
||||
<div
|
||||
ref={useCallback(
|
||||
(ref: HTMLDivElement | null) => {
|
||||
if (ref) {
|
||||
const cleanup = create(ref);
|
||||
addCleanup(pluginName, cleanup);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
},
|
||||
[pluginName, create]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const PluginItem = ({ json }: PluginItemProps) => {
|
||||
const [plugins, setEnabledPlugins] = useAtom(enabledPluginAtom);
|
||||
const checked = useMemo(
|
||||
() => plugins.includes(json.name),
|
||||
[json.name, plugins]
|
||||
);
|
||||
const create = useAtomValue(pluginSettingAtom)[json.name];
|
||||
return (
|
||||
<div className={pluginItemStyle} key={json.name}>
|
||||
<div>
|
||||
{json.name}
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={useCallback(
|
||||
(checked: boolean) => {
|
||||
startTransition(() => {
|
||||
setEnabledPlugins(plugins => {
|
||||
if (checked) {
|
||||
return [...plugins, json.name];
|
||||
} else {
|
||||
return plugins.filter(plugin => plugin !== json.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
[json.name, setEnabledPlugins]
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>{json.description}</div>
|
||||
{create && <PluginSettingDetail pluginName={json.name} create={create} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Plugins = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
||||
const loadedPlugins = useAtomValue(loadedPluginNameAtom);
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={'Plugins'}
|
||||
subtitle={allowedPlugins.length === 0 && t['None yet']()}
|
||||
subtitle={loadedPlugins.length === 0 && t['None yet']()}
|
||||
data-testid="plugins-title"
|
||||
/>
|
||||
{allowedPlugins.map(plugin => (
|
||||
<div className={pluginItem} key={plugin}>
|
||||
<PluginSettingWrapper key={plugin} id={plugin} title={plugin} />
|
||||
</div>
|
||||
{useAtomValue(pluginPackageJson).map(json => (
|
||||
<PluginItem json={json} key={json.name} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingWrapper = style({
|
||||
export const settingWrapperStyle = style({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
@@ -8,7 +8,7 @@ export const settingWrapper = style({
|
||||
maxWidth: '250px',
|
||||
});
|
||||
|
||||
export const pluginItem = style({
|
||||
export const pluginItemStyle = style({
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
transition: '0.3s',
|
||||
padding: '24px 8px',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import {
|
||||
SettingModal as SettingModalBase,
|
||||
type SettingModalProps,
|
||||
type SettingModalProps as SettingModalBaseProps,
|
||||
} from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { AccountSetting } from './account-setting';
|
||||
@@ -18,21 +17,25 @@ import { settingContent } from './style.css';
|
||||
import { WorkspaceSetting } from './workspace-setting';
|
||||
|
||||
type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
|
||||
export type SettingProps = {
|
||||
|
||||
export interface SettingProps {
|
||||
activeTab: ActiveTab;
|
||||
workspaceId: string | null;
|
||||
onSettingClick: (params: {
|
||||
activeTab: ActiveTab;
|
||||
workspaceId: string | null;
|
||||
}) => void;
|
||||
};
|
||||
export const SettingModal: React.FC<SettingModalProps & SettingProps> = ({
|
||||
}
|
||||
|
||||
type SettingModalProps = SettingModalBaseProps & SettingProps;
|
||||
|
||||
export const SettingModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
activeTab = 'appearance',
|
||||
workspaceId = null,
|
||||
onSettingClick,
|
||||
}) => {
|
||||
}: SettingModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const generalSettingList = useGeneralSettingList();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { ScrollableContainer, Tooltip } from '@affine/component';
|
||||
import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
@@ -11,7 +11,6 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
@@ -28,21 +27,24 @@ import {
|
||||
sidebarTitle,
|
||||
} from './style.css';
|
||||
|
||||
export const SettingSidebar: FC<{
|
||||
interface SettingSidebarProps {
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
selectedGeneralKey: string | null;
|
||||
onAccountSettingClick: () => void;
|
||||
}> = ({
|
||||
}
|
||||
|
||||
export const SettingSidebar = ({
|
||||
generalSettingList,
|
||||
onGeneralSettingClick,
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
selectedGeneralKey,
|
||||
}) => {
|
||||
}: SettingSidebarProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div className={settingSlideBar} data-testid="settings-sidebar">
|
||||
<div className={sidebarTitle}>{t['Settings']()}</div>
|
||||
@@ -76,20 +78,27 @@ export const SettingSidebar: FC<{
|
||||
</div>
|
||||
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
||||
<Suspense fallback={<WorkspaceListSkeleton />}>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
<ScrollableContainer>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceList: FC<{
|
||||
interface WorkspaceListProps {
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}> = ({ onWorkspaceSettingClick, selectedWorkspaceId }) => {
|
||||
}
|
||||
|
||||
export const WorkspaceList = ({
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
}: WorkspaceListProps) => {
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
return (
|
||||
@@ -112,19 +121,22 @@ export const WorkspaceList: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceListItemProps {
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const WorkspaceListItem = ({
|
||||
meta,
|
||||
onClick,
|
||||
isCurrent,
|
||||
isActive,
|
||||
}: {
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
}: WorkspaceListItemProps) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(sidebarSelectItem, { active: isActive })}
|
||||
|
||||
@@ -4,7 +4,7 @@ export const settingSlideBar = style({
|
||||
width: '25%',
|
||||
maxWidth: '242px',
|
||||
background: 'var(--affine-background-secondary-color)',
|
||||
padding: '20px 16px',
|
||||
padding: '20px 0px',
|
||||
height: '100%',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
@@ -15,14 +15,14 @@ export const sidebarTitle = style({
|
||||
fontSize: 'var(--affine-font-h-6)',
|
||||
fontWeight: '600',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
paddingLeft: '8px',
|
||||
padding: '0px 16px 0px 24px',
|
||||
});
|
||||
|
||||
export const sidebarSubtitle = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
paddingLeft: '8px',
|
||||
padding: '0px 16px 0px 24px',
|
||||
marginTop: '20px',
|
||||
marginBottom: '4px',
|
||||
display: 'flex',
|
||||
@@ -34,7 +34,7 @@ export const sidebarItemsWrapper = style({
|
||||
selectors: {
|
||||
'&.scroll': {
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
overflowY: 'hidden',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -42,9 +42,9 @@ export const sidebarItemsWrapper = style({
|
||||
export const sidebarSelectItem = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
margin: '0px 16px 4px 16px',
|
||||
padding: '0px 8px',
|
||||
height: '30px',
|
||||
marginBottom: '4px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -32,11 +32,12 @@ globalStyle(`${settingContent} .footer`, {
|
||||
|
||||
globalStyle(`${settingContent} .footer a`, {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
lineHeight: 'normal',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .footer > svg`, {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginRight: '12px',
|
||||
marginTop: '2px',
|
||||
marginTop: '1px',
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Empty, IconButton, Modal, ModalWrapper } from '@affine/component';
|
||||
import { Empty, Modal, ModalWrapper } from '@affine/component';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
|
||||
import {
|
||||
Content,
|
||||
@@ -19,10 +19,12 @@ interface TmpDisableAffineCloudModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const TmpDisableAffineCloudModal: React.FC<
|
||||
TmpDisableAffineCloudModalProps
|
||||
> = ({ open, onClose }) => {
|
||||
export const TmpDisableAffineCloudModal = ({
|
||||
open,
|
||||
onClose,
|
||||
}: TmpDisableAffineCloudModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
data-testid="disable-affine-cloud-modal"
|
||||
@@ -66,7 +68,7 @@ export const TmpDisableAffineCloudModal: React.FC<
|
||||
/>
|
||||
</StyleImage>
|
||||
<StyleButtonContainer>
|
||||
<StyleButton shape="round" type="primary" onClick={onClose}>
|
||||
<StyleButton type="primary" onClick={onClose}>
|
||||
{t['Got it']()}
|
||||
</StyleButton>
|
||||
</StyleButtonContainer>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, displayFlex, styled } from '@affine/component';
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
export const Header = styled('div')({
|
||||
height: '44px',
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { IconButton, Modal, ModalWrapper } from '@affine/component';
|
||||
import { Modal, ModalWrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
|
||||
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
|
||||
|
||||
export type TransformWorkspaceToAffineModalProps = {
|
||||
export interface TransformWorkspaceToAffineModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConform: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const TransformWorkspaceToAffineModal: React.FC<
|
||||
TransformWorkspaceToAffineModalProps
|
||||
> = ({ open, onClose, onConform }) => {
|
||||
export const TransformWorkspaceToAffineModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onConform,
|
||||
}: TransformWorkspaceToAffineModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
@@ -35,7 +37,6 @@ export const TransformWorkspaceToAffineModal: React.FC<
|
||||
<div>
|
||||
<StyleButton
|
||||
data-testid="confirm-enable-cloud-button"
|
||||
shape="round"
|
||||
type="primary"
|
||||
onClick={onConform}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, styled } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
export const Header = styled('div')({
|
||||
height: '44px',
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
type FocusEvent,
|
||||
type InputHTMLAttributes,
|
||||
type KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
||||
import { PageMenu } from './operation-menu';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface BlockSuiteHeaderTitleProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
const EditableTitle = ({
|
||||
value,
|
||||
onFocus: propsOnFocus,
|
||||
...inputProps
|
||||
}: InputHTMLAttributes<HTMLInputElement>) => {
|
||||
const onFocus = useCallback(
|
||||
(e: FocusEvent<HTMLInputElement>) => {
|
||||
e.target.select();
|
||||
propsOnFocus?.(e);
|
||||
},
|
||||
[propsOnFocus]
|
||||
);
|
||||
return (
|
||||
<div className={styles.headerTitleContainer}>
|
||||
<input
|
||||
className={styles.titleInput}
|
||||
autoFocus={true}
|
||||
value={value}
|
||||
type="text"
|
||||
data-testid="title-content"
|
||||
onFocus={onFocus}
|
||||
{...inputProps}
|
||||
/>
|
||||
<span className={styles.shadowTitle}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StableTitle = ({
|
||||
workspace,
|
||||
pageId,
|
||||
onRename,
|
||||
}: BlockSuiteHeaderTitleProps & {
|
||||
onRename?: () => void;
|
||||
}) => {
|
||||
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
|
||||
const title = pageMeta?.title;
|
||||
|
||||
return (
|
||||
<div className={styles.headerTitleContainer}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={pageId}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
data-testid="title-edit-button"
|
||||
className={styles.titleEditButton}
|
||||
onClick={onRename}
|
||||
>
|
||||
{title || 'Untitled'}
|
||||
</span>
|
||||
<PageMenu rename={onRename} pageId={pageId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
||||
const { workspace, pageId } = props;
|
||||
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled');
|
||||
|
||||
const onRename = useCallback(() => {
|
||||
setIsEditable(true);
|
||||
}, []);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
setIsEditable(false);
|
||||
if (!currentPage?.id) {
|
||||
return;
|
||||
}
|
||||
pageTitleMeta.setPageTitle(currentPage.id, title);
|
||||
}, [currentPage?.id, pageTitleMeta, title]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' || e.key === 'Escape') {
|
||||
onBlur();
|
||||
}
|
||||
},
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPageTitle(pageMeta?.title || '');
|
||||
}, [pageMeta?.title]);
|
||||
|
||||
if (isEditable) {
|
||||
return (
|
||||
<EditableTitle
|
||||
onBlur={onBlur}
|
||||
value={title}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
setPageTitle(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <StableTitle {...props} onRename={onRename} />;
|
||||
};
|
||||
|
||||
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.PUBLIC) {
|
||||
return <StableTitle {...props} />;
|
||||
}
|
||||
return <BlockSuiteTitleWithRename {...props} />;
|
||||
};
|
||||
|
||||
BlockSuiteHeaderTitle.displayName = 'BlockSuiteHeaderTitle';
|
||||
@@ -0,0 +1,204 @@
|
||||
import { FlexWrapper, Menu, MenuItem } from '@affine/component';
|
||||
import { Export, MoveToTrash } from '@affine/component/page-list';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
DuplicateIcon,
|
||||
EdgelessIcon,
|
||||
EditIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
ImportIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { pageSettingFamily, setPageModeAtom } from '../../../atoms';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { toast } from '../../../utils';
|
||||
import { HeaderDropDownButton } from '../../pure/header-drop-down-button';
|
||||
import { usePageHelper } from '../block-suite-page-list/utils';
|
||||
|
||||
type PageMenuProps = {
|
||||
rename?: () => void;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
// fixme(himself65): remove these hooks ASAP
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
meta => meta.id === pageId
|
||||
) as PageMeta;
|
||||
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
|
||||
const mode = setting?.mode ?? 'page';
|
||||
|
||||
const favorite = pageMeta.favorite ?? false;
|
||||
const { setPageMeta, setPageTitle } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const [openConfirm, setOpenConfirm] = useState(false);
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const { importFile } = usePageHelper(blockSuiteWorkspace);
|
||||
const handleFavorite = useCallback(() => {
|
||||
setPageMeta(pageId, { favorite: !favorite });
|
||||
toast(favorite ? t['Removed from Favorites']() : t['Added to Favorites']());
|
||||
}, [favorite, pageId, setPageMeta, t]);
|
||||
const handleSwitchMode = useCallback(() => {
|
||||
setSetting(setting => ({
|
||||
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
|
||||
}));
|
||||
toast(
|
||||
mode === 'page'
|
||||
? t['com.affine.edgelessMode']()
|
||||
: t['com.affine.pageMode']()
|
||||
);
|
||||
}, [mode, setSetting, t]);
|
||||
const handleOnConfirm = useCallback(() => {
|
||||
removeToTrash(pageId);
|
||||
toast(t['Moved to Trash']());
|
||||
setOpenConfirm(false);
|
||||
}, [pageId, removeToTrash, t]);
|
||||
const menuItemStyle = {
|
||||
padding: '4px 12px',
|
||||
};
|
||||
const { openPage } = useNavigateHelper();
|
||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const setPageMode = useSetAtom(setPageModeAtom);
|
||||
const duplicate = useCallback(async () => {
|
||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(currentPage);
|
||||
const currentPageMeta = currentPage.meta;
|
||||
const newPage = createPage();
|
||||
await newPage.waitForLoaded();
|
||||
const update = encodeStateAsUpdate(currentPage.spaceDoc);
|
||||
applyUpdate(newPage.spaceDoc, update);
|
||||
setPageMeta(newPage.id, {
|
||||
tags: currentPageMeta.tags,
|
||||
favorite: currentPageMeta.favorite,
|
||||
});
|
||||
setPageMode(newPage.id, mode);
|
||||
setPageTitle(newPage.id, `${currentPageMeta.title}(1)`);
|
||||
openPage(blockSuiteWorkspace.id, newPage.id);
|
||||
}, [
|
||||
blockSuiteWorkspace,
|
||||
createPage,
|
||||
mode,
|
||||
openPage,
|
||||
pageId,
|
||||
setPageMeta,
|
||||
setPageMode,
|
||||
setPageTitle,
|
||||
]);
|
||||
const EditMenu = (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={<EditIcon />}
|
||||
data-testid="editor-option-menu-rename"
|
||||
onClick={rename}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Rename']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onClick={handleSwitchMode}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Convert to ']()}
|
||||
{mode === 'page' ? t['Edgeless']() : t['Page']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onClick={handleFavorite}
|
||||
style={menuItemStyle}
|
||||
icon={
|
||||
favorite ? (
|
||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
|
||||
</MenuItem>
|
||||
{/* {TODO: add tag and duplicate function support} */}
|
||||
{/* <MenuItem
|
||||
icon={<TagsIcon />}
|
||||
data-testid="editor-option-menu-add-tag"
|
||||
onClick={() => {}}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['com.affine.header.option.add-tag']()}
|
||||
</MenuItem> */}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
icon={<DuplicateIcon />}
|
||||
data-testid="editor-option-menu-duplicate"
|
||||
onClick={duplicate}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['com.affine.header.option.duplicate']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<ImportIcon />}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={importFile}
|
||||
style={menuItemStyle}
|
||||
>
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
<Export />
|
||||
<Divider />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onItemClick={() => {
|
||||
setOpenConfirm(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
content={EditMenu}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
menuStyles={{
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
background: 'var(--affine-background-overlay-panel-color)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<HeaderDropDownButton />
|
||||
</div>
|
||||
</Menu>
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={openConfirm}
|
||||
title={pageMeta.title}
|
||||
onConfirm={handleOnConfirm}
|
||||
onCancel={() => {
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
/>
|
||||
</FlexWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerTitleContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const titleEditButton = style({
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
|
||||
export const titleInput = style({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
export const shadowTitle = style({
|
||||
visibility: 'hidden',
|
||||
});
|
||||
@@ -6,9 +6,9 @@ import { useAtom } from 'jotai';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { pageSettingFamily } from '../../../../atoms';
|
||||
import type { BlockSuiteWorkspace } from '../../../../shared';
|
||||
import { toast } from '../../../../utils';
|
||||
import { pageSettingFamily } from '../../../atoms';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import { StyledEditorModeSwitch, StyledKeyboardItem } from './style';
|
||||
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-
|
||||
import { useBlockSuitePagePreview } from '@toeverything/hooks/use-block-suite-page-preview';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type React from 'react';
|
||||
import { Suspense, useCallback, useMemo } from 'react';
|
||||
|
||||
import { allPageModeSelectAtom } from '../../../atoms';
|
||||
@@ -23,13 +22,13 @@ import { filterPage } from '../../../utils/filter';
|
||||
import { emptyDescButton, emptyDescKbd, pageListEmptyStyle } from './index.css';
|
||||
import { usePageHelper } from './utils';
|
||||
|
||||
export type BlockSuitePageListProps = {
|
||||
export interface BlockSuitePageListProps {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
listType: 'all' | 'trash' | 'shared' | 'public';
|
||||
isPublic?: true;
|
||||
isPublic?: boolean;
|
||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||
collection?: Collection;
|
||||
};
|
||||
}
|
||||
|
||||
const filter = {
|
||||
all: (pageMeta: PageMeta) => !pageMeta.trash,
|
||||
@@ -41,13 +40,12 @@ const filter = {
|
||||
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
||||
};
|
||||
|
||||
const PagePreviewInner = ({
|
||||
workspace,
|
||||
pageId,
|
||||
}: {
|
||||
interface PagePreviewInnerProps {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
}) => {
|
||||
}
|
||||
|
||||
const PagePreviewInner = ({ workspace, pageId }: PagePreviewInnerProps) => {
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
assertExists(page);
|
||||
const previewAtom = useBlockSuitePagePreview(page);
|
||||
@@ -55,13 +53,12 @@ const PagePreviewInner = ({
|
||||
return preview;
|
||||
};
|
||||
|
||||
const PagePreview = ({
|
||||
workspace,
|
||||
pageId,
|
||||
}: {
|
||||
interface PagePreviewProps {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
}) => {
|
||||
}
|
||||
|
||||
const PagePreview = ({ workspace, pageId }: PagePreviewProps) => {
|
||||
return (
|
||||
<Suspense>
|
||||
<PagePreviewInner workspace={workspace} pageId={pageId} />
|
||||
@@ -69,10 +66,12 @@ const PagePreview = ({
|
||||
);
|
||||
};
|
||||
|
||||
const PageListEmpty = (props: {
|
||||
interface PageListEmptyProps {
|
||||
createPage?: ReturnType<typeof usePageHelper>['createPage'];
|
||||
listType: BlockSuitePageListProps['listType'];
|
||||
}) => {
|
||||
}
|
||||
|
||||
const PageListEmpty = (props: PageListEmptyProps) => {
|
||||
const { listType, createPage } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -124,13 +123,13 @@ const PageListEmpty = (props: {
|
||||
);
|
||||
};
|
||||
|
||||
export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
export const BlockSuitePageList = ({
|
||||
blockSuiteWorkspace,
|
||||
onOpenPage,
|
||||
listType,
|
||||
isPublic = false,
|
||||
collection,
|
||||
}) => {
|
||||
}: BlockSuitePageListProps) => {
|
||||
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
const {
|
||||
toggleFavorite,
|
||||
@@ -262,6 +261,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PageList
|
||||
workspaceId={blockSuiteWorkspace.id}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { DownloadTips } from '@affine/component/affine-banner';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
|
||||
export const DownloadClientTip = ({
|
||||
show,
|
||||
onClose,
|
||||
}: {
|
||||
// const [showDownloadClientTips, setShowDownloadClientTips] = useAtom(
|
||||
// guideDownloadClientTipAtom
|
||||
// );
|
||||
// const onCloseDownloadClient = useCallback(() => {
|
||||
// setShowDownloadClientTips(false);
|
||||
// }, [setShowDownloadClientTips]);
|
||||
|
||||
// if (!showDownloadClientTips || isDesktop) {
|
||||
// return <></>;
|
||||
// }
|
||||
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
if (!show || isDesktop) {
|
||||
return null;
|
||||
}
|
||||
return <DownloadTips onClose={onClose} />;
|
||||
};
|
||||
export default DownloadClientTip;
|
||||
@@ -1,154 +0,0 @@
|
||||
// fixme(himself65): refactor this file
|
||||
import { FlexWrapper, IconButton, Menu, MenuItem } from '@affine/component';
|
||||
import { Export, MoveToTrash } from '@affine/component/page-list';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
MoreVerticalIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { pageSettingFamily } from '../../../../atoms';
|
||||
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { toast } from '../../../../utils';
|
||||
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
|
||||
import { LanguageMenu } from './language-menu';
|
||||
const CommonMenu = () => {
|
||||
const content = (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MenuThemeModeSwitch />
|
||||
<LanguageMenu />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
content={content}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton data-testid="editor-option-menu">
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
const PageMenu = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
// fixme(himself65): remove these hooks ASAP
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const pageId = useAtomValue(currentPageIdAtom);
|
||||
assertExists(workspace);
|
||||
assertExists(pageId);
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
assertExists(pageMeta);
|
||||
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
|
||||
const mode = setting?.mode ?? 'page';
|
||||
|
||||
const favorite = pageMeta.favorite ?? false;
|
||||
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const [openConfirm, setOpenConfirm] = useState(false);
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const handleFavorite = useCallback(() => {
|
||||
setPageMeta(pageId, { favorite: !favorite });
|
||||
toast(favorite ? t['Removed from Favorites']() : t['Added to Favorites']());
|
||||
}, [favorite, pageId, setPageMeta, t]);
|
||||
const handleSwitchMode = useCallback(() => {
|
||||
setSetting(setting => ({
|
||||
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
|
||||
}));
|
||||
toast(
|
||||
mode === 'page'
|
||||
? t['com.affine.edgelessMode']()
|
||||
: t['com.affine.pageMode']()
|
||||
);
|
||||
}, [mode, setSetting, t]);
|
||||
const handleOnConfirm = useCallback(() => {
|
||||
removeToTrash(pageMeta.id);
|
||||
toast(t['Moved to Trash']());
|
||||
setOpenConfirm(false);
|
||||
}, [pageMeta.id, removeToTrash, t]);
|
||||
|
||||
const EditMenu = (
|
||||
<>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onClick={handleFavorite}
|
||||
icon={
|
||||
favorite ? (
|
||||
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
|
||||
) : (
|
||||
<FavoriteIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onClick={handleSwitchMode}
|
||||
>
|
||||
{t['Convert to ']()}
|
||||
{mode === 'page' ? t['Edgeless']() : t['Page']()}
|
||||
</MenuItem>
|
||||
<Export />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onItemClick={() => {
|
||||
setOpenConfirm(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper alignItems="center" justifyContent="center">
|
||||
<Menu
|
||||
content={EditMenu}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton data-testid="editor-option-menu">
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={openConfirm}
|
||||
title={pageMeta.title}
|
||||
onConfirm={handleOnConfirm}
|
||||
onCancel={() => {
|
||||
setOpenConfirm(false);
|
||||
}}
|
||||
/>
|
||||
</FlexWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const EditorOptionMenu = () => {
|
||||
const { pageId } = useParams();
|
||||
return pageId ? <PageMenu /> : <CommonMenu />;
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
import { Button, displayFlex, Menu, MenuItem, styled } from '@affine/component';
|
||||
import { LOCALES } from '@affine/i18n';
|
||||
import { useI18N } from '@affine/i18n';
|
||||
import { ArrowDownSmallIcon, LanguageIcon } from '@blocksuite/icons';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const LanguageMenuContent: FC = () => {
|
||||
const i18n = useI18N();
|
||||
const changeLanguage = useCallback(
|
||||
(event: string) => {
|
||||
i18n.changeLanguage(event).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{LOCALES.map(option => {
|
||||
return (
|
||||
<StyledListItem
|
||||
key={option.name}
|
||||
title={option.name}
|
||||
onClick={() => {
|
||||
changeLanguage(option.tag);
|
||||
}}
|
||||
>
|
||||
{option.originalName}
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const LanguageMenu: React.FC = () => {
|
||||
const i18n = useI18N();
|
||||
|
||||
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledIconContainer>
|
||||
<LanguageIcon />
|
||||
</StyledIconContainer>
|
||||
<StyledButtonContainer>
|
||||
<Menu
|
||||
content={(<LanguageMenuContent />) as ReactElement}
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
disablePortal={true}
|
||||
>
|
||||
<StyledButton
|
||||
type="plain"
|
||||
icon={
|
||||
<StyledArrowDownContainer>
|
||||
<ArrowDownSmallIcon />
|
||||
</StyledArrowDownContainer>
|
||||
}
|
||||
iconPosition="end"
|
||||
data-testid="language-menu-button"
|
||||
>
|
||||
<StyledCurrentLanguage>
|
||||
{currentLanguage?.originalName}
|
||||
</StyledCurrentLanguage>
|
||||
</StyledButton>
|
||||
</Menu>
|
||||
</StyledButtonContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledListItem = styled(MenuItem)(() => ({
|
||||
width: '132px',
|
||||
height: '38px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
textTransform: 'capitalize',
|
||||
}));
|
||||
|
||||
const StyledContainer = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '48px',
|
||||
backgroundColor: 'transparent',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
padding: '0 14px',
|
||||
};
|
||||
});
|
||||
const StyledIconContainer = styled('div')(() => {
|
||||
return {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '20px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
};
|
||||
});
|
||||
const StyledButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid var(--affine-border-color)`,
|
||||
backgroundColor: 'transparent',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
marginLeft: '12px',
|
||||
};
|
||||
});
|
||||
const StyledButton = styled(Button)(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
...displayFlex('space-between', 'center'),
|
||||
textTransform: 'capitalize',
|
||||
padding: '0',
|
||||
};
|
||||
});
|
||||
const StyledArrowDownContainer = styled('div')(() => {
|
||||
return {
|
||||
height: '32px',
|
||||
borderLeft: `1px solid var(--affine-border-color)`,
|
||||
backgroundColor: 'transparent',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
padding: '4px 6px',
|
||||
fontSize: '24px',
|
||||
};
|
||||
});
|
||||
const StyledCurrentLanguage = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '12px',
|
||||
color: 'var(--affine-text-color)',
|
||||
};
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { CSSProperties, DOMAttributes } from 'react';
|
||||
type IconProps = {
|
||||
style?: CSSProperties;
|
||||
} & DOMAttributes<SVGElement>;
|
||||
|
||||
export const MoonIcon = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.54893 3.31407C9.33328 3.08158 9.27962 2.74521 9.41255 2.45912C9.54547 2.17302 9.83936 1.99233 10.1595 1.99986C13.4456 2.07712 16.5114 4.08044 17.7359 7.29071C19.3437 11.5057 17.1672 16.2024 12.8744 17.781C9.60251 18.9843 6.04745 18.0285 3.82974 15.6428C3.61375 15.4104 3.55978 15.0739 3.69257 14.7876C3.82537 14.5014 4.11931 14.3205 4.43962 14.3279C5.27228 14.3474 6.12412 14.2171 6.94979 13.9135C10.415 12.6391 12.172 8.84782 10.8741 5.44537C10.5657 4.63692 10.1061 3.91474 9.54893 3.31407Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const SunIcon = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.8002 2.5002C10.8002 2.05837 10.442 1.7002 10.0002 1.7002C9.55837 1.7002 9.2002 2.05837 9.2002 2.5002V3.33353C9.2002 3.77536 9.55837 4.13353 10.0002 4.13353C10.442 4.13353 10.8002 3.77536 10.8002 3.33353V2.5002ZM5.14921 4.01784C4.83679 3.70542 4.33026 3.70542 4.01784 4.01784C3.70542 4.33026 3.70542 4.83679 4.01784 5.14921L4.69627 5.82764C5.00869 6.14006 5.51522 6.14006 5.82764 5.82764C6.14006 5.51522 6.14006 5.00869 5.82764 4.69627L5.14921 4.01784ZM15.9825 5.1492C16.2949 4.83678 16.2949 4.33025 15.9825 4.01783C15.6701 3.70542 15.1636 3.70543 14.8511 4.01785L14.1727 4.69628C13.8603 5.00871 13.8603 5.51524 14.1728 5.82765C14.4852 6.14007 14.9917 6.14006 15.3041 5.82763L15.9825 5.1492ZM10.0002 5.86686C7.71742 5.86686 5.86686 7.71742 5.86686 10.0002C5.86686 12.283 7.71742 14.1335 10.0002 14.1335C12.283 14.1335 14.1335 12.283 14.1335 10.0002C14.1335 7.71742 12.283 5.86686 10.0002 5.86686ZM2.5002 9.2002C2.05837 9.2002 1.7002 9.55837 1.7002 10.0002C1.7002 10.442 2.05837 10.8002 2.5002 10.8002H3.33353C3.77536 10.8002 4.13353 10.442 4.13353 10.0002C4.13353 9.55837 3.77536 9.2002 3.33353 9.2002H2.5002ZM16.6669 9.2002C16.225 9.2002 15.8669 9.55837 15.8669 10.0002C15.8669 10.442 16.225 10.8002 16.6669 10.8002H17.5002C17.942 10.8002 18.3002 10.442 18.3002 10.0002C18.3002 9.55837 17.942 9.2002 17.5002 9.2002H16.6669ZM5.82623 15.309C6.13943 14.9973 6.14069 14.4908 5.82906 14.1776C5.51742 13.8644 5.01089 13.8631 4.69769 14.1748L4.01926 14.8498C3.70606 15.1615 3.70479 15.668 4.01643 15.9812C4.32807 16.2944 4.8346 16.2956 5.1478 15.984L5.82623 15.309ZM15.3027 14.1748C14.9895 13.8631 14.483 13.8644 14.1713 14.1776C13.8597 14.4908 13.861 14.9973 14.1742 15.3089L14.8526 15.984C15.1658 16.2956 15.6723 16.2944 15.9839 15.9812C16.2956 15.668 16.2943 15.1615 15.9811 14.8498L15.3027 14.1748ZM10.8002 16.6669C10.8002 16.225 10.442 15.8669 10.0002 15.8669C9.55837 15.8669 9.2002 16.225 9.2002 16.6669V17.5002C9.2002 17.942 9.55837 18.3002 10.0002 18.3002C10.442 18.3002 10.8002 17.942 10.8002 17.5002V16.6669Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DarkModeIcon, LightModeIcon } from '@blocksuite/icons';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import {
|
||||
StyledSwitchItem,
|
||||
StyledThemeButton,
|
||||
StyledThemeButtonContainer,
|
||||
StyledThemeModeContainer,
|
||||
StyledThemeModeSwitch,
|
||||
StyledVerticalDivider,
|
||||
} from './style';
|
||||
|
||||
export const MenuThemeModeSwitch = () => {
|
||||
const { setTheme, resolvedTheme, theme } = useTheme();
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<StyledThemeModeContainer>
|
||||
<StyledThemeModeSwitch data-testid="change-theme-container" inMenu={true}>
|
||||
<StyledSwitchItem active={resolvedTheme === 'light'} inMenu={true}>
|
||||
<LightModeIcon />
|
||||
</StyledSwitchItem>
|
||||
<StyledSwitchItem active={resolvedTheme === 'dark'} inMenu={true}>
|
||||
<DarkModeIcon />
|
||||
</StyledSwitchItem>
|
||||
</StyledThemeModeSwitch>
|
||||
<StyledThemeButtonContainer>
|
||||
<StyledThemeButton
|
||||
data-testid="change-theme-light"
|
||||
active={theme === 'light'}
|
||||
onClick={() => {
|
||||
setTheme('light');
|
||||
}}
|
||||
>
|
||||
{t['light']()}
|
||||
</StyledThemeButton>
|
||||
<StyledVerticalDivider />
|
||||
<StyledThemeButton
|
||||
data-testid="change-theme-dark"
|
||||
active={theme === 'dark'}
|
||||
onClick={() => {
|
||||
setTheme('dark');
|
||||
}}
|
||||
>
|
||||
{t['dark']()}
|
||||
</StyledThemeButton>
|
||||
<StyledVerticalDivider />
|
||||
<StyledThemeButton
|
||||
active={theme === 'system'}
|
||||
onClick={() => {
|
||||
setTheme('system');
|
||||
}}
|
||||
>
|
||||
{t['system']()}
|
||||
</StyledThemeButton>
|
||||
</StyledThemeButtonContainer>
|
||||
</StyledThemeModeContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuThemeModeSwitch;
|
||||
@@ -1,120 +0,0 @@
|
||||
import { css, displayFlex, keyframes, styled } from '@affine/component';
|
||||
// @ts-expect-error: no types for css-spring
|
||||
import spring, { toString } from 'css-spring';
|
||||
|
||||
const ANIMATE_DURATION = 400;
|
||||
export const StyledThemeModeContainer = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '48px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '16px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
padding: '0 14px',
|
||||
};
|
||||
});
|
||||
export const StyledThemeButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
height: '32px',
|
||||
border: `1px solid var(--affine-border-color)`,
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
...displayFlex('space-evenly', 'center'),
|
||||
flexGrow: 1,
|
||||
marginLeft: '12px',
|
||||
};
|
||||
});
|
||||
export const StyledThemeButton = styled('button')<{
|
||||
active: boolean;
|
||||
}>(({ active }) => {
|
||||
return {
|
||||
padding: '0 8px',
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
cursor: 'pointer',
|
||||
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
export const StyledVerticalDivider = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '100%',
|
||||
borderLeft: `1px solid var(--affine-border-color)`,
|
||||
};
|
||||
});
|
||||
export const StyledThemeModeSwitch = styled('button')<{
|
||||
inMenu?: boolean;
|
||||
}>(({ inMenu }) => {
|
||||
return {
|
||||
width: inMenu ? '20px' : '32px',
|
||||
height: inMenu ? '20px' : '32px',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: inMenu ? '20px' : '24px',
|
||||
};
|
||||
});
|
||||
export const StyledSwitchItem = styled('div')<{
|
||||
active: boolean;
|
||||
isHover?: boolean;
|
||||
inMenu?: boolean;
|
||||
}>(({ active, isHover, inMenu }) => {
|
||||
const activeRaiseAnimate = toString(
|
||||
spring({ top: '0' }, { top: '-100%' }, { preset: 'gentle' })
|
||||
);
|
||||
const raiseAnimate = toString(
|
||||
spring({ top: '100%' }, { top: '0' }, { preset: 'gentle' })
|
||||
);
|
||||
const activeDeclineAnimate = toString(
|
||||
spring({ top: '-100%' }, { top: '0' }, { preset: 'gentle' })
|
||||
);
|
||||
const declineAnimate = toString(
|
||||
spring({ top: '0' }, { top: '100%' }, { preset: 'gentle' })
|
||||
);
|
||||
|
||||
const activeStyle = active
|
||||
? {
|
||||
color: 'var(--affine-icon-color)',
|
||||
top: '0',
|
||||
animation: css`
|
||||
${keyframes`${
|
||||
isHover ? activeRaiseAnimate : activeDeclineAnimate
|
||||
}`} ${ANIMATE_DURATION}ms forwards
|
||||
`,
|
||||
animationDirection: isHover ? 'normal' : 'alternate',
|
||||
}
|
||||
: {
|
||||
top: '100%',
|
||||
color: 'var(--affine-primary-color)',
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
animation: css`
|
||||
${keyframes`${
|
||||
isHover ? raiseAnimate : declineAnimate
|
||||
}`} ${ANIMATE_DURATION}ms forwards
|
||||
`,
|
||||
animationDirection: isHover ? 'normal' : 'alternate',
|
||||
};
|
||||
return css`
|
||||
${css(displayFlex('center', 'center'))}
|
||||
width:${inMenu ? '20px' : '32px'} ;
|
||||
height: ${inMenu ? '20px' : '32px'} ;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
cursor: pointer;
|
||||
color: ${activeStyle.color}
|
||||
top: ${activeStyle.top};
|
||||
background-color: ${activeStyle.backgroundColor};
|
||||
animation: ${activeStyle.animation};
|
||||
animation-direction: ${activeStyle.animationDirection};
|
||||
//svg {
|
||||
// width: 24px;
|
||||
// height: 24px;
|
||||
//},
|
||||
`;
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import { Logo1Icon, SignOutIcon } from '@blocksuite/icons';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const EditMenu = (
|
||||
<MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}>
|
||||
Sign Out
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
export const UserAvatar = () => {
|
||||
// fixme: cloud regression
|
||||
const user: any = null;
|
||||
return (
|
||||
<Menu
|
||||
width={276}
|
||||
content={EditMenu}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
{user ? (
|
||||
<WorkspaceAvatar
|
||||
size={24}
|
||||
name={user.name}
|
||||
avatar={user.avatar_url}
|
||||
></WorkspaceAvatar>
|
||||
) : (
|
||||
<WorkspaceAvatar size={24}></WorkspaceAvatar>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceAvatarProps {
|
||||
size: number;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
|
||||
function WorkspaceAvatar(props, ref) {
|
||||
const size = props.size || 20;
|
||||
const sizeStr = size + 'px';
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.avatar ? (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
color: '#fff',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<picture>
|
||||
<img
|
||||
style={{ width: sizeStr, height: sizeStr }}
|
||||
src={props.avatar}
|
||||
alt=""
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
border: '1px solid #fff',
|
||||
color: '#fff',
|
||||
fontSize: Math.ceil(0.5 * size) + 'px',
|
||||
borderRadius: '50%',
|
||||
textAlign: 'center',
|
||||
lineHeight: size + 'px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{props.name ? (
|
||||
props.name.substring(0, 1)
|
||||
) : (
|
||||
<Logo1Icon fontSize={24} color={'#5438FF'} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default UserAvatar;
|
||||
@@ -1,235 +0,0 @@
|
||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
||||
import {
|
||||
appSidebarFloatingAtom,
|
||||
appSidebarOpenAtom,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { headerItemsAtom } from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import DownloadClientTip from './download-tips';
|
||||
import { EditorOptionMenu } from './header-right-items/editor-option-menu';
|
||||
import * as styles from './styles.css';
|
||||
import { OSWarningMessage, shouldShowWarning } from './utils';
|
||||
|
||||
export type BaseHeaderProps<
|
||||
Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace,
|
||||
> = {
|
||||
workspace: Workspace;
|
||||
currentPage: Page | null;
|
||||
isPublic: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
};
|
||||
|
||||
export enum HeaderRightItemName {
|
||||
EditorOptionMenu = 'editorOptionMenu',
|
||||
// some windows only items
|
||||
WindowsAppControls = 'windowsAppControls',
|
||||
}
|
||||
|
||||
type HeaderItem = {
|
||||
Component: FC<BaseHeaderProps>;
|
||||
// todo: public workspace should be one of the flavour
|
||||
availableWhen: (
|
||||
workspace: AffineOfficialWorkspace,
|
||||
currentPage: Page | null,
|
||||
status: {
|
||||
isPublic: boolean;
|
||||
}
|
||||
) => boolean;
|
||||
};
|
||||
|
||||
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
[HeaderRightItemName.EditorOptionMenu]: {
|
||||
Component: EditorOptionMenu,
|
||||
availableWhen: (_, currentPage, { isPublic }) => {
|
||||
return (
|
||||
!isPublic && currentPage?.meta.trash !== true && currentPage !== null
|
||||
);
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.WindowsAppControls]: {
|
||||
Component: () => {
|
||||
const handleMinimizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMinimizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleMaximizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMaximizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleCloseApp = useCallback(() => {
|
||||
window.apis?.ui.handleCloseApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
data-platform-target="win32"
|
||||
className={styles.windowAppControlsWrapper}
|
||||
>
|
||||
<button
|
||||
data-type="minimize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleMinimizeApp}
|
||||
>
|
||||
<MinusIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="maximize"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleMaximizeApp}
|
||||
>
|
||||
<RoundedRectangleIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="close"
|
||||
className={styles.windowAppControl}
|
||||
onClick={handleCloseApp}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
availableWhen: () => {
|
||||
return isDesktop && globalThis.platform === 'win32';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type HeaderProps = BaseHeaderProps;
|
||||
|
||||
const PluginHeader = () => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const headerItems = useAtomValue(headerItemsAtom);
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(headerItems).map(([id, headerItem]) => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', id);
|
||||
const cleanup = headerItem(div);
|
||||
root.appendChild(div);
|
||||
return () => {
|
||||
cleanup();
|
||||
root.removeChild(div);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
disposes.forEach(dispose => dispose());
|
||||
});
|
||||
};
|
||||
}, [headerItems]);
|
||||
|
||||
return <div className={styles.pluginHeaderItems} ref={rootRef} />;
|
||||
};
|
||||
|
||||
export const Header = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
|
||||
>((props, ref) => {
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
const [showDownloadTip, setShowDownloadTip] = useAtom(
|
||||
guideDownloadClientTipAtom
|
||||
);
|
||||
useEffect(() => {
|
||||
setShowWarning(shouldShowWarning());
|
||||
}, []);
|
||||
const open = useAtomValue(appSidebarOpenAtom);
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
|
||||
const mode = useAtomValue(currentModeAtom);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.headerContainer}
|
||||
ref={ref}
|
||||
data-has-warning={showWarning}
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
>
|
||||
{showDownloadTip ? (
|
||||
<DownloadClientTip
|
||||
show={showDownloadTip}
|
||||
onClose={() => {
|
||||
setShowDownloadTip(false);
|
||||
localStorage.setItem('affine-is-dt-hide', '1');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<BrowserWarning
|
||||
show={showWarning}
|
||||
message={<OSWarningMessage />}
|
||||
onClose={() => {
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={styles.header}
|
||||
data-has-warning={showWarning}
|
||||
data-testid="editor-header-items"
|
||||
data-is-edgeless={mode === 'edgeless'}
|
||||
>
|
||||
<div className={styles.headerLeftSide}>
|
||||
{!open && <SidebarSwitch />}
|
||||
{props.leftSlot}
|
||||
</div>
|
||||
|
||||
{props.children}
|
||||
<div className={styles.headerRightSide}>
|
||||
<PluginHeader />
|
||||
{useMemo(() => {
|
||||
return Object.entries(HeaderRightItems).map(
|
||||
([name, { availableWhen, Component }]) => {
|
||||
if (
|
||||
availableWhen(props.workspace, props.currentPage, {
|
||||
isPublic: props.isPublic,
|
||||
})
|
||||
) {
|
||||
return (
|
||||
<Component
|
||||
workspace={props.workspace}
|
||||
currentPage={props.currentPage}
|
||||
isPublic={props.isPublic}
|
||||
key={name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}, [props])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Header.displayName = 'Header';
|
||||
@@ -1,114 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type {
|
||||
FC,
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../../../atoms';
|
||||
import { QuickSearchButton } from '../../pure/quick-search-button';
|
||||
import { EditorModeSwitch } from './editor-mode-switch';
|
||||
import type { BaseHeaderProps } from './header';
|
||||
import { Header } from './header';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export type WorkspaceHeaderProps = BaseHeaderProps;
|
||||
|
||||
export const BlockSuiteEditorHeader: FC<
|
||||
PropsWithChildren<WorkspaceHeaderProps> & HTMLAttributes<HTMLDivElement>
|
||||
> = (props): ReactElement => {
|
||||
const { workspace, currentPage, children, isPublic } = props;
|
||||
// fixme(himself65): remove this atom and move it to props
|
||||
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
|
||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
||||
meta => meta.id === currentPage?.id
|
||||
);
|
||||
const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (isEditable) {
|
||||
setIsEditable(!isEditable);
|
||||
const value = inputRef.current?.value;
|
||||
if (value !== pageMeta?.title && currentPage) {
|
||||
pageTitleMeta.setPageTitle(currentPage?.id, value || '');
|
||||
}
|
||||
} else {
|
||||
setIsEditable(!isEditable);
|
||||
}
|
||||
}, [currentPage, isEditable, pageMeta?.title, pageTitleMeta]);
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
assertExists(pageMeta);
|
||||
const title = pageMeta?.title;
|
||||
return (
|
||||
<Header ref={headerRef} {...props}>
|
||||
{children}
|
||||
{!isPublic && currentPage && (
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.titleWrapper}>
|
||||
<div className={styles.switchWrapper}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={currentPage.id}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{isEditable ? (
|
||||
<div>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className={styles.title}
|
||||
type="text"
|
||||
data-testid="title-content"
|
||||
defaultValue={pageMeta?.title}
|
||||
onBlur={handleClick}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
data-testid="save-edit-button"
|
||||
style={{
|
||||
marginLeft: '12px',
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
data-testid="title-edit-button"
|
||||
onClick={handleClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{title || 'Untitled'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.searchArrowWrapper}>
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
setOpenQuickSearch(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
BlockSuiteEditorHeader.displayName = 'BlockSuiteEditorHeader';
|
||||
@@ -1,256 +0,0 @@
|
||||
import type { ComplexStyleRule } from '@vanilla-extract/css';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerContainer = style({
|
||||
height: 'auto',
|
||||
flexShrink: 0,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
selectors: {
|
||||
'&[data-has-warning="true"]': {
|
||||
height: '96px',
|
||||
},
|
||||
'&[data-sidebar-floating="false"]': {
|
||||
WebkitAppRegion: 'drag',
|
||||
},
|
||||
},
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
':has([data-popper-placement])': {
|
||||
WebkitAppRegion: 'no-drag',
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const header = style({
|
||||
flexShrink: 0,
|
||||
height: '52px',
|
||||
width: '100%',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 99,
|
||||
position: 'relative',
|
||||
selectors: {
|
||||
'&[data-is-edgeless="true"]': {
|
||||
borderBottom: `1px solid var(--affine-border-color)`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const titleContainer = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
margin: 'auto',
|
||||
position: 'absolute',
|
||||
inset: 'auto auto auto 50%',
|
||||
transform: 'translate(-50%, 0px)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignContent: 'unset',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
maxWidth: '620px',
|
||||
transition: 'max-width .15s',
|
||||
userSelect: 'none',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'@media': {
|
||||
'(max-width: 768px)': {
|
||||
selectors: {
|
||||
'&[data-open="true"]': {
|
||||
WebkitAppRegion: 'no-drag',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const titleWrapper = style({
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const headerLeftSide = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '150px',
|
||||
'@media': {
|
||||
'(max-width: 900px)': {
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const headerRightSide = style({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
zIndex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
});
|
||||
|
||||
export const browserWarning = style({
|
||||
backgroundColor: 'var(--affine-background-warning-color)',
|
||||
color: 'var(--affine-warning-color)',
|
||||
height: '36px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
display: 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
selectors: {
|
||||
'&[data-show="true"]': {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const closeButton = style({
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
right: '15px',
|
||||
top: 0,
|
||||
});
|
||||
|
||||
export const switchWrapper = style({
|
||||
position: 'absolute',
|
||||
right: '100%',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const searchArrowWrapper = style({
|
||||
position: 'absolute',
|
||||
left: 'calc(100% + 4px)',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const pageListTitleWrapper = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const allPageListTitleWrapper = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
margin: '0 1px',
|
||||
},
|
||||
});
|
||||
export const pageListTitleIcon = style({
|
||||
fontSize: '20px',
|
||||
height: '1em',
|
||||
marginRight: '12px',
|
||||
});
|
||||
|
||||
export const quickSearchTipButton = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: '12px',
|
||||
color: '#FFFFFF',
|
||||
width: '48px',
|
||||
height: ' 26px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '22px',
|
||||
background: 'var(--affine-primary-color)',
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export const quickSearchTipContent = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const horizontalDivider = style({
|
||||
width: '100%',
|
||||
borderTop: `1px solid var(--affine-border-color)`,
|
||||
});
|
||||
|
||||
export const horizontalDividerContainer = style({
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
});
|
||||
|
||||
export const windowAppControlsWrapper = style({
|
||||
display: 'flex',
|
||||
gap: '2px',
|
||||
transform: 'translateX(8px)',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const windowAppControl = style({
|
||||
WebkitAppRegion: 'no-drag',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
width: '42px',
|
||||
height: 'calc(100% - 10px)',
|
||||
paddingTop: '10px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '0',
|
||||
selectors: {
|
||||
'&[data-type="close"]': {
|
||||
width: '56px',
|
||||
paddingRight: '14px',
|
||||
marginRight: '-14px',
|
||||
},
|
||||
'&[data-type="close"]:hover': {
|
||||
background: 'var(--affine-error-color)',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
'&:hover': {
|
||||
background: 'var(--affine-background-tertiary-color)',
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const pluginHeaderItems = style({
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import './page-detail-editor.css';
|
||||
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type { CallbackMap, LayoutNode } from '@affine/sdk//entry';
|
||||
import type { LayoutNode } from '@affine/sdk//entry';
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
@@ -9,31 +9,31 @@ import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import {
|
||||
contentLayoutAtom,
|
||||
editorItemsAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
addCleanup,
|
||||
pluginEditorAtom,
|
||||
pluginWindowAtom,
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
import { contentLayoutAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { CSSProperties, FC, ReactElement } from 'react';
|
||||
import { memo, Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type { CSSProperties, ReactElement } from 'react';
|
||||
import { memo, Suspense, useCallback, useMemo } from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import { pageSettingFamily } from '../atoms';
|
||||
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
import { TrashButtonGroup } from './blocksuite/workspace-header/header-right-items/trash-button-group';
|
||||
import * as styles from './page-detail-editor.css';
|
||||
import { pluginContainer } from './page-detail-editor.css';
|
||||
import { TrashButtonGroup } from './pure/trash-button-group';
|
||||
|
||||
export type PageDetailEditorProps = {
|
||||
export interface PageDetailEditorProps {
|
||||
isPublic?: boolean;
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
|
||||
onLoad?: (page: Page, editor: EditorContainer) => () => void;
|
||||
};
|
||||
}
|
||||
|
||||
const EditorWrapper = memo(function EditorWrapper({
|
||||
workspace,
|
||||
@@ -76,7 +76,6 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
'--affine-font-family': value,
|
||||
} as CSSProperties
|
||||
}
|
||||
key={`${workspace.id}-${pageId}`}
|
||||
mode={isPublic ? 'page' : currentMode}
|
||||
page={page}
|
||||
onInit={useCallback(
|
||||
@@ -96,7 +95,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
if (onLoad) {
|
||||
dispose = onLoad(page, editor);
|
||||
}
|
||||
const editorItems = rootStore.get(editorItemsAtom);
|
||||
const editorItems = rootStore.get(pluginEditorAtom);
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
|
||||
@@ -128,53 +127,51 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
);
|
||||
});
|
||||
|
||||
const PluginContentAdapter = memo<{
|
||||
windowItem: CallbackMap['window'];
|
||||
}>(function PluginContentAdapter({ windowItem }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const root = ref.current;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
let cleanup: () => void = () => {};
|
||||
let childDiv: HTMLDivElement | null = null;
|
||||
const renderTimeout = setTimeout(() => {
|
||||
const div = document.createElement('div');
|
||||
cleanup = windowItem(div);
|
||||
root.appendChild(div);
|
||||
childDiv = div;
|
||||
});
|
||||
interface PluginContentAdapterProps {
|
||||
windowItem: (div: HTMLDivElement) => () => void;
|
||||
pluginName: string;
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
if (childDiv) {
|
||||
root.removeChild(childDiv);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [windowItem]);
|
||||
return <div className={pluginContainer} ref={ref} />;
|
||||
});
|
||||
const PluginContentAdapter = memo<PluginContentAdapterProps>(
|
||||
function PluginContentAdapter({ windowItem, pluginName }) {
|
||||
return (
|
||||
<div
|
||||
className={pluginContainer}
|
||||
ref={useCallback(
|
||||
(ref: HTMLDivElement | null) => {
|
||||
if (ref) {
|
||||
const div = document.createElement('div');
|
||||
const cleanup = windowItem(div);
|
||||
ref.appendChild(div);
|
||||
addCleanup(pluginName, () => {
|
||||
cleanup();
|
||||
ref.removeChild(div);
|
||||
});
|
||||
}
|
||||
},
|
||||
[pluginName, windowItem]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type LayoutPanelProps = {
|
||||
interface LayoutPanelProps {
|
||||
node: LayoutNode;
|
||||
editorProps: PageDetailEditorProps;
|
||||
};
|
||||
}
|
||||
|
||||
const LayoutPanel = memo(function LayoutPanel(
|
||||
props: LayoutPanelProps
|
||||
): ReactElement {
|
||||
const node = props.node;
|
||||
const windowItems = useAtomValue(windowItemsAtom);
|
||||
const windowItems = useAtomValue(pluginWindowAtom);
|
||||
if (typeof node === 'string') {
|
||||
if (node === 'editor') {
|
||||
return <EditorWrapper {...props.editorProps} />;
|
||||
} else {
|
||||
const windowItem = windowItems[node];
|
||||
return <PluginContentAdapter windowItem={windowItem} />;
|
||||
return <PluginContentAdapter pluginName={node} windowItem={windowItem} />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
@@ -205,7 +202,7 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
}
|
||||
});
|
||||
|
||||
export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
|
||||
export const PageDetailEditor = (props: PageDetailEditorProps) => {
|
||||
const { workspace, pageId } = props;
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
if (!page) {
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type React from 'react';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { ChangeEvent, PropsWithChildren } from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export type UploadProps = React.PropsWithChildren<{
|
||||
export interface UploadProps {
|
||||
uploadType?: string;
|
||||
accept?: string;
|
||||
fileChange: (file: File) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const Upload: React.FC<UploadProps> = ({
|
||||
export const Upload = ({
|
||||
fileChange,
|
||||
accept,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
}: PropsWithChildren<UploadProps>) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const input_ref = useRef<HTMLInputElement>(null);
|
||||
const _chooseFile = () => {
|
||||
@@ -35,6 +34,7 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
input_ref.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<UploadStyle onClick={_chooseFile}>
|
||||
{children ?? <Button>{t['Upload']()}</Button>}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type CSSProperties, type FC, forwardRef } from 'react';
|
||||
import { type CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { stringToColour } from '../../../utils';
|
||||
import { StyledFooter, StyledSignInButton } from './styles';
|
||||
|
||||
export const Footer: FC = () => {
|
||||
export const Footer = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
|
||||
return (
|
||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||
<StyledSignInButton
|
||||
|
||||
@@ -4,17 +4,7 @@ import {
|
||||
styled,
|
||||
textEllipsis,
|
||||
} from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
|
||||
export const StyledSplitLine = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: 'var(--affine-border-color)',
|
||||
marginRight: '24px',
|
||||
};
|
||||
});
|
||||
|
||||
import { Button } from '@toeverything/components/button';
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { IconButtonProps } from '@affine/component';
|
||||
import { IconButton } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
IconButton,
|
||||
type IconButtonProps,
|
||||
} from '@toeverything/components/button';
|
||||
|
||||
const StyledIconButtonWithAnimate = styled(IconButton)(() => {
|
||||
return {
|
||||
@@ -12,21 +14,19 @@ const StyledIconButtonWithAnimate = styled(IconButton)(() => {
|
||||
svg: {
|
||||
transform: 'translateY(3px)',
|
||||
},
|
||||
'::after': {
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
},
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// fixme(himself65): need to refactor
|
||||
export const QuickSearchButton = ({
|
||||
export const HeaderDropDownButton = ({
|
||||
onClick,
|
||||
...props
|
||||
}: Omit<IconButtonProps, 'children'>) => {
|
||||
return (
|
||||
<StyledIconButtonWithAnimate
|
||||
data-testid="header-quickSearchButton"
|
||||
data-testid="header-dropDownButton"
|
||||
{...props}
|
||||
onClick={e => {
|
||||
onClick?.(e);
|
||||
150
apps/core/src/components/pure/header/index.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Wrapper } from '@affine/component';
|
||||
import {
|
||||
appSidebarFloatingAtom,
|
||||
appSidebarOpenAtom,
|
||||
SidebarSwitch,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import throttle from 'lodash.throttle';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import * as style from './style.css';
|
||||
import { TopTip } from './top-tip';
|
||||
import { WindowsAppControls } from './windows-app-controls';
|
||||
interface HeaderPros {
|
||||
left?: ReactNode;
|
||||
right?: ReactNode;
|
||||
center?: ReactNode;
|
||||
}
|
||||
|
||||
const useIsTinyScreen = ({
|
||||
mainContainer,
|
||||
leftDoms,
|
||||
centerDom,
|
||||
rightDoms,
|
||||
}: {
|
||||
mainContainer: HTMLElement;
|
||||
leftDoms: MutableRefObject<HTMLElement | null>[];
|
||||
centerDom: MutableRefObject<HTMLElement | null>;
|
||||
rightDoms: MutableRefObject<HTMLElement | null>[];
|
||||
}) => {
|
||||
const [isTinyScreen, setIsTinyScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = throttle(() => {
|
||||
if (!centerDom.current) {
|
||||
return;
|
||||
}
|
||||
const leftTotalWidth = leftDoms.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
const rightTotalWidth = rightDoms.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
const containerRect = mainContainer.getBoundingClientRect();
|
||||
const centerRect = centerDom.current.getBoundingClientRect();
|
||||
|
||||
const offset = isTinyScreen ? 50 : 0;
|
||||
if (
|
||||
leftTotalWidth + containerRect.left >= centerRect.left - offset ||
|
||||
containerRect.right - centerRect.right <= rightTotalWidth + offset
|
||||
) {
|
||||
setIsTinyScreen(true);
|
||||
} else {
|
||||
setIsTinyScreen(false);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
handleResize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(mainContainer);
|
||||
}, [centerDom, isTinyScreen, leftDoms, mainContainer, rightDoms]);
|
||||
|
||||
return isTinyScreen;
|
||||
};
|
||||
|
||||
// The Header component is used to solve the following problems
|
||||
// 1. Manage layout issues independently of page or business logic
|
||||
// 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed
|
||||
export const Header = ({ left, center, right }: HeaderPros) => {
|
||||
const sidebarSwitchRef = useRef<HTMLDivElement | null>(null);
|
||||
const leftSlotRef = useRef<HTMLDivElement | null>(null);
|
||||
const centerSlotRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightSlotRef = useRef<HTMLDivElement | null>(null);
|
||||
const windowControlsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const isTinyScreen = useIsTinyScreen({
|
||||
mainContainer: document.querySelector('.main-container') || document.body,
|
||||
leftDoms: [sidebarSwitchRef, leftSlotRef],
|
||||
centerDom: centerSlotRef,
|
||||
rightDoms: [rightSlotRef, windowControlsRef],
|
||||
});
|
||||
|
||||
const isWindowsDesktop = globalThis.platform === 'win32' && isDesktop;
|
||||
const open = useAtomValue(appSidebarOpenAtom);
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
return (
|
||||
<>
|
||||
<TopTip />
|
||||
<div
|
||||
className={style.header}
|
||||
// data-has-warning={showWarning}
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
data-testid="header"
|
||||
>
|
||||
<div
|
||||
className={clsx(style.headerSideContainer, {
|
||||
block: isTinyScreen,
|
||||
})}
|
||||
>
|
||||
<div className={clsx(style.headerItem, 'top-item')}>
|
||||
<div ref={sidebarSwitchRef}>
|
||||
{!open && (
|
||||
<Wrapper marginRight={20}>
|
||||
<SidebarSwitch />
|
||||
</Wrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(style.headerItem, 'left')}>
|
||||
<div ref={leftSlotRef}>{left}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx({
|
||||
[style.headerCenter]: center,
|
||||
'is-window': isWindowsDesktop,
|
||||
'has-min-width': !isTinyScreen,
|
||||
})}
|
||||
ref={centerSlotRef}
|
||||
>
|
||||
{center}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(style.headerSideContainer, 'right', {
|
||||
block: isTinyScreen,
|
||||
})}
|
||||
>
|
||||
<div className={clsx(style.headerItem, 'top-item')}>
|
||||
<div ref={windowControlsRef}>
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(style.headerItem, 'right')}>
|
||||
<div ref={rightSlotRef}>{right}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
111
apps/core/src/components/pure/header/style.css.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { ComplexStyleRule } from '@vanilla-extract/css';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const header = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
position: 'relative',
|
||||
padding: '0 16px',
|
||||
minHeight: '52px',
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
selectors: {
|
||||
'&[data-sidebar-floating="false"]': {
|
||||
WebkitAppRegion: 'drag',
|
||||
},
|
||||
},
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
':has([data-popper-placement])': {
|
||||
WebkitAppRegion: 'no-drag',
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const headerItem = style({
|
||||
minHeight: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
selectors: {
|
||||
'&.top-item': {
|
||||
height: '52px',
|
||||
},
|
||||
'&.left': {
|
||||
justifyContent: 'left',
|
||||
},
|
||||
'&.right': {
|
||||
justifyContent: 'right',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const headerCenter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '52px',
|
||||
flexShrink: 0,
|
||||
maxWidth: '60%',
|
||||
position: 'absolute',
|
||||
transform: 'translateX(-50%)',
|
||||
left: '50%',
|
||||
zIndex: 1,
|
||||
selectors: {
|
||||
'&.is-window': {
|
||||
maxWidth: '50%',
|
||||
},
|
||||
'&.is-window.has-min-width': {
|
||||
minWidth: '400px',
|
||||
},
|
||||
'&.shadow': {
|
||||
position: 'static',
|
||||
visibility: 'hidden',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const headerSideContainer = style({
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
selectors: {
|
||||
'&.right': {
|
||||
flexDirection: 'row-reverse',
|
||||
},
|
||||
'&.block': {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const windowAppControlsWrapper = style({
|
||||
display: 'flex',
|
||||
marginLeft: '20px',
|
||||
});
|
||||
|
||||
export const windowAppControl = style({
|
||||
WebkitAppRegion: 'no-drag',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
width: '52px',
|
||||
height: '52px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '0',
|
||||
selectors: {
|
||||
'&[data-type="close"]': {
|
||||
width: '56px',
|
||||
paddingRight: '5px',
|
||||
marginRight: '-12px',
|
||||
},
|
||||
'&[data-type="close"]:hover': {
|
||||
background: 'var(--affine-windows-close-button)',
|
||||
color: 'var(--affine-pure-white)',
|
||||
},
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
@@ -1,12 +1,16 @@
|
||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
||||
import { DownloadTips } from '@affine/component/affine-banner';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type React from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
|
||||
|
||||
const minimumChromeVersion = 102;
|
||||
|
||||
export const shouldShowWarning = () => {
|
||||
const shouldShowWarning = () => {
|
||||
if (isDesktop) {
|
||||
// even though desktop has compatibility issues,
|
||||
// we don't want to show the warning
|
||||
@@ -23,7 +27,7 @@ export const shouldShowWarning = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const OSWarningMessage: React.FC = () => {
|
||||
const OSWarningMessage = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [notChrome, setNotChrome] = useState(false);
|
||||
const [notGoodVersion, setNotGoodVersion] = useState(false);
|
||||
@@ -50,3 +54,34 @@ export const OSWarningMessage: React.FC = () => {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const TopTip = () => {
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
const [showDownloadTip, setShowDownloadTip] = useAtom(
|
||||
guideDownloadClientTipAtom
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setShowWarning(shouldShowWarning());
|
||||
}, []);
|
||||
|
||||
if (showDownloadTip && isDesktop) {
|
||||
return (
|
||||
<DownloadTips
|
||||
onClose={() => {
|
||||
setShowDownloadTip(false);
|
||||
localStorage.setItem('affine-is-dt-hide', '1');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserWarning
|
||||
show={showWarning}
|
||||
message={<OSWarningMessage />}
|
||||
onClose={() => {
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as style from './style.css';
|
||||
|
||||
export const WindowsAppControls = () => {
|
||||
const handleMinimizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMinimizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleMaximizeApp = useCallback(() => {
|
||||
window.apis?.ui.handleMaximizeApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
const handleCloseApp = useCallback(() => {
|
||||
window.apis?.ui.handleCloseApp().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-platform-target="win32"
|
||||
className={style.windowAppControlsWrapper}
|
||||
>
|
||||
<button
|
||||
data-type="minimize"
|
||||
className={style.windowAppControl}
|
||||
onClick={handleMinimizeApp}
|
||||
>
|
||||
<MinusIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="maximize"
|
||||
className={style.windowAppControl}
|
||||
onClick={handleMaximizeApp}
|
||||
>
|
||||
<RoundedRectangleIcon />
|
||||
</button>
|
||||
<button
|
||||
data-type="close"
|
||||
className={style.windowAppControl}
|
||||
onClick={handleCloseApp}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -57,7 +57,7 @@ export const HelpIsland = ({
|
||||
inEdgelessPage={mode === 'edgeless'}
|
||||
>
|
||||
<StyledAnimateWrapper
|
||||
style={{ height: spread ? `${showList.length * 44}px` : 0 }}
|
||||
style={{ height: spread ? `${showList.length * 40 + 4}px` : 0 }}
|
||||
>
|
||||
{showList.includes('whatNew') && (
|
||||
<Tooltip content={t["Discover what's new!"]()} placement="left-end">
|
||||
|
||||
44
apps/core/src/components/pure/plugin-header/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
addCleanup,
|
||||
pluginHeaderItemAtom,
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { startTransition, useCallback, useRef } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
export const PluginHeader = () => {
|
||||
const headerItem = useAtomValue(pluginHeaderItemAtom);
|
||||
const pluginsRef = useRef<string[]>([]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.pluginHeaderItems}
|
||||
ref={useCallback(
|
||||
(root: HTMLDivElement | null) => {
|
||||
if (root) {
|
||||
Object.entries(headerItem).forEach(([pluginName, create]) => {
|
||||
if (pluginsRef.current.includes(pluginName)) {
|
||||
return;
|
||||
}
|
||||
pluginsRef.current.push(pluginName);
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', pluginName);
|
||||
startTransition(() => {
|
||||
const cleanup = create(div);
|
||||
root.appendChild(div);
|
||||
addCleanup(pluginName, () => {
|
||||
pluginsRef.current = pluginsRef.current.filter(
|
||||
name => name !== pluginName
|
||||
);
|
||||
root.removeChild(div);
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[headerItem]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const pluginHeaderItems = style({
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
});
|
||||