Compare commits

...

47 Commits

Author SHA1 Message Date
liuyi
8b066a4b39 fix(core): auth events are not continuous (#8874) 2024-11-20 16:25:37 +08:00
renovate[bot]
cc0462e7fe chore: bump up @blocksuite/affine version to v0.17.33 (#8873)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 07:26:43 +00:00
Chen
b87c3840f3 chore(core): remove snapshot import export feature flag (#8865) 2024-11-20 07:06:20 +00:00
pengx17
47243247b9 fix(electron): respect locale for spellchecker (#8844)
may fix #8837.
fix AF-1712

MacOS automatically do spellcheck based on given text. This only works for Win/Linux.
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/25058395-d70a-42a4-b869-c69fa71bc418.png)
2024-11-20 06:44:36 +00:00
Peng Xiao
8689465e00 fix(electron): potential crash on quit (#8855) 2024-11-20 14:44:06 +08:00
akumatus
b0ca3c6d58 fix(core): linked doc named input box has excessive desire to select all (#8861)
Fix issue [AF-1706](https://linear.app/affine-design/issue/AF-1706).
2024-11-20 06:25:36 +00:00
renovate[bot]
2857568f03 chore: bump up @blocksuite/icons version to v2.1.70 (#8866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 14:24:31 +08:00
Cats Juice
2e94944d2b fix(core): ai subscribe button is not show in ai usage page (#8869) 2024-11-20 14:23:30 +08:00
Peng Xiao
afa0e31ecd feat(core): open in app cmd (#8870) 2024-11-20 14:23:18 +08:00
CatsJuice
3390fbc5db feat(mobile): hide tab when virtual-keyboard show up (#8862) 2024-11-20 01:33:19 +00:00
CatsJuice
cd2c2b7fdb feat(ios): live reload script and docs (#8863) 2024-11-20 01:33:18 +00:00
pengx17
3f5dadb4f5 fix(core): doc info event tracking issues (#8868)
fix AF-1769
2024-11-20 01:15:51 +00:00
Peng Xiao
401106203c fix(core): info modal style issue (#8842) 2024-11-18 23:36:31 +08:00
Peng Xiao
e200e0a1a0 fix(core): peek view animation (#8858) 2024-11-18 23:31:10 +08:00
Peng Xiao
56a3f054f9 fix(core): emoji doc name rendering on windows (#8857) 2024-11-18 23:28:54 +08:00
Cats Juice
abaea9e605 fix(core): cloud pricing plan cards can not scroll (#8851) 2024-11-18 23:28:39 +08:00
darkskygit
1c2b23b160 feat(native): split package (#8853) 2024-11-18 13:18:16 +00:00
CatsJuice
9642566086 fix(core): make right-sidebar scrollable (#8830) 2024-11-18 08:47:12 +00:00
CatsJuice
bd7c422c46 feat(mobile): impl masonry docs with flex and predict card height (#8849)
previous `columns` implementation has some limitation:
- the card order is not as expected
- there may be strange shadow on top
2024-11-18 08:30:08 +00:00
renovate
bf093710b7 chore: bump up nestjs to v10.4.8 (#8843)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@nestjs/common](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common)) | [`10.4.7` -> `10.4.8`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/10.4.7/10.4.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcommon/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcommon/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/core](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core)) | [`10.4.7` -> `10.4.8`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/10.4.7/10.4.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fcore/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fcore/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/platform-express](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-express)) | [`10.4.7` -> `10.4.8`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/10.4.7/10.4.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-express/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-express/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-express/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-express/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/platform-socket.io](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-socket.io)) | [`10.4.7` -> `10.4.8`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-socket.io/10.4.7/10.4.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-socket.io/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fplatform-socket.io/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fplatform-socket.io/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-socket.io/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/testing](https://nestjs.com) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/testing)) | [`10.4.7` -> `10.4.8`](https://renovatebot.com/diffs/npm/@nestjs%2ftesting/10.4.7/10.4.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2ftesting/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2ftesting/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2ftesting/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2ftesting/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) |
| [@nestjs/websockets](https://redirect.github.com/nestjs/nest) ([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/websockets)) | [`10.4.7` -> `10.4.8`](https://renovatebot.com/diffs/npm/@nestjs%2fwebsockets/10.4.7/10.4.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fwebsockets/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nestjs%2fwebsockets/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nestjs%2fwebsockets/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fwebsockets/10.4.7/10.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>nestjs/nest (@&#8203;nestjs/common)</summary>

### [`v10.4.8`](https://redirect.github.com/nestjs/nest/releases/tag/v10.4.8)

[Compare Source](https://redirect.github.com/nestjs/nest/compare/v10.4.7...v10.4.8)

##### v10.4.8 (2024-11-15)

##### Bug fixes

-   `microservices`
    -   [#&#8203;14059](https://redirect.github.com/nestjs/nest/pull/14059) fix(microservices): include discarded rmq client options ([@&#8203;v-sum](https://redirect.github.com/v-sum))
    -   [#&#8203;14132](https://redirect.github.com/nestjs/nest/pull/14132) fix(microservices): no messages emitted with mqtt when qos set ([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
-   `core`
    -   [#&#8203;14133](https://redirect.github.com/nestjs/nest/pull/14133) fix(core): flaky durable provider, remove instance on error  ([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))

##### Enhancements

-   `core`
    -   [#&#8203;14143](https://redirect.github.com/nestjs/nest/pull/14143) feat(core): expose listening stream from http adapter host ([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
    -   [#&#8203;14139](https://redirect.github.com/nestjs/nest/pull/14139) chore(core): defer application shutdown until init finishes ([@&#8203;mksony](https://redirect.github.com/mksony))

##### Committers: 3

-   Kamil Mysliwiec ([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
-   Max ([@&#8203;mksony](https://redirect.github.com/mksony))
-   Vasile Sumanschi ([@&#8203;v-sum](https://redirect.github.com/v-sum))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMS41IiwidXBkYXRlZEluVmVyIjoiMzkuMTEuNSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-11-18 05:14:06 +00:00
CatsJuice
ffa4d5422d fix(mobile): use mobile app-fallback in app-container (#8804)
close AF-1633
2024-11-18 02:31:16 +00:00
Brooooooklyn
a97ee60502 test(server): fix fakerjs deprecated warning (#8834) 2024-11-15 14:11:57 +00:00
liuyi
84cfcb193f fix(server): should not enable experimental mobile version in selfhost (#8840) 2024-11-15 17:47:34 +08:00
CatsJuice
1f71e87460 fix(mobile): correct search & home-header bg color (#8835) 2024-11-15 08:19:15 +00:00
forehalo
54c51225ed fix(core): wrong title field from editor container proxy (#8826)
fix AF-1689
2024-11-15 06:02:41 +00:00
CatsJuice
5fade7aaf5 fix(mobile): correct doc card title display (#8836) 2024-11-15 04:36:24 +00:00
pengx17
df99e2ca97 fix(electron): potential crash on quit (#8829)
fix AF-1696

Related log:
```
Thread 0 Crashed:: CrBrowserMain Dispatch queue: com.apple.main-thread
0   Electron Framework            	       0x10c3bb09c electron::MessagePort::Close()
1   Electron Framework            	       0x10c3bcb78 gin::internal::Dispatcher<void (electron::MessagePort*)>::DispatchToCallbackImpl(gin::Arguments*)
2   Electron Framework            	       0x10c3bc984 gin::internal::Dispatcher<void (electron::MessagePort*)>::DispatchToCallback(v8::FunctionCallbackInfo<v8::Value> const&)
3   ???                           	       0x147e0f58c ???
4   ???                           	       0x147e0d45c ???
5   ???                           	       0x147e0d45c ???
6   ???                           	       0x147e0d45c ???
7   ???                           	       0x1401743b8 ???
8   ???                           	       0x147e0b088 ???
9   ???                           	       0x147e0acd4 ???
10  Electron Framework            	       0x10d657578 v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*)
11  Electron Framework            	       0x10d53e478 v8::Function::Call(v8::Isolate*, v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*)
12  Electron Framework            	       0x11337ad34 node::InternalMakeCallback(node::Environment*, v8::Local<v8::Object>, v8::Local<v8::Object>, v8::Local<v8::Function>, int, v8::Local<v8::Value>*, node::async_context)
13  Electron Framework            	       0x11337b0b0 node::MakeCallback(v8::Isolate*, v8::Local<v8::Object>, v8::Local<v8::Function>, int, v8::Local<v8::Value>*, node::async_context)
14  Electron Framework            	       0x10c46d9dc gin_helper::internal::CallMethodWithArgs(v8::Isolate*, v8::Local<v8::Object>, char const*, base::span<v8::Local<v8::Value>, 18446744073709551615ul, v8::Local<v8::Value>*>)
15  Electron Framework            	       0x10c3833b0 bool gin_helper::EventEmitterMixin<electron::api::WebContents>::Emit<>(std::__Cr::basic_string_view<char, std::__Cr::char_traits<char>>)
16  Electron Framework            	       0x10c38324c electron::api::WebContents::WebContentsDestroyed()
17  Electron Framework            	       0x10ebc2ee8 void content::WebContentsImpl::WebContentsObserverList::NotifyObservers<void (content::WebContentsObserver::*)()>(void (content::WebContentsObserver::*)())
18  Electron Framework            	       0x10ebc229c content::WebContentsImpl::~WebContentsImpl()
19  Electron Framework            	       0x10ebc32b0 content::WebContentsImpl::~WebContentsImpl()
20  Electron Framework            	       0x10c4212dc electron::InspectableWebContents::~InspectableWebContents()
21  Electron Framework            	       0x10c4213ac electron::InspectableWebContents::~InspectableWebContents()
22  Electron Framework            	       0x10c382ffc electron::api::WebContents::~WebContents()
23  Electron Framework            	       0x10c383498 non-virtual thunk to electron::api::WebContents::~WebContents()
24  Electron Framework            	       0x10c46cb80 gin_helper::CleanedUpAtExit::DoCleanup()
25  Electron Framework            	       0x10c3ec994 electron::JavascriptEnvironment::DestroyMicrotasksRunner()
26  Electron Framework            	       0x10c3d2828 electron::ElectronBrowserMainParts::PostMainMessageLoopRun()
27  Electron Framework            	       0x10e663458 content::BrowserMainLoop::ShutdownThreadsAndCleanUp()
28  Electron Framework            	       0x10e664a80 content::BrowserMainRunnerImpl::Shutdown()
29  Electron Framework            	       0x10e661098 content::BrowserMain(content::MainFunctionParams)
30  Electron Framework            	       0x10c6339a4 content::RunBrowserProcessMain(content::MainFunctionParams, content::ContentMainDelegate*)
31  Electron Framework            	       0x10c634968 content::ContentMainRunnerImpl::RunBrowser(content::MainFunctionParams, bool)
32  Electron Framework            	       0x10c6347e8 content::ContentMainRunnerImpl::Run()
33  Electron Framework            	       0x10c633208 content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*)
34  Electron Framework            	       0x10c6332f0 content::ContentMain(content::ContentMainParams)
35  Electron Framework            	       0x10c2e98c0 ElectronMain
36  dyld                          	       0x18b5a8274 start
```
2024-11-15 02:58:19 +00:00
renovate
4610f1e934 chore: bump up slackapi/slack-github-action action to v2 (#8833)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [slackapi/slack-github-action](https://redirect.github.com/slackapi/slack-github-action) | action | major | `v1.27.0` -> `v2.0.0` |

---

### Release Notes

<details>
<summary>slackapi/slack-github-action (slackapi/slack-github-action)</summary>

### [`v2.0.0`](https://redirect.github.com/slackapi/slack-github-action/compare/v1.27.1...v2.0.0)

[Compare Source](https://redirect.github.com/slackapi/slack-github-action/compare/v1.27.1...v2.0.0)

### [`v1.27.1`](https://redirect.github.com/slackapi/slack-github-action/releases/tag/v1.27.1): Slack Send V1.27.1

[Compare Source](https://redirect.github.com/slackapi/slack-github-action/compare/v1.27.0...v1.27.1)

##### What's changed

This release tags multiple updates to dependencies and a clarification in documentation 📚

No changes are needed to update from `slackapi/slack-github-action@v1.27.0` - other than bumping the version - but ongoing development is now happening on version `@v2` and `@v1` is no longer planning to receive significant updates after this.

Please stay tuned for upcoming changes, and may all of your workflows run well ❤️

##### 📚 Documentation

-   docs(fix): reference octokit context and github event webhook payloads in variables in [https://github.com/slackapi/slack-github-action/pull/348](https://redirect.github.com/slackapi/slack-github-action/pull/348) - thanks [@&#8203;zimeg](https://redirect.github.com/zimeg)!

##### 🧪 Maintenance

-   ci: include the slack health score as part of ci tests in [https://github.com/slackapi/slack-github-action/pull/346](https://redirect.github.com/slackapi/slack-github-action/pull/346) - thanks [@&#8203;zimeg](https://redirect.github.com/zimeg)!

##### 📦 Dependencies

-   build(deps): bump axios from 1.7.5 to 1.7.7 in [https://github.com/slackapi/slack-github-action/pull/334](https://redirect.github.com/slackapi/slack-github-action/pull/334) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump mocha from 10.7.0 to 10.7.3 in [https://github.com/slackapi/slack-github-action/pull/335](https://redirect.github.com/slackapi/slack-github-action/pull/335) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump eslint-plugin-jsdoc from 48.10.2 to 50.2.2 in [https://github.com/slackapi/slack-github-action/pull/336](https://redirect.github.com/slackapi/slack-github-action/pull/336) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps): bump [@&#8203;slack/web-api](https://redirect.github.com/slack/web-api) from 7.3.4 to 7.5.0 in [https://github.com/slackapi/slack-github-action/pull/339](https://redirect.github.com/slackapi/slack-github-action/pull/339) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump eslint-plugin-import from 2.29.1 to 2.30.0 in [https://github.com/slackapi/slack-github-action/pull/341](https://redirect.github.com/slackapi/slack-github-action/pull/341) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump eslint-plugin-jsdoc from 50.2.2 to 50.3.1 in [https://github.com/slackapi/slack-github-action/pull/344](https://redirect.github.com/slackapi/slack-github-action/pull/344) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump nyc from 17.0.0 to 17.1.0 in [https://github.com/slackapi/slack-github-action/pull/342](https://redirect.github.com/slackapi/slack-github-action/pull/342) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump sinon from 18.0.0 to 19.0.2 in [https://github.com/slackapi/slack-github-action/pull/343](https://redirect.github.com/slackapi/slack-github-action/pull/343) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump mocha from 10.7.3 to 10.8.2 in [https://github.com/slackapi/slack-github-action/pull/350](https://redirect.github.com/slackapi/slack-github-action/pull/350) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps): bump [@&#8203;slack/web-api](https://redirect.github.com/slack/web-api) from 7.5.0 to 7.7.0 in [https://github.com/slackapi/slack-github-action/pull/351](https://redirect.github.com/slackapi/slack-github-action/pull/351) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump eslint-plugin-import from 2.30.0 to 2.31.0 in [https://github.com/slackapi/slack-github-action/pull/352](https://redirect.github.com/slackapi/slack-github-action/pull/352) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps-dev): bump eslint-plugin-jsdoc from 50.3.1 to 50.4.3 in [https://github.com/slackapi/slack-github-action/pull/353](https://redirect.github.com/slackapi/slack-github-action/pull/353) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps): bump [@&#8203;actions/core](https://redirect.github.com/actions/core) from 1.10.1 to 1.11.1 in [https://github.com/slackapi/slack-github-action/pull/354](https://redirect.github.com/slackapi/slack-github-action/pull/354) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!
-   build(deps): bump codecov/codecov-action from 4.5.0 to 4.6.0 in [https://github.com/slackapi/slack-github-action/pull/355](https://redirect.github.com/slackapi/slack-github-action/pull/355) - thanks [@&#8203;dependabot](https://redirect.github.com/dependabot)!

**Full Changelog**: https://github.com/slackapi/slack-github-action/compare/v1.27.0...v1.27.1

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMS41IiwidXBkYXRlZEluVmVyIjoiMzkuMTEuNSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-11-15 02:38:19 +00:00
renovate
78ef9fee34 chore: bump up codecov/codecov-action action to v5 (#8832)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [codecov/codecov-action](https://redirect.github.com/codecov/codecov-action) | action | major | `v4` -> `v5` |

---

### Release Notes

<details>
<summary>codecov/codecov-action (codecov/codecov-action)</summary>

### [`v5`](https://redirect.github.com/codecov/codecov-action/compare/v4...v5)

[Compare Source](https://redirect.github.com/codecov/codecov-action/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMS41IiwidXBkYXRlZEluVmVyIjoiMzkuMTEuNSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-11-15 00:49:48 +00:00
forehalo
18089c7369 chore(i18n): update completenesses 2024-11-14 20:16:16 +08:00
darkskygit
991e0b9b63 feat: improve challenge test case (#8825) 2024-11-14 10:22:38 +00:00
DarkSky
129cceade9 feat: add compatible for captcha request with challenge (#8827) 2024-11-14 10:22:02 +00:00
forehalo
055fa0a8b4 fix(core): polyfill iterator helper for safari (#8824)
fix AF-1691
2024-11-14 09:59:01 +00:00
forehalo
9f3dceb220 feat(server): add captcha runtime flag (#8823)
fix AF-1702
2024-11-14 09:41:37 +00:00
forehalo
6a64055886 fix(core): auth fail reason should be error.name (#8822)
fix AF-1701
2024-11-14 09:24:01 +00:00
EYHN
c712e87114 fix(core): warning when open center peek (#8819) 2024-11-14 09:04:38 +00:00
CatsJuice
343152e162 fix(core): center peek zoomIn animation not work in firefox (#8817) 2024-11-14 08:30:07 +00:00
JimmFly
97d6f53932 chore: remove community link from header (#8818)
close AF-1695
2024-11-14 08:15:01 +00:00
Brooooooklyn
44e00f67c4 ci: fix release notes generation (#8815) 2024-11-14 07:13:02 +00:00
CatsJuice
39cb1b7714 chore: bump theme, update mobile home background color (#8799) 2024-11-14 06:57:08 +00:00
donteatfriedrice
6f5c61b8b6 feat(core): update import entry in all page and page list (#8814) 2024-11-14 06:41:01 +00:00
CatsJuice
87520e9bf9 fix(core): center peek can't open in firefox and safari (#8816)
close AF-1690
2024-11-14 06:23:34 +00:00
github-actions[bot]
181b213a3e chore(i18n): sync translations (#8785)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-11-14 06:22:20 +00:00
L-Sun
a8938ab403 chore(mobile): disable reference popup on mobile (#8743)
This PR disable reference node popup on mobile. Close [BS-1730](https://linear.app/affine-design/issue/BS-1730/%E7%A6%81%E7%94%A8-block-yuan%E7%B4%A0%E7%9A%84-toolbar)
2024-11-14 04:47:31 +00:00
darkskygit
ca8bb6dc90 feat(server): make copilot test faster (#8806) 2024-11-14 04:24:28 +00:00
forehalo
21c7d1810d chore: bump base version to 0.18.0 2024-11-14 11:41:51 +08:00
forehalo
2fa843b960 ci: wrong replica config for stable 2024-11-14 11:37:23 +08:00
171 changed files with 2028 additions and 1127 deletions

View File

@@ -41,7 +41,7 @@ const isBeta = buildType === 'beta';
const isInternal = buildType === 'internal';
const replicaConfig = {
production: {
stable: {
web: 3,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,

28
.github/actions/setup-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: 'Rust setup'
description: 'Rust setup, including cache configuration'
inputs:
components:
description: 'Cargo components'
required: false
targets:
description: 'Cargo target'
required: false
toolchain:
description: 'Rustup toolchain'
required: false
default: 'stable'
runs:
using: 'composite'
steps:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.toolchain }}
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}
- name: Add Targets
if: ${{ inputs.targets }}
run: rustup target add ${{ inputs.targets }}
shell: bash
- uses: Swatinem/rust-cache@v2

View File

@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.17.0"
appVersion: "0.18.0"

View File

@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.17.0"
appVersion: "0.18.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.17.0"
appVersion: "0.18.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -223,7 +223,7 @@ jobs:
run: yarn nx test:coverage @affine/monorepo
- name: Upload unit test coverage results
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/store/lcov.info
@@ -371,7 +371,7 @@ jobs:
COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key'
- name: Upload server test coverage results
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
@@ -379,6 +379,23 @@ jobs:
name: affine
fail_ci_if_error: false
server-native-test:
name: Run server native tests
runs-on: ubuntu-latest
env:
RUSTFLAGS: -D warnings
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: ./.github/actions/setup-rust
- name: Install latest nextest release
uses: taiki-e/install-action@nextest
- name: Run tests
run: cargo nextest run --release
copilot-api-test:
name: Server Copilot Api Test
runs-on: ubuntu-latest
@@ -454,7 +471,7 @@ jobs:
- name: Upload server test coverage results
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
@@ -755,6 +772,8 @@ jobs:
- build-server-native
- build-electron-renderer
- server-test
- server-native-test
- copilot-api-test
- copilot-e2e-test
- server-e2e-test
- desktop-test

View File

@@ -86,7 +86,7 @@ jobs:
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
- name: Upload server test coverage results
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info

View File

@@ -173,41 +173,31 @@ jobs:
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
- name: Post Failed event to a Slack channel
id: failed-slack
uses: slackapi/slack-github-action@v1.27.0
uses: slackapi/slack-github-action@v2.0.0
if: ${{ always() && contains(needs.*.result, 'failure') }}
with:
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>",
"type": "mrkdwn"
}
}
]
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
blocks:
- type: section
text:
type: mrkdwn
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
- name: Post Cancel event to a Slack channel
id: cancel-slack
uses: slackapi/slack-github-action@v1.27.0
uses: slackapi/slack-github-action@v2.0.0
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
with:
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
token: ${{ secrets.SLACK_BOT_TOKEN }}
method: chat.postMessage
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>",
"type": "mrkdwn"
}
}
]
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"
blocks:
- type: section
text:
type: mrkdwn
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"

View File

@@ -97,6 +97,7 @@ jobs:
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps:
- uses: actions/checkout@v4
@@ -213,6 +214,7 @@ jobs:
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
steps:
- uses: actions/checkout@v4

55
Cargo.lock generated
View File

@@ -17,10 +17,21 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "affine_common"
version = "0.1.0"
dependencies = [
"chrono",
"rand",
"rayon",
"sha3",
]
[[package]]
name = "affine_native"
version = "0.0.0"
dependencies = [
"affine_common",
"affine_schema",
"anyhow",
"chrono",
@@ -32,6 +43,7 @@ dependencies = [
"once_cell",
"parking_lot",
"rand",
"rayon",
"serde",
"serde_json",
"sha3",
@@ -48,6 +60,7 @@ version = "0.0.0"
name = "affine_server_native"
version = "1.0.0"
dependencies = [
"affine_common",
"chrono",
"file-format",
"mimalloc",
@@ -55,6 +68,7 @@ dependencies = [
"napi-build",
"napi-derive",
"rand",
"rayon",
"sha3",
"tiktoken-rs",
"tokio",
@@ -331,6 +345,25 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.11"
@@ -1013,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -1514,6 +1547,26 @@ dependencies = [
"rand",
]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"

View File

@@ -1,29 +1,36 @@
[workspace]
members = ["./packages/backend/native", "./packages/frontend/native", "./packages/frontend/native/schema"]
members = [
"./packages/backend/native",
"./packages/common/native",
"./packages/frontend/native",
"./packages/frontend/native/schema"
]
resolver = "2"
[workspace.dependencies]
anyhow = "1"
chrono = "0.4"
dotenv = "0.15"
file-format = { version = "0.26", features = ["reader"] }
mimalloc = "0.1"
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-alpha.12" }
notify = { version = "7", features = ["serde"] }
once_cell = "1"
parking_lot = "0.12"
rand = "0.8"
serde = "1"
serde_json = "1"
sha3 = "0.10"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
tiktoken-rs = "0.6"
tokio = "1.37"
uuid = "1.8"
v_htmlescape = "0.15"
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
affine_common = { path = "./packages/common/native" }
anyhow = "1"
chrono = "0.4"
dotenv = "0.15"
file-format = { version = "0.26", features = ["reader"] }
mimalloc = "0.1"
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-alpha.12" }
notify = { version = "7", features = ["serde"] }
once_cell = "1"
parking_lot = "0.12"
rand = "0.8"
rayon = "1.10"
serde = "1"
serde_json = "1"
sha3 = "0.10"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
tiktoken-rs = "0.6"
tokio = "1.37"
uuid = "1.8"
v_htmlescape = "0.15"
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
[profile.dev.package.sqlx-macros]
opt-level = 3

View File

@@ -19,5 +19,5 @@
],
"ext": "ts,md,json"
},
"version": "0.17.0"
"version": "0.18.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/monorepo",
"version": "0.17.0",
"version": "0.18.0",
"private": true,
"author": "toeverything",
"license": "MIT",

View File

@@ -7,15 +7,16 @@ version = "1.0.0"
crate-type = ["cdylib"]
[dependencies]
chrono = { workspace = true }
file-format = { workspace = true }
napi = { workspace = true }
napi-derive = { workspace = true }
rand = { workspace = true }
sha3 = { workspace = true }
tiktoken-rs = { workspace = true }
v_htmlescape = { workspace = true }
y-octo = { workspace = true }
affine_common = { workspace = true }
chrono = { workspace = true }
file-format = { workspace = true }
napi = { workspace = true }
napi-derive = { workspace = true }
rand = { workspace = true }
sha3 = { workspace = true }
tiktoken-rs = { workspace = true }
v_htmlescape = { workspace = true }
y-octo = { workspace = true }
[target.'cfg(not(target_os = "linux"))'.dependencies]
mimalloc = { workspace = true }
@@ -24,7 +25,8 @@ mimalloc = { workspace = true }
mimalloc = { workspace = true, features = ["local_dynamic_tls"] }
[dev-dependencies]
tokio = "1"
rayon = { workspace = true }
tokio = { workspace = true }
[build-dependencies]
napi-build = { workspace = true }

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/server-native",
"version": "0.17.0",
"version": "0.18.0",
"engines": {
"node": ">= 10.16.0 < 11 || >= 11.8.0"
},

View File

@@ -1 +0,0 @@
../../../frontend/native/src/hashcash.rs

View File

@@ -0,0 +1,69 @@
use std::convert::TryFrom;
use affine_common::hashcash::Stamp;
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
use napi_derive::napi;
pub struct AsyncVerifyChallengeResponse {
response: String,
bits: u32,
resource: String,
}
#[napi]
impl Task for AsyncVerifyChallengeResponse {
type Output = bool;
type JsValue = JsBoolean;
fn compute(&mut self) -> NapiResult<Self::Output> {
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
stamp.check(self.bits, &self.resource)
} else {
false
})
}
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
env.get_boolean(output)
}
}
#[napi]
pub fn verify_challenge_response(
response: String,
bits: u32,
resource: String,
) -> AsyncTask<AsyncVerifyChallengeResponse> {
AsyncTask::new(AsyncVerifyChallengeResponse {
response,
bits,
resource,
})
}
pub struct AsyncMintChallengeResponse {
bits: Option<u32>,
resource: String,
}
#[napi]
impl Task for AsyncMintChallengeResponse {
type Output = String;
type JsValue = JsString;
fn compute(&mut self) -> NapiResult<Self::Output> {
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
}
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
env.create_string(&output)
}
}
#[napi]
pub fn mint_challenge_response(
resource: String,
bits: Option<u32>,
) -> AsyncTask<AsyncMintChallengeResponse> {
AsyncTask::new(AsyncMintChallengeResponse { bits, resource })
}

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
provider = "postgresql"

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.17.0",
"version": "0.18.0",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -12,9 +12,9 @@
"start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts",
"dev": "nodemon ./src/index.ts",
"test": "ava --concurrency 1 --serial",
"test:copilot": "ava --concurrency 1 --serial \"tests/**/copilot-*.e2e.ts\"",
"test:copilot": "ava \"tests/**/copilot-*.spec.ts\"",
"test:coverage": "c8 ava --concurrency 1 --serial",
"test:copilot:coverage": "c8 ava --timeout=5m --concurrency 1 --serial \"tests/**/copilot-*.e2e.ts\"",
"test:copilot:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.spec.ts\"",
"postinstall": "prisma generate",
"data-migration": "node --loader ts-node/esm/transpile-only.mjs ./src/data/index.ts",
"predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run",

View File

@@ -147,9 +147,11 @@ export class SelfhostModule implements OnModuleInit {
// fallback all unknown routes
app.get([basePath, basePath + '/*'], this.check.use, (req, res) => {
const mobile = isMobile({
ua: req.headers['user-agent'] ?? undefined,
});
const mobile =
this.config.AFFINE_ENV === 'dev' &&
isMobile({
ua: req.headers['user-agent'] ?? undefined,
});
return res.sendFile(
join(

View File

@@ -1,9 +1,18 @@
import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config';
import {
defineRuntimeConfig,
defineStartupConfig,
ModuleConfig,
} from '../../fundamentals/config';
import { CaptchaConfig } from './types';
declare module '../config' {
interface PluginsConfig {
captcha: ModuleConfig<CaptchaConfig>;
captcha: ModuleConfig<
CaptchaConfig,
{
enable: boolean;
}
>;
}
}
@@ -21,3 +30,10 @@ defineStartupConfig('plugins.captcha', {
bits: 20,
},
});
defineRuntimeConfig('plugins.captcha', {
enable: {
desc: 'Check captcha challenge when user authenticating the app.',
default: false,
},
});

View File

@@ -6,6 +6,7 @@ import type {
import { Injectable } from '@nestjs/common';
import {
Config,
getRequestResponseFromContext,
GuardProvider,
} from '../../fundamentals';
@@ -18,11 +19,18 @@ export class CaptchaGuardProvider
{
name = 'captcha' as const;
constructor(private readonly captcha: CaptchaService) {
constructor(
private readonly captcha: CaptchaService,
private readonly config: Config
) {
super();
}
async canActivate(context: ExecutionContext) {
if (!(await this.config.runtime.fetch('plugins.captcha/enable'))) {
return true;
}
const { req } = getRequestResponseFromContext(context);
// require headers, old client send through query string

View File

@@ -1,3 +1,5 @@
import './config';
import { AuthModule } from '../../core/auth';
import { ServerFeature } from '../../core/config';
import { Plugin } from '../registry';

View File

@@ -88,15 +88,14 @@ export class CaptchaService {
async verifyRequest(credential: Credential, req: Request) {
const challenge = credential.challenge;
let resource: string | null = null;
if (typeof challenge === 'string' && challenge) {
const resource = await this.token
resource = await this.token
.getToken(TokenType.Challenge, challenge)
.then(token => token?.credential);
if (!resource) {
throw new CaptchaVerificationFailed('Invalid Challenge');
}
.then(token => token?.credential || null);
}
if (resource) {
const isChallengeVerified = await this.verifyChallengeResponse(
credential.token,
resource

View File

@@ -63,7 +63,7 @@ const runIfCopilotConfigured = test.macro(
}
);
test.beforeEach(async t => {
test.serial.before(async t => {
const module = await createTestingModule({
imports: [
ConfigModule.forRoot({
@@ -101,7 +101,7 @@ test.beforeEach(async t => {
};
});
test.beforeEach(async t => {
test.serial.before(async t => {
const { prompt, executors } = t.context;
executors.image.register();
@@ -121,12 +121,12 @@ test.beforeEach(async t => {
}
});
test.afterEach.always(async _ => {
test.after(async _ => {
unregisterCopilotProvider(OpenAIProvider.type);
unregisterCopilotProvider(FalProvider.type);
});
test.afterEach.always(async t => {
test.after(async t => {
await t.context.module.close();
});
@@ -143,7 +143,7 @@ const assertNotWrappedInCodeBlock = (
const checkMDList = (text: string) => {
const lines = text.split('\n');
const listItemRegex = /^( {2})*(-|\*|\+) .+$/;
const listItemRegex = /^( {2})*(-|\u2010-\u2015|\*|\+)? .+$/;
let prevIndent = null;
for (const line of lines) {
@@ -166,7 +166,9 @@ const checkMDList = (text: string) => {
}
}
prevIndent = currentIndent;
if (line.trim().startsWith('-')) {
prevIndent = currentIndent;
}
}
return true;
@@ -190,14 +192,14 @@ const retry = async (
while (i--) {
const ret = await t.try(callback);
if (ret.passed) {
ret.commit();
break;
return ret.commit();
} else {
ret.discard();
t.log(ret.errors.map(e => e.message).join('\n'));
t.log(`retrying ${action} ${3 - i}/3 ...`);
}
}
t.fail(`failed to run ${action}`);
};
// ==================== utils ====================
@@ -248,6 +250,16 @@ test('should validate markdown list', t => {
- item 1.1.1.1
`)
);
t.true(
checkMDList(`
- item 1
- item 1.1
- item 1.1.1.1
item 1.1.1.1 line breaks
- item 1.1.1.2
`),
'should allow line breaks'
);
});
// ==================== action ====================
@@ -447,14 +459,14 @@ const workflows = [
{
name: 'brainstorm',
content: 'apple company',
verifier: (t: ExecutionContext<Tester>, result: string) => {
verifier: (t: ExecutionContext, result: string) => {
t.assert(checkMDList(result), 'should be a markdown list');
},
},
{
name: 'presentation',
content: 'apple company',
verifier: (t: ExecutionContext<Tester>, result: string) => {
verifier: (t: ExecutionContext, result: string) => {
for (const l of result.split('\n')) {
t.notThrows(() => {
JSON.parse(l.trim());
@@ -475,11 +487,11 @@ for (const { name, content, verifier } of workflows) {
let result = '';
for await (const ret of workflow.runGraph({ content }, name)) {
if (ret.status === GraphExecutorState.EnterNode) {
console.log('enter node:', ret.node.name);
t.log('enter node:', ret.node.name);
} else if (ret.status === GraphExecutorState.ExitNode) {
console.log('exit node:', ret.node.name);
t.log('exit node:', ret.node.name);
} else if (ret.status === GraphExecutorState.EmitAttachment) {
console.log('stream attachment:', ret);
t.log('stream attachment:', ret);
} else {
result += ret.content;
}

View File

@@ -9,5 +9,5 @@
"@types/debug": "^4.1.12",
"vitest": "2.1.4"
},
"version": "0.17.0"
"version": "0.18.0"
}

View File

@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"devDependencies": {
"@blocksuite/affine": "0.17.32",
"@blocksuite/affine": "0.17.33",
"vitest": "2.1.4"
},
"exports": {
@@ -21,5 +21,5 @@
"dependencies": {
"zod": "^3.22.4"
},
"version": "0.17.0"
"version": "0.18.0"
}

View File

@@ -15,7 +15,7 @@
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/templates": "workspace:*",
"@blocksuite/affine": "0.17.32",
"@blocksuite/affine": "0.17.33",
"@datastructures-js/binary-search-tree": "^5.3.2",
"eventemitter2": "^6.4.9",
"foxact": "^0.2.33",
@@ -60,5 +60,5 @@
"optional": true
}
},
"version": "0.17.0"
"version": "0.18.0"
}

View File

@@ -1,3 +1,4 @@
import { DebugLogger } from '@affine/debug';
import { Unreachable } from '@affine/env/constant';
import {
type AffineTextAttributes,
@@ -16,6 +17,8 @@ import { DocScope } from '../scopes/doc';
import type { DocsStore } from '../stores/docs';
import { DocService } from './doc';
const logger = new DebugLogger('DocsService');
export class DocsService extends Service {
list = this.framework.createEntity(DocRecordList);
@@ -52,6 +55,15 @@ export class DocsService extends Service {
record: docRecord,
});
try {
blockSuiteDoc.load();
} catch (e) {
logger.error('Failed to load doc', {
docId,
error: e,
});
}
const doc = docScope.get(DocService).doc;
const { obj, release } = this.pool.put(docId, doc);

View File

@@ -206,16 +206,6 @@ export const AFFINE_FLAGS = {
configurable: false,
defaultState: isMobile,
},
enable_snapshot_import_export: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-snapshot-import-export.name',
description:
'com.affine.settings.workspace.experimental-features.enable-snapshot-import-export.description',
hide: true,
configurable: true,
defaultState: false,
},
} satisfies { [key in string]: FlagInfo };
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;

View File

@@ -0,0 +1,12 @@
[package]
edition = "2021"
name = "affine_common"
version = "0.1.0"
[dependencies]
chrono = { workspace = true }
rand = { workspace = true }
sha3 = { workspace = true }
[dev-dependencies]
rayon = { workspace = true }

View File

@@ -0,0 +1,204 @@
use std::convert::TryFrom;
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use rand::{
distributions::{Alphanumeric, Distribution},
thread_rng,
};
use sha3::{Digest, Sha3_256};
const SALT_LENGTH: usize = 16;
#[derive(Debug)]
pub struct Stamp {
version: String,
claim: u32,
ts: String,
resource: String,
ext: String,
rand: String,
counter: String,
}
impl Stamp {
fn check_expiration(&self) -> bool {
NaiveDateTime::parse_from_str(&self.ts, "%Y%m%d%H%M%S")
.ok()
.map(|ts| DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc))
.and_then(|utc| {
utc
.checked_add_signed(Duration::minutes(5))
.map(|utc| Utc::now() <= utc)
})
.unwrap_or(false)
}
pub fn check<S: AsRef<str>>(&self, bits: u32, resource: S) -> bool {
if self.version == "1"
&& bits <= self.claim
&& self.check_expiration()
&& self.resource == resource.as_ref()
{
let hex_digits = ((self.claim as f32) / 4.).floor() as usize;
// check challenge
let mut hasher = Sha3_256::new();
hasher.update(self.format().as_bytes());
let result = format!("{:x}", hasher.finalize());
result[..hex_digits] == String::from_utf8(vec![b'0'; hex_digits]).unwrap()
} else {
false
}
}
pub fn format(&self) -> String {
format!(
"{}:{}:{}:{}:{}:{}:{}",
self.version, self.claim, self.ts, self.resource, self.ext, self.rand, self.counter
)
}
/// Mint a new hashcash stamp.
pub fn mint(resource: String, bits: Option<u32>) -> Self {
let version = "1";
let now = Utc::now();
let ts = now.format("%Y%m%d%H%M%S");
let bits = bits.unwrap_or(20);
let rand = String::from_iter(
Alphanumeric
.sample_iter(thread_rng())
.take(SALT_LENGTH)
.map(char::from),
);
let challenge = format!("{}:{}:{}:{}:{}:{}", version, bits, ts, &resource, "", rand);
Stamp {
version: version.to_string(),
claim: bits,
ts: ts.to_string(),
resource,
ext: "".to_string(),
rand,
counter: {
let mut hasher = Sha3_256::new();
let mut counter = 0;
let hex_digits = ((bits as f32) / 4.).ceil() as usize;
let zeros = String::from_utf8(vec![b'0'; hex_digits]).unwrap();
loop {
hasher.update(format!("{}:{:x}", challenge, counter).as_bytes());
let result = format!("{:x}", hasher.finalize_reset());
if result[..hex_digits] == zeros {
break format!("{:x}", counter);
};
counter += 1
}
},
}
}
}
impl TryFrom<&str> for Stamp {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let stamp_vec = value.split(':').collect::<Vec<&str>>();
if stamp_vec.len() != 7
|| stamp_vec
.iter()
.enumerate()
.any(|(i, s)| i != 4 && s.is_empty())
{
return Err(format!(
"Malformed stamp, expected 6 parts, got {}",
stamp_vec.len()
));
}
Ok(Stamp {
version: stamp_vec[0].to_string(),
claim: stamp_vec[1]
.parse()
.map_err(|_| "Malformed stamp".to_string())?,
ts: stamp_vec[2].to_string(),
resource: stamp_vec[3].to_string(),
ext: stamp_vec[4].to_string(),
rand: stamp_vec[5].to_string(),
counter: stamp_vec[6].to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::Stamp;
use rand::{distributions::Alphanumeric, Rng};
use rayon::prelude::*;
#[test]
fn test_mint() {
{
let response = Stamp::mint("test".into(), Some(20)).format();
assert!(
Stamp::try_from(response.as_str())
.unwrap()
.check(20, "test"),
"should pass"
);
}
{
let response = Stamp::mint("test".into(), Some(19)).format();
assert!(
!Stamp::try_from(response.as_str())
.unwrap()
.check(20, "test"),
"should fail with lower bits"
);
}
{
let response = Stamp::mint("test".into(), Some(20)).format();
assert!(
!Stamp::try_from(response.as_str())
.unwrap()
.check(20, "test2"),
"should fail with different resource"
);
}
}
#[test]
fn test_check_expiration() {
let response = Stamp::mint("test".into(), Some(20));
assert!(response.check_expiration());
}
#[test]
fn test_format() {
let response = Stamp::mint("test".into(), Some(20));
assert_eq!(
response.format(),
format!(
"1:20:{}:test::{}:{}",
response.ts, response.rand, response.counter
)
);
}
#[test]
fn test_fuzz() {
(0..1000).into_par_iter().for_each(|_| {
let bit = rand::random::<u32>() % 20 + 1;
let resource = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>();
let response = Stamp::mint(resource.clone(), Some(bit)).format();
assert!(
Stamp::try_from(response.as_str())
.unwrap()
.check(bit, resource),
"should pass"
);
});
}
}

View File

@@ -0,0 +1 @@
pub mod hashcash;

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/admin",
"version": "0.17.0",
"version": "0.18.0",
"private": true,
"dependencies": {
"@affine/core": "workspace:*",

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/android",
"version": "0.17.0",
"version": "0.18.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"browser": "src/index.tsx",
@@ -13,8 +13,8 @@
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/affine": "0.17.32",
"@blocksuite/icons": "^2.1.69",
"@blocksuite/affine": "0.17.33",
"@blocksuite/icons": "^2.1.70",
"@capacitor/android": "^6.1.2",
"@capacitor/core": "^6.1.2",
"@sentry/react": "^8.0.0",

View File

@@ -1,5 +1,5 @@
import { AffineContext } from '@affine/core/components/context';
import { AppFallback } from '@affine/core/mobile/components';
import { AppContainer } from '@affine/core/desktop/components/app-container';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { router } from '@affine/core/mobile/router';
import { configureCommonModules } from '@affine/core/modules';
@@ -47,7 +47,7 @@ export function App() {
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<RouterProvider
fallbackElement={<AppFallback />}
fallbackElement={<AppContainer fallback />}
router={router}
future={future}
/>

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.17.0",
"version": "0.18.0",
"author": "toeverything",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -28,7 +28,7 @@
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/affine": "0.17.32",
"@blocksuite/affine": "0.17.33",
"@electron-forge/cli": "^7.3.0",
"@electron-forge/core": "^7.3.0",
"@electron-forge/core-utils": "^7.3.0",

View File

@@ -0,0 +1,20 @@
import { app } from 'electron';
import { logger } from './logger';
const cleanupRegistry: (() => void)[] = [];
export function beforeAppQuit(fn: () => void) {
cleanupRegistry.push(fn);
}
app.on('before-quit', () => {
cleanupRegistry.forEach(fn => {
// some cleanup functions might throw on quit and crash the app
try {
fn();
} catch (err) {
logger.warn('cleanup error on quit', err);
}
});
});

View File

@@ -1,7 +1,8 @@
import { app, BrowserWindow, WebContentsView } from 'electron';
import { BrowserWindow, WebContentsView } from 'electron';
import { AFFINE_EVENT_CHANNEL_NAME } from '../shared/type';
import { applicationMenuEvents } from './application-menu';
import { beforeAppQuit } from './cleanup';
import { logger } from './logger';
import { sharedStorageEvents } from './shared-storage';
import { uiEvents } from './ui/events';
@@ -56,14 +57,10 @@ export function registerEvents() {
unsubs.push(unsubscribe);
}
}
app.on('before-quit', () => {
// subscription on quit sometimes crashes the app
unsubs.forEach(unsub => {
try {
unsub();
} catch (err) {
logger.warn('unsubscribe error on quit', err);
}
unsubs.forEach(unsub => {
beforeAppQuit(() => {
unsub();
});
});
}

View File

@@ -13,6 +13,7 @@ import {
import type { HelperToMain, MainToHelper } from '../shared/type';
import { MessageEventChannel } from '../shared/utils';
import { beforeAppQuit } from './cleanup';
import { logger } from './logger';
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
@@ -65,7 +66,7 @@ class HelperProcessManager {
});
});
app.on('before-quit', () => {
beforeAppQuit(() => {
this.#process.kill();
});
}
@@ -78,8 +79,12 @@ class HelperProcessManager {
renderer.postMessage('helper-connection', null, [rendererPort]);
return () => {
helperPort.close();
rendererPort.close();
try {
helperPort.close();
rendererPort.close();
} catch (err) {
logger.error('[helper] close port error', err);
}
};
}

View File

@@ -1,6 +1,7 @@
import { app, nativeTheme, shell } from 'electron';
import { getLinkPreview } from 'link-preview-js';
import { isMacOS } from '../../shared/utils';
import { persistentConfig } from '../config-storage/persist';
import { logger } from '../logger';
import type { WorkbenchViewMeta } from '../shared-state-schema';
@@ -220,4 +221,15 @@ export const uiHandlers = {
app.relaunch();
app.quit();
},
onLanguageChange: async (e, language: string) => {
// only works for win/linux
// see https://www.electronjs.org/docs/latest/tutorial/spellchecker#how-to-set-the-languages-the-spellchecker-uses
if (isMacOS()) {
return;
}
if (e.sender.session.availableSpellCheckerLanguages.includes(language)) {
e.sender.session.setSpellCheckerLanguages([language, 'en-US']);
}
},
} satisfies NamespaceHandlers;

View File

@@ -5,6 +5,7 @@ import electronWindowState from 'electron-window-state';
import { BehaviorSubject } from 'rxjs';
import { isLinux, isMacOS, isWindows } from '../../shared/utils';
import { beforeAppQuit } from '../cleanup';
import { buildType } from '../config';
import { mainWindowOrigin } from '../constants';
import { ensureHelperProcess } from '../helper-process';
@@ -116,11 +117,17 @@ export class MainWindowManager {
uiSubjects.onFullScreen$.next(mainWindow.isFullScreen());
});
beforeAppQuit(() => {
this.cleanupWindows();
});
mainWindow.on('close', e => {
// TODO(@pengx17): gracefully close the app, for example, ask user to save unsaved changes
e.preventDefault();
if (!isMacOS()) {
closeAllWindows();
this.mainWindowReady = undefined;
this.mainWindow$.next(undefined);
} else {
// hide window on macOS
// application quit will be handled by closing the hidden window

View File

@@ -25,6 +25,7 @@ import {
} from 'rxjs';
import { isMacOS } from '../../shared/utils';
import { beforeAppQuit } from '../cleanup';
import { CLOUD_BASE_URL, isDev } from '../config';
import { mainWindowOrigin, shellViewUrl } from '../constants';
import { ensureHelperProcess } from '../helper-process';
@@ -749,8 +750,10 @@ export class WebContentViewsManager {
})
);
app.on('before-quit', () => {
disposables.forEach(d => d.unsubscribe());
disposables.forEach(d => {
beforeAppQuit(() => {
d.unsubscribe();
});
});
const focusActiveView = () => {

View File

@@ -157,7 +157,11 @@ const createMessagePortChannel = (port: MessagePort): EventBasedChannel => {
port.start();
return () => {
port.onmessage = null;
port.close();
try {
port.close();
} catch (err) {
console.error('[helper] close port error', err);
}
};
},
send(data) {

View File

@@ -44,4 +44,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698
COCOAPODS: 1.16.1
COCOAPODS: 1.16.2

View File

@@ -4,7 +4,17 @@ AFFiNE iOS app.
## Build
- yarn install
- BUILD_TYPE=canary PUBLIC_PATH="/" yarn workspace @affine/ios build
- yarn workspace @affine/ios cap sync
- yarn workspace @affine/ios cap open ios
- `yarn install`
- `BUILD_TYPE=canary PUBLIC_PATH="/" yarn workspace @affine/ios build`
- `yarn workspace @affine/ios cap sync`
- `yarn workspace @affine/ios cap open ios`
## Live Reload
> Capacitor doc: https://capacitorjs.com/docs/guides/live-reload#using-with-framework-clis
- `yarn install`
- `yarn dev`
- select `ios` for the "Distribution" option
- `yarn workspace @affine/ios sync:dev`
- `yarn workspace @affine/ios cap open ios`

View File

@@ -24,4 +24,12 @@ const config: CapacitorConfig = {
},
};
if (process.env.CAP_SERVER_URL) {
Object.assign(config, {
server: {
url: process.env.CAP_SERVER_URL,
},
});
}
export default config;

View File

@@ -1,20 +1,22 @@
{
"name": "@affine/ios",
"version": "0.17.0",
"version": "0.18.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"browser": "src/index.tsx",
"scripts": {
"build": "cross-env DISTRIBUTION=ios yarn workspace @affine/cli bundle",
"dev": "yarn workspace @affine/cli dev",
"sync": "yarn cap sync",
"sync:dev": "CAP_SERVER_URL=http://localhost:8080 yarn cap sync",
"static-server": "cross-env DISTRIBUTION=ios yarn workspace @affine/cli dev --static"
},
"dependencies": {
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/affine": "0.17.32",
"@blocksuite/icons": "^2.1.69",
"@blocksuite/affine": "0.17.33",
"@blocksuite/icons": "^2.1.70",
"@capacitor/app": "^6.0.1",
"@capacitor/browser": "^6.0.3",
"@capacitor/core": "^6.1.2",

View File

@@ -1,6 +1,7 @@
import { AffineContext } from '@affine/core/components/context';
import { AppFallback } from '@affine/core/mobile/components';
import { AppContainer } from '@affine/core/desktop/components/app-container';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
import { router } from '@affine/core/mobile/router';
import { configureCommonModules } from '@affine/core/modules';
import {
@@ -20,6 +21,7 @@ import {
} from '@affine/core/modules/workspace-engine';
import { App as CapacitorApp } from '@capacitor/app';
import { Browser } from '@capacitor/browser';
import { Keyboard } from '@capacitor/keyboard';
import {
Framework,
FrameworkRoot,
@@ -76,6 +78,14 @@ framework.impl(ValidatorProvider, {
return res.value;
},
});
framework.impl(VirtualKeyboardProvider, {
addEventListener: (event, callback) => {
Keyboard.addListener(event as any, callback as any);
},
removeAllListeners: () => {
Keyboard.removeAllListeners();
},
});
const frameworkProvider = framework.provider();
// setup application lifecycle events, and emit application start event
@@ -123,7 +133,7 @@ export function App() {
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<RouterProvider
fallbackElement={<AppFallback />}
fallbackElement={<AppContainer fallback />}
router={router}
future={future}
/>

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/mobile",
"version": "0.17.0",
"version": "0.18.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"browser": "src/index.tsx",
@@ -13,8 +13,8 @@
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/affine": "0.17.32",
"@blocksuite/icons": "^2.1.69",
"@blocksuite/affine": "0.17.33",
"@blocksuite/icons": "^2.1.70",
"@sentry/react": "^8.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@@ -1,5 +1,5 @@
import { AffineContext } from '@affine/core/components/context';
import { AppFallback } from '@affine/core/mobile/components';
import { AppContainer } from '@affine/core/desktop/components/app-container';
import { configureMobileModules } from '@affine/core/mobile/modules';
import { router } from '@affine/core/mobile/router';
import { configureCommonModules } from '@affine/core/modules';
@@ -67,7 +67,7 @@ export function App() {
<I18nProvider>
<AffineContext store={getCurrentStore()}>
<RouterProvider
fallbackElement={<AppFallback />}
fallbackElement={<AppContainer fallback />}
router={router}
future={future}
/>

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/web",
"version": "0.17.0",
"version": "0.18.0",
"description": "AFFiNE Desktop Web application",
"private": true,
"browser": "src/index.tsx",

View File

@@ -14,7 +14,7 @@
},
"peerDependencies": {
"@blocksuite/affine": "*",
"@blocksuite/icons": "2.1.67"
"@blocksuite/icons": "2.1.68"
},
"dependencies": {
"@affine/cli": "workspace:*",
@@ -24,7 +24,7 @@
"@affine/i18n": "workspace:*",
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blocksuite/icons": "2.1.69",
"@blocksuite/icons": "2.1.70",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@radix-ui/react-avatar": "^1.0.4",
@@ -41,7 +41,7 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@toeverything/theme": "^1.0.17",
"@toeverything/theme": "^1.0.18",
"@vanilla-extract/dynamic": "^2.1.0",
"check-password-strength": "^2.0.10",
"clsx": "^2.1.0",
@@ -63,8 +63,8 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@blocksuite/affine": "0.17.32",
"@blocksuite/icons": "2.1.69",
"@blocksuite/affine": "0.17.33",
"@blocksuite/icons": "2.1.70",
"@chromatic-com/storybook": "^3.0.0",
"@storybook/addon-essentials": "^8.2.9",
"@storybook/addon-interactions": "^8.2.9",
@@ -82,5 +82,5 @@
"vite": "^5.2.8",
"vitest": "2.1.4"
},
"version": "0.17.0"
"version": "0.18.0"
}

View File

@@ -9,10 +9,6 @@ export const useNavConfig = () => {
title: t['com.affine.other-page.nav.official-website'](),
path: 'https://affine.pro',
},
{
title: t['com.affine.other-page.nav.affine-community'](),
path: 'https://community.affine.pro/home',
},
{
title: t['com.affine.other-page.nav.blog'](),
path: 'https://affine.pro/blog',

View File

@@ -21,7 +21,7 @@ export const useAutoFocus = <T extends HTMLElement = HTMLElement>(
export const useAutoSelect = <T extends HTMLInputElement = HTMLInputElement>(
autoSelect?: boolean
) => {
const ref = useAutoFocus<T>(autoSelect);
const ref = useRef<T | null>(null);
useLayoutEffect(() => {
if (ref.current && autoSelect) {

View File

@@ -45,17 +45,20 @@ export const RowInput = forwardRef<HTMLInputElement, RowInputProps>(
const focusRef = useAutoFocus<HTMLInputElement>(autoFocus);
const selectRef = useAutoSelect<HTMLInputElement>(autoSelect);
const inputRef = (el: HTMLInputElement | null) => {
focusRef.current = el;
selectRef.current = el;
if (upstreamRef) {
if (typeof upstreamRef === 'function') {
upstreamRef(el);
} else {
upstreamRef.current = el;
const inputRef = useCallback(
(el: HTMLInputElement | null) => {
focusRef.current = el;
selectRef.current = el;
if (upstreamRef) {
if (typeof upstreamRef === 'function') {
upstreamRef(el);
} else {
upstreamRef.current = el;
}
}
}
};
},
[focusRef, selectRef, upstreamRef]
);
// use native blur event to get event after unmount
// don't use useLayoutEffect here, because the cleanup function will be called before unmount

View File

@@ -68,7 +68,7 @@ export const modalOverlay = style({
animation: 'none',
},
'&.mobile': {
backgroundColor: cssVarV2('layer/mobile/modal'),
backgroundColor: cssVarV2('layer/background/mobile/modal'),
},
},
});

View File

@@ -2,7 +2,7 @@
"name": "@affine/core",
"type": "module",
"private": true,
"version": "0.17.0",
"version": "0.18.0",
"exports": {
"./*": "./src/*",
"./bootstrap": "./src/bootstrap/index.ts"
@@ -16,8 +16,8 @@
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/track": "workspace:*",
"@blocksuite/affine": "0.17.32",
"@blocksuite/icons": "2.1.69",
"@blocksuite/affine": "0.17.33",
"@blocksuite/icons": "2.1.70",
"@capacitor/app": "^6.0.1",
"@capacitor/browser": "^6.0.3",
"@dnd-kit/core": "^6.1.0",
@@ -37,7 +37,7 @@
"@radix-ui/react-toolbar": "^1.0.4",
"@sentry/react": "^8.0.0",
"@toeverything/pdf-viewer": "^0.1.1",
"@toeverything/theme": "^1.0.17",
"@toeverything/theme": "^1.0.18",
"@vanilla-extract/dynamic": "^2.1.0",
"animejs": "^3.2.2",
"bytes": "^3.1.2",

View File

@@ -1,4 +1,5 @@
import { polyfillDispose } from './dispose';
import { polyfillIteratorHelpers } from './iterator-helpers';
import { polyfillPromise } from './promise-with-resolvers';
import { polyfillEventLoop } from './request-idle-callback';
import { polyfillResizeObserver } from './resize-observer';
@@ -7,3 +8,4 @@ polyfillResizeObserver();
polyfillEventLoop();
await polyfillPromise();
await polyfillDispose();
await polyfillIteratorHelpers();

View File

@@ -0,0 +1,7 @@
export async function polyfillIteratorHelpers() {
if (typeof globalThis['Iterator'] !== 'function') {
// @ts-expect-error ignore
// https://github.com/zloirock/core-js/blob/master/packages/core-js/proposals/iterator-helpers-stage-3.js
await import('core-js/proposals/iterator-helpers-stage-3');
}
}

View File

@@ -2,6 +2,7 @@ import { Skeleton } from '@affine/component';
import { Button } from '@affine/component/ui/button';
import { UrlService } from '@affine/core/modules/url';
import { OAuthProviderType } from '@affine/graphql';
import track from '@affine/track';
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { type ReactElement, useCallback } from 'react';
@@ -86,6 +87,8 @@ function OAuthProvider({
const oauthUrl =
BUILD_CONFIG.serverUrlPrefix + `/oauth/login?${params.toString()}`;
track.$.$.auth.signIn({ method: 'oauth', provider });
popupWindow(oauthUrl);
}, [popupWindow, provider, redirectUrl, scheme]);

View File

@@ -255,9 +255,7 @@ export function setupTracker() {
});
AIProvider.slots.requestLogin.on(() => {
track.$.$.auth.signIn({
control: 'aiAction',
});
track.doc.editor.aiActions.requestSignIn();
});
AIProvider.slots.actions.on(event => {

View File

@@ -32,10 +32,6 @@ export type EditorProps = {
};
function usePageRoot(page: Doc) {
if (!page.ready) {
page.load();
}
if (!page.root) {
use(
new Promise<void>((resolve, reject) => {

View File

@@ -40,6 +40,7 @@ import type {
PeekOptions,
PeekViewService as BSPeekViewService,
QuickSearchResult,
ReferenceNodeConfig,
RootBlockConfig,
RootService,
} from '@blocksuite/affine/blocks';
@@ -57,6 +58,7 @@ import {
PeekViewExtension,
QuickSearchExtension,
ReferenceNodeConfigExtension,
ReferenceNodeConfigIdentifier,
} from '@blocksuite/affine/blocks';
import { type BlockSnapshot, Text } from '@blocksuite/affine/store';
import {
@@ -167,10 +169,10 @@ export function patchNotificationService({
<div>
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
<Input
autoSelect={true}
placeholder={placeholder}
defaultValue={value}
onChange={e => (value = e)}
ref={input => input?.select()}
/>
</div>
);
@@ -188,6 +190,7 @@ export function patchNotificationService({
onCancel: () => {
resolve(null);
},
autoFocusConfirm: false,
});
abort?.addEventListener('abort', () => {
resolve(null);
@@ -567,6 +570,17 @@ export function patchForMobile() {
});
}
// Hide reference popup on mobile.
{
const prev = di.getFactory(ReferenceNodeConfigIdentifier);
di.override(ReferenceNodeConfigIdentifier, provider => {
return {
...prev?.(provider),
hidePopup: true,
} satisfies ReferenceNodeConfig;
});
}
// Disable some toolbar widgets for mobile.
{
di.override(WidgetViewMapIdentifier('affine:page'), () => {

View File

@@ -11,11 +11,7 @@ import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/us
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
import {
Export,
MoveToTrash,
Snapshot,
} from '@affine/core/components/page-list';
import { Export, MoveToTrash } from '@affine/core/components/page-list';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
@@ -44,7 +40,6 @@ import {
TocIcon,
} from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useLiveData,
useService,
useServiceOptional,
@@ -84,10 +79,6 @@ export const PageHeaderMenuButton = ({
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
const workbench = useService(WorkbenchService).workbench;
const featureFlagService = useService(FeatureFlagService);
const enableSnapshotImportExport = useLiveData(
featureFlagService.flags.enable_snapshot_import_export.$
);
const openInAppService = useServiceOptional(OpenInAppService);
const { favorite, toggleFavorite } = useFavorite(pageId);
@@ -402,7 +393,6 @@ export const PageHeaderMenuButton = ({
{t['Import']()}
</MenuItem>
<Export exportHandler={exportHandler} pageMode={currentMode} />
{enableSnapshotImportExport && <Snapshot />}
<MenuSeparator />
<MoveToTrash
data-testid="editor-option-menu-delete"

View File

@@ -131,11 +131,13 @@ export const DocPropertyRow = ({
typeInfo && 'value' in typeInfo ? typeInfo.value : undefined;
const handleChange = useCallback(
(value: any) => {
if (typeof value !== 'string') {
throw new Error('only allow string value');
(value: any, skipCommit?: boolean) => {
if (!skipCommit) {
if (typeof value !== 'string') {
throw new Error('only allow string value');
}
docService.doc.record.setCustomProperty(propertyInfo.id, value);
}
docService.doc.record.setCustomProperty(propertyInfo.id, value);
onChange?.(value);
},
[docService, onChange, propertyInfo]

View File

@@ -1,6 +1,5 @@
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { TagsIcon } from '@blocksuite/icons/rc';
import {
LiveData,
@@ -26,6 +25,7 @@ interface TagsEditorProps {
interface TagsInlineEditorProps extends TagsEditorProps {
placeholder?: string;
className?: string;
onChange?: (value: unknown) => void;
}
export const TagsInlineEditor = ({
@@ -33,19 +33,18 @@ export const TagsInlineEditor = ({
readonly,
placeholder,
className,
onChange,
}: TagsInlineEditorProps) => {
const workspace = useService(WorkspaceService);
const tagService = useService(TagService);
const tagIds = useLiveData(tagService.tagList.tagIdsByPageId$(pageId));
const tagIds$ = tagService.tagList.tagIdsByPageId$(pageId);
const tagIds = useLiveData(tagIds$);
const tags = useLiveData(tagService.tagList.tags$);
const tagColors = tagService.tagColors;
const onCreateTag = useCallback(
(name: string, color: string) => {
const newTag = tagService.tagList.createTag(name, color);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
return {
id: newTag.id,
value: newTag.value$.value,
@@ -58,21 +57,17 @@ export const TagsInlineEditor = ({
const onSelectTag = useCallback(
(tagId: string) => {
tagService.tagList.tagByTagId$(tagId).value?.tag(pageId);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
onChange?.(tagIds$.value);
},
[pageId, tagService.tagList]
[onChange, pageId, tagIds$, tagService.tagList]
);
const onDeselectTag = useCallback(
(tagId: string) => {
tagService.tagList.tagByTagId$(tagId).value?.untag(pageId);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
onChange?.(tagIds$.value);
},
[pageId, tagService.tagList]
[onChange, pageId, tagIds$, tagService.tagList]
);
const onTagChange = useCallback(
@@ -82,11 +77,9 @@ export const TagsInlineEditor = ({
} else if (property === 'color') {
tagService.tagList.tagByTagId$(id).value?.changeColor(value);
}
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
onChange?.(tagIds$.value);
},
[tagService.tagList]
[onChange, tagIds$, tagService.tagList]
);
const deleteTags = useDeleteTagConfirmModal();
@@ -94,11 +87,9 @@ export const TagsInlineEditor = ({
const onTagDelete = useAsyncCallback(
async (id: string) => {
await deleteTags([id]);
track.doc.inlineDocInfo.property.editProperty({
type: 'tags',
});
onChange?.(tagIds$.value);
},
[deleteTags]
[onChange, tagIds$, deleteTags]
);
const adaptedTags = useLiveData(

View File

@@ -10,8 +10,9 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import * as styles from './doc-primary-mode.css';
import type { PropertyValueProps } from './types';
export const DocPrimaryModeValue = () => {
export const DocPrimaryModeValue = ({ onChange }: PropertyValueProps) => {
const t = useI18n();
const doc = useService(DocService).doc;
@@ -44,8 +45,9 @@ export const DocPrimaryModeValue = () => {
? t['com.affine.toastMessage.defaultMode.page.message']()
: t['com.affine.toastMessage.defaultMode.edgeless.message'](),
});
onChange?.(mode, true);
},
[doc, t]
[doc, t, onChange]
);
return (
<PropertyValue className={styles.container} hoverable={false}>

View File

@@ -4,6 +4,7 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import * as styles from './edgeless-theme.css';
import type { PropertyValueProps } from './types';
const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
[
@@ -21,7 +22,7 @@ const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
},
] satisfies RadioItem[];
export const EdgelessThemeValue = () => {
export const EdgelessThemeValue = ({ onChange }: PropertyValueProps) => {
const t = useI18n();
const doc = useService(DocService).doc;
const edgelessTheme = useLiveData(doc.properties$).edgelessColorTheme;
@@ -29,8 +30,9 @@ export const EdgelessThemeValue = () => {
const handleChange = useCallback(
(theme: string) => {
doc.record.setProperty('edgelessColorTheme', theme);
onChange?.(theme, true);
},
[doc]
[doc, onChange]
);
const themeItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);

View File

@@ -13,8 +13,9 @@ import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as styles from './journal.css';
import type { PropertyValueProps } from './types';
export const JournalValue = () => {
export const JournalValue = ({ onChange }: PropertyValueProps) => {
const t = useI18n();
const journalService = useService(JournalService);
@@ -50,8 +51,9 @@ export const JournalValue = () => {
const date = dayjs(day).format('YYYY-MM-DD');
setSelectedDate(date);
journalService.setJournalDate(doc.id, date);
onChange?.(date, true);
},
[journalService, doc.id]
[journalService, doc.id, onChange]
);
const handleCheck = useCallback(
@@ -59,11 +61,12 @@ export const JournalValue = () => {
if (!v) {
journalService.removeJournalDate(doc.id);
setShowDatePicker(false);
onChange?.(null, true);
} else {
handleDateSelect(selectedDate);
}
},
[handleDateSelect, journalService, doc.id, selectedDate]
[onChange, journalService, doc.id, handleDateSelect, selectedDate]
);
const workbench = useService(WorkbenchService).workbench;
@@ -101,11 +104,7 @@ export const JournalValue = () => {
onClick={toggle}
>
<div className={styles.root}>
<Checkbox
className={styles.checkbox}
checked={checked}
onChange={handleCheck}
/>
<Checkbox className={styles.checkbox} checked={checked} />
{checked ? (
<Menu
contentOptions={{

View File

@@ -5,9 +5,9 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { container } from './page-width.css';
import type { PageLayoutMode } from './types';
import type { PageLayoutMode, PropertyValueProps } from './types';
export const PageWidthValue = () => {
export const PageWidthValue = ({ onChange }: PropertyValueProps) => {
const t = useI18n();
const editorSetting = useService(EditorSettingService).editorSetting;
const defaultPageWidth = useLiveData(editorSetting.settings$).fullWidthLayout;
@@ -44,8 +44,9 @@ export const PageWidthValue = () => {
const handleChange = useCallback(
(value: PageLayoutMode) => {
doc.record.setProperty('pageWidth', value);
onChange?.(value, true);
},
[doc]
[doc, onChange]
);
return (
<PropertyValue className={container} hoverable={false}>

View File

@@ -5,8 +5,9 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
import { TagsInlineEditor } from '../tags-inline-editor';
import * as styles from './tags.css';
import type { PropertyValueProps } from './types';
export const TagsValue = () => {
export const TagsValue = ({ onChange }: PropertyValueProps) => {
const t = useI18n();
const doc = useService(DocService).doc;
@@ -27,6 +28,7 @@ export const TagsValue = () => {
'com.affine.page-properties.property-value-placeholder'
]()}
pageId={doc.id}
onChange={value => onChange(value, true)}
/>
</PropertyValue>
);

View File

@@ -69,7 +69,6 @@ export const textInvisible = style({
visibility: 'hidden',
fontSize: cssVar('fontSm'),
lineHeight: '22px',
padding: `6px`,
});
export const mobileTextInvisible = style([textInvisible, mobileTextareaBase]);

View File

@@ -3,7 +3,7 @@ import type { DocCustomPropertyInfo } from '@toeverything/infra';
export interface PropertyValueProps {
propertyInfo?: DocCustomPropertyInfo;
value: any;
onChange: (value: any) => void;
onChange: (value: any, skipCommit?: boolean) => void; // if skipCommit is true, the change will be handled in the component itself
}
export type PageLayoutMode = 'standard' | 'fullWidth';

View File

@@ -7,14 +7,21 @@ import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { Editor } from '@affine/core/modules/editor';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { OpenInAppService } from '@affine/core/modules/open-in-app';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons/rc';
import {
EdgelessIcon,
HistoryIcon,
LocalWorkspaceIcon,
PageIcon,
} from '@blocksuite/icons/rc';
import {
DocService,
useLiveData,
useService,
useServiceOptional,
WorkspaceService,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
@@ -73,6 +80,8 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
const openInAppService = useServiceOptional(OpenInAppService);
useEffect(() => {
const unsubs: Array<() => void> = [];
const preconditionStrategy = () =>
@@ -251,6 +260,23 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-export-to-snapshot`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: t['Export to Snapshot'](),
async run() {
track.$.cmdk.editor.export({
type: 'snapshot',
});
exportHandler('snapshot');
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-move-to-trash`,
@@ -298,6 +324,20 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
);
}
if (isCloudWorkspace && BUILD_CONFIG.isWeb) {
unsubs.push(
registerAffineCommand({
id: 'editor:open-in-app',
category: `editor:${mode}`,
icon: <LocalWorkspaceIcon />,
label: t['com.affine.header.option.open-in-desktop'](),
run() {
openInAppService?.showOpenInAppPage();
},
})
);
}
unsubs.push(
registerAffineCommand({
id: 'alert-ctrl-s',
@@ -335,5 +375,6 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
pageWidth,
defaultPageWidth,
checked,
openInAppService,
]);
}

View File

@@ -74,7 +74,7 @@ globalStyle(`${root} > :last-child`, {
paddingRight: '8px',
});
export const titleIconsWrapper = style({
padding: '0 5px',
width: 34,
display: 'flex',
alignItems: 'center',
gap: '10px',
@@ -119,6 +119,8 @@ export const iconCell = style({
fontSize: cssVar('fontH3'),
color: cssVar('iconColor'),
flexShrink: 0,
width: 24,
height: 24,
});
export const tagsCell = style({
display: 'flex',

View File

@@ -6,11 +6,11 @@ import {
Scrollable,
useConfirmModal,
} from '@affine/component';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { Tag } from '@affine/core/modules/tag';
import { TagService } from '@affine/core/modules/tag';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { isNewTabTrigger } from '@affine/core/utils';
import type { Collection } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
@@ -41,31 +41,54 @@ import { PageListNewPageButton } from './page-list-new-page-button';
export const PageListHeader = () => {
const t = useI18n();
const { workspaceService } = useServices({
WorkspaceService,
});
const { workspaceService, workspaceDialogService, workbenchService } =
useServices({
WorkspaceService,
WorkspaceDialogService,
WorkbenchService,
});
const workbench = workbenchService.workbench;
const workspace = workspaceService.workspace;
const { importFile, createEdgeless, createPage } = usePageHelper(
workspace.docCollection
);
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
const title = useMemo(() => {
return t['com.affine.all-pages.header']();
}, [t]);
const onImportFile = useAsyncCallback(async () => {
const options = await importFile();
if (options.isWorkspaceFile) {
track.allDocs.header.actions.createWorkspace({
control: 'import',
});
} else {
track.allDocs.header.actions.createDoc({
control: 'import',
});
}
}, [importFile]);
const handleOpenDocs = useCallback(
(result: {
docIds: string[];
entryId?: string;
isWorkspaceFile?: boolean;
}) => {
const { docIds, entryId, isWorkspaceFile } = result;
// If the imported file is a workspace file, open the entry page.
if (isWorkspaceFile && entryId) {
workbench.openDoc(entryId);
} else if (!docIds.length) {
return;
}
// Open all the docs when there are multiple docs imported.
if (docIds.length > 1) {
workbench.openAll();
} else {
// Otherwise, open the only doc.
workbench.openDoc(docIds[0]);
}
},
[workbench]
);
const onImportFile = useCallback(() => {
track.$.header.importModal.open();
workspaceDialogService.open('import', undefined, payload => {
if (!payload) {
return;
}
handleOpenDocs(payload);
});
}, [workspaceDialogService, handleOpenDocs]);
return (
<div className={styles.docListHeader}>

View File

@@ -72,9 +72,10 @@ globalStyle(`${root} > :last-child`, {
paddingRight: '8px',
});
export const titleIconsWrapper = style({
padding: '0 5px',
width: 34,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
});
export const selectionCell = style({

View File

@@ -6,6 +6,7 @@ import {
ExportToHtmlIcon,
ExportToMarkdownIcon,
ExportToPngIcon,
PageIcon,
PrinterIcon,
} from '@blocksuite/icons/rc';
import type { ReactNode } from 'react';
@@ -22,7 +23,9 @@ interface ExportMenuItemProps<T> {
}
interface ExportProps {
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => void;
exportHandler: (
type: 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot'
) => void;
pageMode?: 'page' | 'edgeless';
className?: string;
}
@@ -94,6 +97,13 @@ export const ExportMenuItems = ({
icon={<ExportToMarkdownIcon />}
label={t['Export to Markdown']()}
/>
<ExportMenuItem
onSelect={() => exportHandler('snapshot')}
className={className}
type="snapshot"
icon={<PageIcon />}
label={t['Export to Snapshot']()}
/>
</>
);
};

View File

@@ -2,4 +2,3 @@ export * from './disable-public-sharing';
export * from './export';
// export * from './MoveTo';
export * from './move-to-trash';
export * from './snapshot';

View File

@@ -1,223 +0,0 @@
import { MenuItem, MenuSeparator, MenuSub, notify } from '@affine/component';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { openFileOrFiles, ZipTransformer } from '@blocksuite/affine/blocks';
import type { Doc } from '@blocksuite/affine/store';
import { ExportIcon, ImportIcon, ToneIcon } from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useService,
WorkspaceService,
} from '@toeverything/infra';
import { type ReactNode, useCallback } from 'react';
import { useExportPage } from '../../hooks/affine/use-export-page';
import { useAsyncCallback } from '../../hooks/affine-async-hooks';
import { transitionStyle } from './index.css';
interface SnapshotMenuItemsProps {
snapshotActionHandler: (action: 'import' | 'export' | 'disable') => void;
className?: string;
}
interface SnapshotMenuItemProps<T> {
onSelect: () => void;
className?: string;
type: T;
icon: ReactNode;
label: string;
}
interface SnapshotProps {
className?: string;
}
export function SnapshotMenuItem<T>({
onSelect,
className,
type,
icon,
label,
}: SnapshotMenuItemProps<T>) {
return (
<MenuItem
className={className}
data-testid={`snapshot-${type}`}
onSelect={onSelect}
block
prefixIcon={icon}
>
{label}
</MenuItem>
);
}
export const DisableSnapshotMenuItems = ({
snapshotActionHandler,
className = transitionStyle,
}: SnapshotMenuItemsProps) => {
const t = useI18n();
return (
<SnapshotMenuItem
onSelect={() => snapshotActionHandler('disable')}
className={className}
type="disable"
icon={<ToneIcon />}
label={t['Disable Snapshot']()}
/>
);
};
export const SnapshotMenuItems = ({
snapshotActionHandler,
className = transitionStyle,
}: SnapshotMenuItemsProps) => {
const t = useI18n();
return (
<>
<SnapshotMenuItem
onSelect={() => snapshotActionHandler('import')}
className={className}
type="import"
icon={<ImportIcon />}
label={t['Import']()}
/>
<SnapshotMenuItem
onSelect={() => snapshotActionHandler('export')}
className={className}
type="export"
icon={<ExportIcon />}
label={t['Export']()}
/>
</>
);
};
export const Snapshot = ({ className }: SnapshotProps) => {
const t = useI18n();
const workspace = useService(WorkspaceService).workspace;
const docCollection = workspace.docCollection;
const workbench = useService(WorkbenchService).workbench;
const exportHandler = useExportPage();
const featureFlagService = useService(FeatureFlagService);
const importSnapshot = useCallback(async () => {
try {
const file = await openFileOrFiles({ acceptType: 'Zip' });
if (!file) return null;
track.$.header.snapshot.import({
type: 'snapshot',
status: 'importing',
});
const importedDocs = (
await ZipTransformer.importDocs(docCollection, file)
).filter(doc => doc !== undefined);
if (importedDocs.length === 0) {
notify.error({
title: 'Import Snapshot Failed',
message: 'No valid documents found in the imported file.',
});
return null;
}
notify.success({
title: 'Imported Snapshot Successfully',
message: `Imported ${importedDocs.length} doc(s)`,
});
track.$.header.snapshot.import({
type: 'snapshot',
status: 'success',
result: {
docCount: importedDocs.length,
},
});
return importedDocs;
} catch (error) {
console.error('Error importing snapshot:', error);
notify.error({
title: 'Import Snapshot Failed',
message: 'Failed to import snapshot. Please try again.',
});
track.$.header.snapshot.import({
type: 'snapshot',
status: 'failed',
error: error instanceof Error ? error.message : undefined,
});
return null;
}
}, [docCollection]);
const openImportedDocs = useCallback(
(importedDocs: Doc[]) => {
if (importedDocs.length > 1) {
workbench.openAll();
} else if (importedDocs[0]?.id) {
workbench.openDoc(importedDocs[0].id);
}
},
[workbench]
);
const handleImportSnapshot = useAsyncCallback(async () => {
const importedDocs = await importSnapshot();
if (importedDocs) {
openImportedDocs(importedDocs);
track.$.header.docOptions.import();
track.$.header.actions.createDoc({
control: 'import',
});
}
}, [importSnapshot, openImportedDocs]);
const disableSnapshotActionOption = useCallback(() => {
featureFlagService.flags.enable_snapshot_import_export.set(false);
}, [featureFlagService]);
const snapshotActionHandler = useCallback(
(action: 'import' | 'export' | 'disable') => {
switch (action) {
case 'import':
return handleImportSnapshot();
case 'export':
track.$.header.snapshot.export({
type: 'snapshot',
});
return exportHandler('snapshot');
case 'disable':
return disableSnapshotActionOption();
}
},
[handleImportSnapshot, exportHandler, disableSnapshotActionOption]
);
const items = (
<>
<SnapshotMenuItems
snapshotActionHandler={snapshotActionHandler}
className={className}
/>
<MenuSeparator />
<DisableSnapshotMenuItems
snapshotActionHandler={snapshotActionHandler}
className={className}
/>
</>
);
return (
<MenuSub
items={items}
triggerOptions={{
className: transitionStyle,
prefixIcon: <ToneIcon />,
'data-testid': 'snapshot-menu',
}}
subOptions={{}}
>
{t['Snapshot']()}
</MenuSub>
);
};

View File

@@ -1,30 +0,0 @@
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { MenuItem } from '@affine/core/modules/app-sidebar/views';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import type { DocCollection } from '@blocksuite/affine/store';
import { ImportIcon } from '@blocksuite/icons/rc';
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => {
const t = useI18n();
const { importFile } = usePageHelper(docCollection);
const onImportFile = useAsyncCallback(async () => {
const options = await importFile();
track.$.navigationPanel.workspaceList[
options.isWorkspaceFile ? 'createWorkspace' : 'createDoc'
]({
control: 'import',
});
}, [importFile]);
return (
<MenuItem icon={<ImportIcon />} onClick={onImportFile}>
{t['Import']()}
</MenuItem>
);
};
export default ImportPage;

View File

@@ -27,7 +27,7 @@ export const SignInItem = () => {
const t = useI18n();
const onClickSignIn = useCallback(() => {
track.$.navigationPanel.workspaceList.signIn();
track.$.navigationPanel.workspaceList.requestSignIn();
setOpen(state => ({
...state,
openModal: true,

View File

@@ -22,6 +22,7 @@ import {
type ReactElement,
} from 'react';
import { AppFallback } from './mobile';
import * as styles from './styles.css';
export const AppContainer = ({
@@ -102,7 +103,23 @@ const BrowserLayout = ({
);
};
const LayoutComponent = BUILD_CONFIG.isElectron ? DesktopLayout : BrowserLayout;
const MobileLayout = ({
children,
fallback = false,
}: PropsWithChildren<{ fallback?: boolean }>) => {
return (
<div className={styles.browserAppViewContainer}>
{fallback ? <AppFallback /> : null}
<MainContainer>{children}</MainContainer>
</div>
);
};
const LayoutComponent = BUILD_CONFIG.isElectron
? DesktopLayout
: BUILD_CONFIG.isMobileEdition
? MobileLayout
: BrowserLayout;
const MainContainer = forwardRef<
HTMLDivElement,

View File

@@ -1,6 +1,5 @@
import { SafeArea, Skeleton } from '@affine/component';
import { WorkspaceSelector } from '../workspace-selector';
import { WorkspaceSelector } from '@affine/core/mobile/components';
const SectionTitleFallback = () => {
return (

View File

@@ -72,6 +72,15 @@ export const InfoTable = ({
});
}, []);
const onPropertyChange = useCallback(
(property: DocCustomPropertyInfo, _value: unknown) => {
track.$.docInfoPanel.property.editProperty({
type: property.type,
});
},
[]
);
return (
<>
{backlinks && backlinks.length > 0 ? (
@@ -122,6 +131,7 @@ export const InfoTable = ({
key={property.id}
propertyInfo={property}
defaultOpenEditMenu={newPropertyId === property.id}
onChange={value => onPropertyChange(property, value)}
/>
))}
<Menu

View File

@@ -8,6 +8,8 @@ export const container = style({
padding: '20px 0',
alignSelf: 'start',
marginTop: '120px',
maxHeight: 'calc(100dvh - 240px)',
overflow: 'auto',
});
export const titleContainer = style({

View File

@@ -8,6 +8,7 @@ import { UrlService } from '@affine/core/modules/url';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import {
HtmlTransformer,
MarkdownTransformer,
NotionHtmlTransformer,
openFileOrFiles,
@@ -15,24 +16,22 @@ import {
} from '@blocksuite/affine/blocks';
import type { DocCollection } from '@blocksuite/affine/store';
import {
ExportToHtmlIcon,
ExportToMarkdownIcon,
HelpIcon,
NotionIcon,
PageIcon,
ZipIcon,
} from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useLiveData,
useService,
WorkspaceService,
} from '@toeverything/infra';
import { useService, WorkspaceService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { type ReactElement, useCallback, useState } from 'react';
import * as style from './styles.css';
type ImportType = 'markdown' | 'markdownZip' | 'notion' | 'snapshot';
type AcceptType = 'Markdown' | 'Zip';
type ImportType = 'markdown' | 'markdownZip' | 'notion' | 'snapshot' | 'html';
type AcceptType = 'Markdown' | 'Zip' | 'Html';
type Status = 'idle' | 'importing' | 'success' | 'error';
type ImportResult = {
docIds: string[];
@@ -68,14 +67,31 @@ const importOptions = [
key: 'markdownZip',
label: 'com.affine.import.markdown-with-media-files',
prefixIcon: (
<ExportToMarkdownIcon
<ZipIcon color={cssVarV2('icon/primary')} width={20} height={20} />
),
suffixIcon: (
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
),
suffixTooltip: 'com.affine.import.markdown-with-media-files.tooltip',
testId: 'editor-option-menu-import-markdown-with-media',
type: 'markdownZip' as ImportType,
},
{
key: 'html',
label: 'com.affine.import.html-files',
prefixIcon: (
<ExportToHtmlIcon
color={cssVarV2('icon/primary')}
width={20}
height={20}
/>
),
testId: 'editor-option-menu-import-markdown-with-media',
type: 'markdownZip' as ImportType,
suffixIcon: (
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
),
suffixTooltip: 'com.affine.import.html-files.tooltip',
testId: 'editor-option-menu-import-html',
type: 'html' as ImportType,
},
{
key: 'notion',
@@ -88,6 +104,15 @@ const importOptions = [
testId: 'editor-option-menu-import-notion',
type: 'notion' as ImportType,
},
{
key: 'snapshot',
label: 'com.affine.import.snapshot',
prefixIcon: (
<PageIcon color={cssVarV2('icon/primary')} width={20} height={20} />
),
testId: 'editor-option-menu-import-snapshot',
type: 'snapshot' as ImportType,
},
];
const importConfigs: Record<ImportType, ImportConfig> = {
@@ -128,6 +153,28 @@ const importConfigs: Record<ImportType, ImportConfig> = {
};
},
},
html: {
fileOptions: { acceptType: 'Html', multiple: true },
importFunction: async (docCollection, files) => {
if (!Array.isArray(files)) {
throw new Error('Expected an array of files for html files import');
}
const docIds: string[] = [];
for (const file of files) {
const text = await file.text();
const fileName = file.name.split('.').slice(0, -1).join('.');
const docId = await HtmlTransformer.importHTMLToDoc({
collection: docCollection,
html: text,
fileName,
});
if (docId) docIds.push(docId);
}
return {
docIds,
};
},
},
notion: {
fileOptions: { acceptType: 'Zip', multiple: false },
importFunction: async (docCollection, file) => {
@@ -200,10 +247,6 @@ const ImportOptions = ({
onImport: (type: ImportType) => void;
}) => {
const t = useI18n();
const featureFlagService = useService(FeatureFlagService);
const enableSnapshotImportExport = useLiveData(
featureFlagService.flags.enable_snapshot_import_export.$
);
return (
<>
@@ -232,14 +275,6 @@ const ImportOptions = ({
)
)}
</div>
{enableSnapshotImportExport && (
<div className={style.importModalTip}>
{t['Import']()}{' '}
<span className={style.link} onClick={() => onImport('snapshot')}>
{t['Snapshot']()}.
</span>
</div>
)}
<div className={style.importModalTip}>
{t['com.affine.import.modal.tip']()}{' '}
<a
@@ -373,6 +408,9 @@ export const ImportDialog = ({
docCount: docIds.length,
},
});
track.$.importModal.$.createDoc({
control: 'import',
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';

View File

@@ -31,6 +31,7 @@ export const AIUsagePanel = ({
useEffect(() => {
// revalidate latest subscription status
subscriptionService.subscription.revalidate();
subscriptionService.prices.revalidate();
}, [subscriptionService]);
const copilotQuotaService = useService(UserCopilotQuotaService);
useEffect(() => {

View File

@@ -10,11 +10,7 @@ import { appIconMap, appNames } from '@affine/core/utils/channel';
import { useI18n } from '@affine/i18n';
import { mixpanel } from '@affine/track';
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useLiveData,
useServices,
} from '@toeverything/infra';
import { useServices } from '@toeverything/infra';
import { useCallback } from 'react';
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
@@ -29,13 +25,9 @@ export const AboutAffine = () => {
const channel = BUILD_CONFIG.appBuildType;
const appIcon = appIconMap[channel];
const appName = appNames[channel];
const { urlService, featureFlagService } = useServices({
const { urlService } = useServices({
UrlService,
FeatureFlagService,
});
const enableSnapshotImportExport = useLiveData(
featureFlagService.flags.enable_snapshot_import_export.$
);
const onSwitchAutoCheck = useCallback(
(checked: boolean) => {
@@ -65,13 +57,6 @@ export const AboutAffine = () => {
[updateSettings]
);
const onSwitchSnapshotImportExport = useCallback(
(checked: boolean) => {
featureFlagService.flags.enable_snapshot_import_export.set(checked);
},
[featureFlagService]
);
return (
<>
<SettingHeader
@@ -155,16 +140,6 @@ export const AboutAffine = () => {
{t['com.affine.aboutAFFiNE.contact.community']()}
<OpenInNewIcon className="icon" />
</a>
<SettingRow
name={t['com.affine.snapshot.import-export.enable']()}
desc={t['com.affine.snapshot.import-export.enable.desc']()}
className={styles.snapshotImportExportRow}
>
<Switch
checked={enableSnapshotImportExport}
onChange={onSwitchSnapshotImportExport}
/>
</SettingRow>
</SettingWrapper>
<SettingWrapper title={t['com.affine.aboutAFFiNE.community.title']()}>
<div className={styles.communityWrapper}>

View File

@@ -74,7 +74,12 @@ export const AISubscribe = ({
onBeforeCheckout={onBeforeCheckout}
checkoutOptions={checkoutOptions}
renderer={props => (
<Button variant="primary" {...props} {...btnProps}>
<Button
variant="primary"
{...props}
{...btnProps}
data-testid="ai-subscribe-button"
>
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`}
{displayedFrequency === 'monthly' ? (
<span

View File

@@ -135,8 +135,8 @@ const SettingModalInner = ({
className={style.wrapper}
ref={modalContentWrapperRef}
>
<div ref={modalContentRef} className={style.centerContainer}>
<div className={style.content}>
<div className={style.centerContainer}>
<div ref={modalContentRef} className={style.content}>
<Suspense fallback={<WorkspaceDetailSkeleton />}>
{}
{settingState.activeTab === 'account' &&

View File

@@ -1,5 +1,4 @@
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import {
AllPageListOperationsMenu,
PageDisplayMenu,
@@ -7,12 +6,15 @@ import {
} from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { isNewTabTrigger } from '@affine/core/utils';
import type { Filter } from '@affine/env/filter';
import { track } from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc';
import { useServices, WorkspaceService } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback } from 'react';
import * as styles from './all-page.css';
@@ -25,26 +27,49 @@ export const AllPageHeader = ({
filters: Filter[];
onChangeFilters: (filters: Filter[]) => void;
}) => {
const { workspaceService } = useServices({
WorkspaceService,
});
const { workspaceService, workspaceDialogService, workbenchService } =
useServices({
WorkspaceService,
WorkspaceDialogService,
WorkbenchService,
});
const workbench = workbenchService.workbench;
const workspace = workspaceService.workspace;
const { importFile, createEdgeless, createPage } = usePageHelper(
workspace.docCollection
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
const handleOpenDocs = useCallback(
(result: {
docIds: string[];
entryId?: string;
isWorkspaceFile?: boolean;
}) => {
const { docIds, entryId, isWorkspaceFile } = result;
// If the imported file is a workspace file, open the entry page.
if (isWorkspaceFile && entryId) {
workbench.openDoc(entryId);
} else if (!docIds.length) {
return;
}
// Open all the docs when there are multiple docs imported.
if (docIds.length > 1) {
workbench.openAll();
} else {
// Otherwise, open the only doc.
workbench.openDoc(docIds[0]);
}
},
[workbench]
);
const onImportFile = useAsyncCallback(async () => {
const options = await importFile();
if (options.isWorkspaceFile) {
track.allDocs.header.actions.createWorkspace({
control: 'import',
});
} else {
track.allDocs.header.actions.createDoc({
control: 'import',
});
}
}, [importFile]);
const onImportFile = useCallback(() => {
track.$.header.importModal.open();
workspaceDialogService.open('import', undefined, payload => {
if (!payload) {
return;
}
handleOpenDocs(payload);
});
}, [workspaceDialogService, handleOpenDocs]);
return (
<Header

View File

@@ -44,3 +44,7 @@ export const affineDocViewport = style({
export const scrollbar = style({
marginRight: '4px',
});
export const sidebarScrollArea = style({
height: '100%',
});

View File

@@ -302,19 +302,39 @@ const DetailPageImpl = memo(function DetailPageImpl() {
)}
<ViewSidebarTab tabId="properties" icon={<PropertyIcon />}>
<DocPropertySidebar />
<Scrollable.Root className={styles.sidebarScrollArea}>
<Scrollable.Viewport>
<DocPropertySidebar />
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</ViewSidebarTab>
<ViewSidebarTab tabId="journal" icon={<TodayIcon />}>
<EditorJournalPanel />
<Scrollable.Root className={styles.sidebarScrollArea}>
<Scrollable.Viewport>
<EditorJournalPanel />
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</ViewSidebarTab>
<ViewSidebarTab tabId="outline" icon={<TocIcon />}>
<EditorOutlinePanel editor={editorContainer} />
<Scrollable.Root className={styles.sidebarScrollArea}>
<Scrollable.Viewport>
<EditorOutlinePanel editor={editorContainer} />
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</ViewSidebarTab>
<ViewSidebarTab tabId="frame" icon={<FrameIcon />}>
<EditorFramePanel editor={editorContainer} />
<Scrollable.Root className={styles.sidebarScrollArea}>
<Scrollable.Viewport>
<EditorFramePanel editor={editorContainer} />
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</ViewSidebarTab>
<GlobalPageHistoryModal />

View File

@@ -9,6 +9,7 @@ import { assignInlineVars } from '@vanilla-extract/dynamic';
import React from 'react';
import type { Location } from 'react-router-dom';
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
import { AppTabJournal } from './journal';
import * as styles from './styles.css';
@@ -54,14 +55,20 @@ const routes: Route[] = [
];
export const AppTabs = ({ background }: { background?: string }) => {
const virtualKeyboardService = useService(VirtualKeyboardService);
const virtualKeyboardVisible = useLiveData(virtualKeyboardService.show$);
return (
<SafeArea
bottom
className={styles.appTabs}
bottomOffset={2}
style={assignInlineVars({
[styles.appTabsBackground]: background,
})}
style={{
...assignInlineVars({
[styles.appTabsBackground]: background,
}),
visibility: virtualKeyboardVisible ? 'hidden' : 'visible',
}}
>
<ul className={styles.appTabsInner} id="app-tabs" role="tablist">
{routes.map(route => {

View File

@@ -7,7 +7,7 @@ export const appTabsBackground = createVar('appTabsBackground');
export const appTabs = style({
vars: {
[appTabsBackground]: cssVarV2('layer/background/secondary'),
[appTabsBackground]: cssVarV2('layer/background/mobile/primary'),
},
backgroundColor: appTabsBackground,
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,

View File

@@ -2,11 +2,13 @@ import { IconButton } from '@affine/component';
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
import { PagePreview } from '@affine/core/components/page-list/page-content-preview';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import {
WorkbenchLink,
type WorkbenchLinkProps,
} from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import type { DocMeta } from '@blocksuite/affine/store';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import clsx from 'clsx';
@@ -15,11 +17,9 @@ import { forwardRef, type ReactNode, useMemo } from 'react';
import * as styles from './styles.css';
import { DocCardTags } from './tag';
const calcRowsById = (id: string) => {
const [MIN, MAX] = [2, 8];
export const calcRowsById = (id: string, min = 2, max = 8) => {
const code = id.charCodeAt(0);
return Math.floor((code % (MAX - MIN)) + MIN);
return Math.floor((code % (max - min)) + min);
};
export interface DocCardProps extends Omit<WorkbenchLinkProps, 'to'> {
@@ -40,8 +40,13 @@ export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
{ showTags = true, meta, className, autoHeightById, ...attrs },
ref
) {
const t = useI18n();
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const workspace = useService(WorkspaceService).workspace;
const docDisplayService = useService(DocDisplayMetaService);
const titleInfo = useLiveData(docDisplayService.title$(meta.id));
const title =
typeof titleInfo === 'string' ? titleInfo : t[titleInfo.i18nKey]();
const favorited = useLiveData(favAdapter.isFavorite$(meta.id, 'doc'));
@@ -68,9 +73,7 @@ export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
{...attrs}
>
<header className={styles.head} data-testid="doc-card-header">
<h3 className={styles.title}>
{meta.title || <span className={styles.untitled}>Untitled</span>}
</h3>
<h3 className={styles.title}>{title}</h3>
<IconButton
aria-label="favorite"
icon={

View File

@@ -10,7 +10,7 @@ export const card = style({
borderRadius: 12,
border: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
boxShadow: '0px 2px 3px rgba(0,0,0,0.05)',
background: cssVarV2('layer/background/primary'),
background: cssVarV2('layer/background/mobile/secondary'),
display: 'flex',
flexDirection: 'column',

View File

@@ -3,6 +3,5 @@ export * from './doc-card';
export * from './rename';
export * from './search-input';
export * from './search-result';
export * from './skeletons';
export * from './user-plan-tag';
export * from './workspace-selector';

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