mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 16:44:56 +00:00
Compare commits
104 Commits
v0.25.5
...
darksky/na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac9158f87 | ||
|
|
e7d0f31546 | ||
|
|
fe5d6c0c0f | ||
|
|
510933becf | ||
|
|
3633c75c6f | ||
|
|
41addfe311 | ||
|
|
9a7f8e7d4d | ||
|
|
60de882a30 | ||
|
|
9f96633b33 | ||
|
|
1e8095c224 | ||
|
|
0b0ae5ea0a | ||
|
|
f745f7b669 | ||
|
|
97507e7043 | ||
|
|
91e6f3c45c | ||
|
|
c7b74384a4 | ||
|
|
20c4951847 | ||
|
|
7ef550a736 | ||
|
|
bc03fab649 | ||
|
|
99332228da | ||
|
|
95ef04f3e0 | ||
|
|
e2adab7805 | ||
|
|
30fb953344 | ||
|
|
ff2e96d847 | ||
|
|
95a5e941e7 | ||
|
|
d6b380aee5 | ||
|
|
1b9d065778 | ||
|
|
e12fe9c12b | ||
|
|
1bfd29df99 | ||
|
|
a38e94f314 | ||
|
|
6951f1002f | ||
|
|
20a80015c0 | ||
|
|
504460438f | ||
|
|
582340b0b7 | ||
|
|
11d9a41433 | ||
|
|
f49f42ce76 | ||
|
|
f78dc44690 | ||
|
|
a38e7e58e0 | ||
|
|
4f1d57ade5 | ||
|
|
1b532d5c6c | ||
|
|
6514614df8 | ||
|
|
702dbf7be4 | ||
|
|
78949044ec | ||
|
|
4eed92cebf | ||
|
|
3fe8923fc3 | ||
|
|
ca386283c5 | ||
|
|
2e38898937 | ||
|
|
e8693a3a25 | ||
|
|
b6dc68eddf | ||
|
|
08a30edb2d | ||
|
|
6c9ab603eb | ||
|
|
4b721dffe0 | ||
|
|
a1f1c61a9f | ||
|
|
76524084d1 | ||
|
|
a9937e18b6 | ||
|
|
7539135c4d | ||
|
|
8f59509e73 | ||
|
|
321965a424 | ||
|
|
efbdee5508 | ||
|
|
28a1ac4772 | ||
|
|
caeec23ec6 | ||
|
|
a1767ebedb | ||
|
|
b052c92421 | ||
|
|
66407f2b2f | ||
|
|
f5076a37ae | ||
|
|
4717886c9e | ||
|
|
844b9d9592 | ||
|
|
a0eeed0cdb | ||
|
|
246e09e0cd | ||
|
|
7f96c97b67 | ||
|
|
f832b28dac | ||
|
|
b258fc3775 | ||
|
|
396cda2fff | ||
|
|
cb0ff04efa | ||
|
|
40f3337d45 | ||
|
|
215541d331 | ||
|
|
90d0ca847a | ||
|
|
255b4571c0 | ||
|
|
2efb41fc1a | ||
|
|
027f741ed6 | ||
|
|
bc115baf35 | ||
|
|
776ca2c702 | ||
|
|
903e0c4d71 | ||
|
|
f29e47e9d2 | ||
|
|
6e6b85098e | ||
|
|
cf14accd2b | ||
|
|
cf4e37c584 | ||
|
|
69cdeedc4e | ||
|
|
0495fac6f1 | ||
|
|
5cac8971eb | ||
|
|
1196101226 | ||
|
|
bcc892c8ec | ||
|
|
88a2e4aa4b | ||
|
|
0bedaaadba | ||
|
|
1d9fe3b8d9 | ||
|
|
33a014977a | ||
|
|
221c493c56 | ||
|
|
6c36fc5941 | ||
|
|
477e6f4106 | ||
|
|
b7ebe3d0d6 | ||
|
|
20ba8875c1 | ||
|
|
8544e58c01 | ||
|
|
36a08190e0 | ||
|
|
b229c96ee5 | ||
|
|
62fe6982fb |
@@ -6,7 +6,6 @@ yarn install
|
||||
|
||||
# Build Server Dependencies
|
||||
yarn affine @affine/server-native build
|
||||
yarn affine @affine/reader build
|
||||
|
||||
# Create database
|
||||
yarn affine @affine/server prisma migrate reset -f
|
||||
|
||||
@@ -397,7 +397,7 @@
|
||||
},
|
||||
"urlPrefix": {
|
||||
"type": "string",
|
||||
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
|
||||
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
|
||||
},
|
||||
"signKey": {
|
||||
"type": "string",
|
||||
@@ -518,7 +518,7 @@
|
||||
},
|
||||
"urlPrefix": {
|
||||
"type": "string",
|
||||
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
|
||||
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
|
||||
},
|
||||
"signKey": {
|
||||
"type": "string",
|
||||
@@ -611,11 +611,6 @@
|
||||
"type": "object",
|
||||
"description": "Configuration for flags module",
|
||||
"properties": {
|
||||
"earlyAccessControl": {
|
||||
"type": "boolean",
|
||||
"description": "Only allow users with early access features to access the app\n@default false",
|
||||
"default": false
|
||||
},
|
||||
"allowGuestDemoWorkspace": {
|
||||
"type": "boolean",
|
||||
"description": "Whether allow guest users to create demo workspaces.\n@default true",
|
||||
@@ -928,7 +923,7 @@
|
||||
},
|
||||
"urlPrefix": {
|
||||
"type": "string",
|
||||
"description": "The presigned url prefix for the cloudflare r2 storage provider.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
|
||||
"description": "The custom domain URL prefix for the cloudflare r2 storage provider.\nWhen `enabled=true` and `urlPrefix` + `signKey` are provided, the server will:\n- Redirect GET requests to this custom domain with an HMAC token.\n- Return upload URLs under `/api/storage/*` for uploads.\nPresigned/upload proxy TTL is 1 hour.\nsee https://developers.cloudflare.com/waf/custom-rules/use-cases/configure-token-authentication/ to configure it.\nExample value: \"https://storage.example.com\"\nExample rule: is_timed_hmac_valid_v0(\"your_secret\", http.request.uri, 10800, http.request.timestamp.sec, 6)"
|
||||
},
|
||||
"signKey": {
|
||||
"type": "string",
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
8
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -74,3 +74,11 @@ body:
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
Tip: You can attach images here
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is your content generated by AI?
|
||||
description: >
|
||||
(Required) Please confirm that the content you submit was not generated by AI or only minimally edited by AI.
|
||||
If an administrator believes the post contains a large amount of AI-generated content, they may directly close the question.
|
||||
options:
|
||||
- label: I confirm that the content I submitted was **not** generated by AI / **merely contained minimal** AI edits.
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
8
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
@@ -35,3 +35,11 @@ body:
|
||||
See the AFFiNE [Contributing Guide](https://github.com/toeverything/affine/blob/canary/CONTRIBUTING.md) to get started.
|
||||
options:
|
||||
- label: Yes I'd like to help by submitting a PR!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is your content generated by AI?
|
||||
description: >
|
||||
(Required) Please confirm that the content you submit was not generated by AI or only minimally edited by AI.
|
||||
If an administrator believes the post contains a large amount of AI-generated content, they may directly close the question.
|
||||
options:
|
||||
- label: I confirm that the content I submitted was **not** generated by AI / **merely contained minimal** AI edits.
|
||||
|
||||
6
.github/actions/build-rust/action.yml
vendored
6
.github/actions/build-rust/action.yml
vendored
@@ -75,7 +75,11 @@ runs:
|
||||
shell: bash
|
||||
if: ${{ runner.os != 'Windows' && inputs.no-build != 'true' }}
|
||||
run: |
|
||||
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
|
||||
if [[ "${{ inputs.target }}" == "x86_64-unknown-linux-gnu" ]]; then
|
||||
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }}
|
||||
else
|
||||
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
|
||||
fi
|
||||
env:
|
||||
DEBUG: 'napi:*'
|
||||
|
||||
|
||||
5
.github/actions/server-test-env/action.yml
vendored
5
.github/actions/server-test-env/action.yml
vendored
@@ -4,11 +4,6 @@ description: 'Prepare Server Test Environment'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Bundle @affine/reader
|
||||
shell: bash
|
||||
run: |
|
||||
yarn affine @affine/reader build
|
||||
|
||||
- name: Initialize database
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
2
.github/helm/affine/Chart.yaml
vendored
2
.github/helm/affine/Chart.yaml
vendored
@@ -3,4 +3,4 @@ name: affine
|
||||
description: AFFiNE cloud chart
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.25.5"
|
||||
appVersion: "0.25.7"
|
||||
|
||||
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: doc
|
||||
description: AFFiNE doc server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.25.5"
|
||||
appVersion: "0.25.7"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
@@ -3,7 +3,7 @@ name: graphql
|
||||
description: AFFiNE GraphQL server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.25.5"
|
||||
appVersion: "0.25.7"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
@@ -3,7 +3,7 @@ name: renderer
|
||||
description: AFFiNE renderer server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.25.5"
|
||||
appVersion: "0.25.7"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: sync
|
||||
description: AFFiNE Sync Server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.25.5"
|
||||
appVersion: "0.25.7"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
2
.github/workflows/build-images.yml
vendored
2
.github/workflows/build-images.yml
vendored
@@ -187,8 +187,6 @@ jobs:
|
||||
path: ./packages/backend/native
|
||||
- name: List server-native files
|
||||
run: ls -alh ./packages/backend/native
|
||||
- name: Build @affine/reader
|
||||
run: yarn workspace @affine/reader build
|
||||
- name: Build Server
|
||||
run: yarn workspace @affine/server build
|
||||
- name: Upload server dist
|
||||
|
||||
9
.github/workflows/build-test.yml
vendored
9
.github/workflows/build-test.yml
vendored
@@ -152,11 +152,6 @@ jobs:
|
||||
name: server-native.node
|
||||
path: ./packages/backend/native
|
||||
|
||||
- name: Bundle @affine/reader
|
||||
shell: bash
|
||||
run: |
|
||||
yarn workspace @affine/reader build
|
||||
|
||||
- name: Run Check
|
||||
run: |
|
||||
yarn affine init
|
||||
@@ -812,7 +807,7 @@ jobs:
|
||||
settings:
|
||||
- { target: 'x86_64-unknown-linux-gnu', os: 'ubuntu-latest' }
|
||||
- { target: 'aarch64-unknown-linux-gnu', os: 'ubuntu-24.04-arm' }
|
||||
- { target: 'x86_64-apple-darwin', os: 'macos-13' }
|
||||
- { target: 'x86_64-apple-darwin', os: 'macos-15-intel' }
|
||||
- { target: 'aarch64-apple-darwin', os: 'macos-latest' }
|
||||
- { target: 'x86_64-pc-windows-msvc', os: 'windows-latest' }
|
||||
- { target: 'aarch64-pc-windows-msvc', os: 'windows-11-arm' }
|
||||
@@ -1356,7 +1351,7 @@ jobs:
|
||||
run: |
|
||||
sudo add-apt-repository universe
|
||||
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
|
||||
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak update
|
||||
# some flatpak deps need git protocol.file.allow
|
||||
git config --global protocol.file.allow always
|
||||
|
||||
226
.github/workflows/release-desktop-platform.yml
vendored
Normal file
226
.github/workflows/release-desktop-platform.yml
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
name: Release Desktop Platform
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_type:
|
||||
required: true
|
||||
type: string
|
||||
app_version:
|
||||
required: true
|
||||
type: string
|
||||
git_short_hash:
|
||||
required: true
|
||||
type: string
|
||||
runner:
|
||||
required: true
|
||||
type: string
|
||||
platform:
|
||||
required: true
|
||||
type: string
|
||||
arch:
|
||||
required: true
|
||||
type: string
|
||||
target:
|
||||
required: true
|
||||
type: string
|
||||
apple_codesign:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
install_linux_deps:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
enable_scripts:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
outputs:
|
||||
files_to_be_signed:
|
||||
description: Files to be signed (Windows only)
|
||||
value: ${{ jobs.build.outputs.files_to_be_signed }}
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ inputs.runner }}
|
||||
outputs:
|
||||
files_to_be_signed: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
|
||||
env:
|
||||
BUILD_TYPE: ${{ inputs.build_type }}
|
||||
RELEASE_VERSION: ${{ inputs.app_version }}
|
||||
DEBUG: 'affine:*,napi:*'
|
||||
APP_NAME: affine
|
||||
MACOSX_DEPLOYMENT_TARGET: '12.0'
|
||||
SKIP_GENERATE_ASSETS: 1
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: 'affine'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ inputs.app_version }}
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
app-version: ${{ inputs.app_version }}
|
||||
|
||||
- name: Setup Node.js
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
|
||||
hard-link-nm: false
|
||||
nmHoistingLimits: workspaces
|
||||
enableScripts: ${{ inputs.enable_scripts }}
|
||||
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ inputs.target }}
|
||||
package: '@affine/native'
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: desktop-web
|
||||
path: packages/frontend/apps/electron/resources/web-static
|
||||
|
||||
- name: Build Desktop Layers
|
||||
run: yarn affine @affine/electron build
|
||||
|
||||
- name: Signing By Apple Developer ID
|
||||
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
- name: Install additional dependencies on Linux
|
||||
if: ${{ inputs.platform == 'linux' && inputs.install_linux_deps }}
|
||||
run: |
|
||||
df -h
|
||||
sudo add-apt-repository universe
|
||||
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
|
||||
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak update
|
||||
# some flatpak deps need git protocol.file.allow
|
||||
git config --global protocol.file.allow always
|
||||
# clean up apt cache to save disk space
|
||||
sudo -E apt-get -y purge azure-cli* zulu* hhvm* llvm* firefox* google* dotnet* aspnetcore* powershell* adoptopenjdk* mysql* php* mongodb* moby* snap* || true
|
||||
sudo -E apt-get -qq autoremove --purge
|
||||
sudo rm -rf /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/lib/android
|
||||
sudo apt-get clean
|
||||
rm -rf ~/.cache/yarn ~/.npm
|
||||
df -h
|
||||
|
||||
- name: Remove nbstore node_modules (darwin/linux)
|
||||
if: ${{ inputs.platform != 'win32' }}
|
||||
shell: bash
|
||||
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
|
||||
run: |
|
||||
cargo clean
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
|
||||
|
||||
- name: Remove nbstore node_modules (windows)
|
||||
if: ${{ inputs.platform == 'win32' }}
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
|
||||
|
||||
- name: make
|
||||
if: ${{ inputs.platform != 'win32' }}
|
||||
run: yarn affine @affine/electron make --platform=${{ inputs.platform }} --arch=${{ inputs.arch }}
|
||||
env:
|
||||
SKIP_WEB_BUILD: 1
|
||||
HOIST_NODE_MODULES: 1
|
||||
NODE_OPTIONS: --max-old-space-size=14384
|
||||
|
||||
- name: package
|
||||
if: ${{ inputs.platform == 'win32' }}
|
||||
run: |
|
||||
yarn affine @affine/electron package --platform=${{ inputs.platform }} --arch=${{ inputs.arch }}
|
||||
env:
|
||||
SKIP_WEB_BUILD: 1
|
||||
HOIST_NODE_MODULES: 1
|
||||
NODE_OPTIONS: --max-old-space-size=14384
|
||||
|
||||
- name: signing DMG
|
||||
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
|
||||
run: |
|
||||
codesign --force --sign "Developer ID Application: TOEVERYTHING PTE. LTD." packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make/AFFiNE.dmg
|
||||
|
||||
- name: Save artifacts (mac)
|
||||
if: ${{ inputs.platform == 'darwin' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv packages/frontend/apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/darwin/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
|
||||
|
||||
- name: Save artifacts (linux)
|
||||
if: ${{ inputs.platform == 'linux' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ inputs.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
|
||||
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
|
||||
mv packages/frontend/apps/electron/out/*/make/deb/${{ inputs.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
|
||||
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
if: ${{ inputs.platform == 'darwin' }}
|
||||
with:
|
||||
subject-path: |
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
if: ${{ inputs.platform == 'linux' }}
|
||||
with:
|
||||
subject-path: |
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.zip
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.appimage
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
|
||||
|
||||
- name: Upload Artifact
|
||||
if: ${{ inputs.platform == 'darwin' || inputs.platform == 'linux' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: affine-${{ inputs.platform }}-${{ inputs.arch }}-builds
|
||||
path: builds
|
||||
|
||||
- name: get all files to be signed
|
||||
id: get_files_to_be_signed
|
||||
if: ${{ inputs.platform == 'win32' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
|
||||
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
|
||||
echo $FILES_TO_BE_SIGNED
|
||||
|
||||
- name: Zip artifacts for faster upload
|
||||
if: ${{ inputs.platform == 'win32' }}
|
||||
shell: pwsh
|
||||
run: Compress-Archive -CompressionLevel Fastest -Path packages/frontend/apps/electron/out/* -DestinationPath archive.zip
|
||||
|
||||
- name: Save packaged artifacts for signing
|
||||
if: ${{ inputs.platform == 'win32' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packaged-${{ inputs.platform }}-${{ inputs.arch }}
|
||||
path: |
|
||||
archive.zip
|
||||
!**/*.map
|
||||
294
.github/workflows/release-desktop.yml
vendored
294
.github/workflows/release-desktop.yml
vendored
@@ -12,6 +12,21 @@ on:
|
||||
git-short-hash:
|
||||
required: true
|
||||
type: string
|
||||
desktop_macos:
|
||||
description: 'Desktop - macOS'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
desktop_windows:
|
||||
description: 'Desktop - Windows'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
desktop_linux:
|
||||
description: 'Desktop - Linux'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
@@ -29,6 +44,7 @@ env:
|
||||
|
||||
jobs:
|
||||
before-make:
|
||||
if: ${{ inputs.desktop_macos || inputs.desktop_windows || inputs.desktop_linux }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.build-type }}
|
||||
steps:
|
||||
@@ -58,7 +74,8 @@ jobs:
|
||||
name: desktop-web
|
||||
path: packages/frontend/apps/electron/resources/web-static
|
||||
|
||||
make-distribution:
|
||||
make-distribution-macos:
|
||||
if: ${{ inputs.desktop_macos }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -71,223 +88,90 @@ jobs:
|
||||
platform: darwin
|
||||
arch: arm64
|
||||
target: aarch64-apple-darwin
|
||||
- runner: ubuntu-latest
|
||||
platform: linux
|
||||
arch: x64
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.spec.runner }}
|
||||
needs: before-make
|
||||
environment: ${{ inputs.build-type }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
SKIP_GENERATE_ASSETS: 1
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: 'affine'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ inputs.app-version }}
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
app-version: ${{ inputs.app-version }}
|
||||
- name: Setup Node.js
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
|
||||
hard-link-nm: false
|
||||
nmHoistingLimits: workspaces
|
||||
enableScripts: false
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
package: '@affine/native'
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: desktop-web
|
||||
path: packages/frontend/apps/electron/resources/web-static
|
||||
uses: ./.github/workflows/release-desktop-platform.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: ${{ inputs.build-type }}
|
||||
app_version: ${{ inputs.app-version }}
|
||||
git_short_hash: ${{ inputs.git-short-hash }}
|
||||
runner: ${{ matrix.spec.runner }}
|
||||
platform: ${{ matrix.spec.platform }}
|
||||
arch: ${{ matrix.spec.arch }}
|
||||
target: ${{ matrix.spec.target }}
|
||||
apple_codesign: true
|
||||
|
||||
- name: Build Desktop Layers
|
||||
run: yarn affine @affine/electron build
|
||||
|
||||
- name: Signing By Apple Developer ID
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
- name: Install additional dependencies on Linux
|
||||
if: ${{ matrix.spec.platform == 'linux' }}
|
||||
run: |
|
||||
sudo add-apt-repository universe
|
||||
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
|
||||
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak update
|
||||
# some flatpak deps need git protocol.file.allow
|
||||
git config --global protocol.file.allow always
|
||||
|
||||
- name: Remove nbstore node_modules
|
||||
shell: bash
|
||||
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
|
||||
run: |
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
|
||||
|
||||
- name: make
|
||||
run: yarn affine @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||
env:
|
||||
SKIP_WEB_BUILD: 1
|
||||
HOIST_NODE_MODULES: 1
|
||||
NODE_OPTIONS: --max-old-space-size=14384
|
||||
|
||||
- name: signing DMG
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
run: |
|
||||
codesign --force --sign "Developer ID Application: TOEVERYTHING PTE. LTD." packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make/AFFiNE.dmg
|
||||
|
||||
- name: Save artifacts (mac)
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv packages/frontend/apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
|
||||
- name: Save artifacts (linux)
|
||||
if: ${{ matrix.spec.platform == 'linux' }}
|
||||
run: |
|
||||
mkdir -p builds
|
||||
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.zip
|
||||
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.appimage
|
||||
mv packages/frontend/apps/electron/out/*/make/deb/${{ matrix.spec.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.deb
|
||||
# mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.flatpak
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
if: ${{ matrix.spec.platform == 'darwin' }}
|
||||
with:
|
||||
subject-path: |
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
|
||||
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
if: ${{ matrix.spec.platform == 'linux' }}
|
||||
with:
|
||||
subject-path: |
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.zip
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.appimage
|
||||
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.deb
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
|
||||
path: builds
|
||||
|
||||
package-distribution-windows:
|
||||
environment: ${{ inputs.build-type }}
|
||||
make-distribution-linux:
|
||||
if: ${{ inputs.desktop_linux }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
spec:
|
||||
- runner: windows-latest
|
||||
platform: win32
|
||||
- runner: ubuntu-latest
|
||||
platform: linux
|
||||
arch: x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
- runner: windows-latest
|
||||
platform: win32
|
||||
arch: arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.spec.runner }}
|
||||
target: x86_64-unknown-linux-gnu
|
||||
needs: before-make
|
||||
outputs:
|
||||
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
|
||||
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
|
||||
env:
|
||||
SKIP_GENERATE_ASSETS: 1
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: 'affine'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ inputs.app-version }}
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Version
|
||||
uses: ./.github/actions/setup-version
|
||||
with:
|
||||
app-version: ${{ inputs.app-version }}
|
||||
- name: Setup Node.js
|
||||
timeout-minutes: 10
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
|
||||
hard-link-nm: false
|
||||
nmHoistingLimits: workspaces
|
||||
- name: Build AFFiNE native
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
package: '@affine/native'
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: desktop-web
|
||||
path: packages/frontend/apps/electron/resources/web-static
|
||||
uses: ./.github/workflows/release-desktop-platform.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: ${{ inputs.build-type }}
|
||||
app_version: ${{ inputs.app-version }}
|
||||
git_short_hash: ${{ inputs.git-short-hash }}
|
||||
runner: ${{ matrix.spec.runner }}
|
||||
platform: ${{ matrix.spec.platform }}
|
||||
arch: ${{ matrix.spec.arch }}
|
||||
target: ${{ matrix.spec.target }}
|
||||
install_linux_deps: true
|
||||
|
||||
- name: Build Desktop Layers
|
||||
run: yarn affine @affine/electron build
|
||||
package-distribution-windows-x64:
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs: before-make
|
||||
uses: ./.github/workflows/release-desktop-platform.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: ${{ inputs.build-type }}
|
||||
app_version: ${{ inputs.app-version }}
|
||||
git_short_hash: ${{ inputs.git-short-hash }}
|
||||
runner: windows-latest
|
||||
platform: win32
|
||||
arch: x64
|
||||
target: x86_64-pc-windows-msvc
|
||||
enable_scripts: true
|
||||
|
||||
- name: Remove nbstore node_modules
|
||||
shell: bash
|
||||
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
|
||||
run: |
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules
|
||||
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
|
||||
|
||||
- name: package
|
||||
run: |
|
||||
yarn affine @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||
env:
|
||||
SKIP_WEB_BUILD: 1
|
||||
HOIST_NODE_MODULES: 1
|
||||
NODE_OPTIONS: --max-old-space-size=14384
|
||||
|
||||
- name: get all files to be signed
|
||||
id: get_files_to_be_signed
|
||||
run: |
|
||||
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
|
||||
"FILES_TO_BE_SIGNED_${{ matrix.spec.arch }}=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
|
||||
echo $FILES_TO_BE_SIGNED
|
||||
|
||||
- name: Zip artifacts for faster upload
|
||||
run: Compress-Archive -CompressionLevel Fastest -Path packages/frontend/apps/electron/out/* -DestinationPath archive.zip
|
||||
|
||||
- name: Save packaged artifacts for signing
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||
path: |
|
||||
archive.zip
|
||||
!**/*.map
|
||||
package-distribution-windows-arm64:
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs: before-make
|
||||
uses: ./.github/workflows/release-desktop-platform.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build_type: ${{ inputs.build-type }}
|
||||
app_version: ${{ inputs.app-version }}
|
||||
git_short_hash: ${{ inputs.git-short-hash }}
|
||||
runner: windows-latest
|
||||
platform: win32
|
||||
arch: arm64
|
||||
target: aarch64-pc-windows-msvc
|
||||
enable_scripts: true
|
||||
|
||||
sign-packaged-artifacts-windows_x64:
|
||||
needs: package-distribution-windows
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs: package-distribution-windows-x64
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_x64 }}
|
||||
files: ${{ needs.package-distribution-windows-x64.outputs.files_to_be_signed }}
|
||||
artifact-name: packaged-win32-x64
|
||||
|
||||
sign-packaged-artifacts-windows_arm64:
|
||||
needs: package-distribution-windows
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs: package-distribution-windows-arm64
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_arm64 }}
|
||||
files: ${{ needs.package-distribution-windows-arm64.outputs.files_to_be_signed }}
|
||||
artifact-name: packaged-win32-arm64
|
||||
|
||||
make-windows-installer:
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs:
|
||||
- sign-packaged-artifacts-windows_x64
|
||||
- sign-packaged-artifacts-windows_arm64
|
||||
@@ -349,6 +233,7 @@ jobs:
|
||||
path: archive.zip
|
||||
|
||||
sign-installer-artifacts-windows-x64:
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs: make-windows-installer
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
@@ -356,6 +241,7 @@ jobs:
|
||||
artifact-name: installer-win32-x64
|
||||
|
||||
sign-installer-artifacts-windows-arm64:
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs: make-windows-installer
|
||||
uses: ./.github/workflows/windows-signer.yml
|
||||
with:
|
||||
@@ -363,6 +249,7 @@ jobs:
|
||||
artifact-name: installer-win32-arm64
|
||||
|
||||
finalize-installer-windows:
|
||||
if: ${{ inputs.desktop_windows }}
|
||||
needs:
|
||||
[
|
||||
sign-installer-artifacts-windows-x64,
|
||||
@@ -410,17 +297,18 @@ jobs:
|
||||
path: builds
|
||||
|
||||
release:
|
||||
needs: [before-make, make-distribution, finalize-installer-windows]
|
||||
if: ${{ inputs.desktop_macos && inputs.desktop_linux && inputs.desktop_windows }}
|
||||
needs:
|
||||
[
|
||||
before-make,
|
||||
make-distribution-macos,
|
||||
make-distribution-linux,
|
||||
finalize-installer-windows,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: desktop-web
|
||||
path: web-static
|
||||
- name: Zip web-static
|
||||
run: zip -r web-static.zip web-static
|
||||
- name: Download Artifacts (macos-x64)
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
|
||||
92
.github/workflows/release.yml
vendored
92
.github/workflows/release.yml
vendored
@@ -11,8 +11,18 @@ on:
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
desktop:
|
||||
description: 'Release Desktop?'
|
||||
desktop_macos:
|
||||
description: 'Desktop - macOS'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
desktop_windows:
|
||||
description: 'Desktop - Windows'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
desktop_linux:
|
||||
description: 'Desktop - Linux'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -50,6 +60,68 @@ jobs:
|
||||
id: prepare
|
||||
uses: ./.github/actions/prepare-release
|
||||
|
||||
canary-gate:
|
||||
name: Canary Gate
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
outputs:
|
||||
SHOULD_RELEASE: ${{ steps.decide.outputs.SHOULD_RELEASE }}
|
||||
LAST_CANARY_TAG: ${{ steps.decide.outputs.LAST_CANARY_TAG }}
|
||||
LAST_CANARY_SHA: ${{ steps.decide.outputs.LAST_CANARY_SHA }}
|
||||
steps:
|
||||
- name: Decide whether to release
|
||||
id: decide
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const buildType = '${{ needs.prepare.outputs.BUILD_TYPE }}'
|
||||
if (buildType !== 'canary') {
|
||||
core.setOutput('SHOULD_RELEASE', 'true')
|
||||
return
|
||||
}
|
||||
|
||||
const owner = context.repo.owner
|
||||
const repo = context.repo.repo
|
||||
const currentSha = context.sha
|
||||
const canaryTagRe = /^v\d+\.\d+\.\d+-canary\.[0-9a-f]+$/i
|
||||
|
||||
let page = 1
|
||||
const perPage = 100
|
||||
let lastCanary = null
|
||||
|
||||
while (!lastCanary && page <= 10) {
|
||||
const { data } = await github.rest.repos.listTags({
|
||||
owner,
|
||||
repo,
|
||||
per_page: perPage,
|
||||
page,
|
||||
})
|
||||
|
||||
for (const tag of data) {
|
||||
if (canaryTagRe.test(tag.name)) {
|
||||
lastCanary = tag
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length < perPage) break
|
||||
page++
|
||||
}
|
||||
|
||||
if (!lastCanary) {
|
||||
core.warning('No canary tags found; proceeding with canary release.')
|
||||
core.setOutput('SHOULD_RELEASE', 'true')
|
||||
return
|
||||
}
|
||||
|
||||
core.setOutput('LAST_CANARY_TAG', lastCanary.name)
|
||||
core.setOutput('LAST_CANARY_SHA', lastCanary.commit.sha)
|
||||
|
||||
const shouldRelease = lastCanary.commit.sha !== currentSha
|
||||
core.info(`Latest canary tag ${lastCanary.name} -> ${lastCanary.commit.sha}; current ${currentSha}; should_release=${shouldRelease}`)
|
||||
core.setOutput('SHOULD_RELEASE', shouldRelease ? 'true' : 'false')
|
||||
|
||||
cloud:
|
||||
name: Release Cloud
|
||||
if: ${{ inputs.web || github.event_name != 'workflow_dispatch' }}
|
||||
@@ -64,9 +136,11 @@ jobs:
|
||||
|
||||
image:
|
||||
name: Release Docker Image
|
||||
if: ${{ needs.canary-gate.outputs.SHOULD_RELEASE == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
- canary-gate
|
||||
- cloud
|
||||
steps:
|
||||
- uses: trstringer/manual-approval@v1
|
||||
@@ -74,7 +148,7 @@ jobs:
|
||||
name: Wait for approval
|
||||
with:
|
||||
secret: ${{ secrets.GITHUB_TOKEN }}
|
||||
approvers: forehalo,fengmk2,darkskygit
|
||||
approvers: darkskygit,pengx17,L-Sun,EYHN
|
||||
minimum-approvals: 1
|
||||
fail-on-denial: true
|
||||
issue-title: Please confirm to release docker image
|
||||
@@ -102,15 +176,25 @@ jobs:
|
||||
|
||||
desktop:
|
||||
name: Release Desktop
|
||||
if: ${{ inputs.desktop || github.event_name != 'workflow_dispatch' }}
|
||||
if: >-
|
||||
${{
|
||||
(github.event_name != 'workflow_dispatch' && needs.canary-gate.outputs.SHOULD_RELEASE == 'true') ||
|
||||
inputs.desktop_macos ||
|
||||
inputs.desktop_windows ||
|
||||
inputs.desktop_linux
|
||||
}}
|
||||
needs:
|
||||
- prepare
|
||||
- canary-gate
|
||||
uses: ./.github/workflows/release-desktop.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
build-type: ${{ needs.prepare.outputs.BUILD_TYPE }}
|
||||
app-version: ${{ needs.prepare.outputs.APP_VERSION }}
|
||||
git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }}
|
||||
desktop_macos: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_macos }}
|
||||
desktop_windows: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_windows }}
|
||||
desktop_linux: ${{ github.event_name != 'workflow_dispatch' || inputs.desktop_linux }}
|
||||
|
||||
mobile:
|
||||
name: Release Mobile
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -47,6 +47,8 @@ testem.log
|
||||
.pnpm-debug.log
|
||||
/typings
|
||||
tsconfig.tsbuildinfo
|
||||
rfc*.md
|
||||
todo.md
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,4 +12,4 @@ npmPublishAccess: public
|
||||
|
||||
npmRegistryServer: "https://registry.npmjs.org"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
|
||||
654
Cargo.lock
generated
654
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -47,10 +47,11 @@ resolver = "3"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
memory-indexer = "0.2.1"
|
||||
mimalloc = "0.1"
|
||||
mp4parse = "0.17"
|
||||
nanoid = "0.4"
|
||||
napi = { version = "3.0.0-beta.3", features = [
|
||||
napi = { version = "3.7.0", features = [
|
||||
"async",
|
||||
"chrono_date",
|
||||
"error_anyhow",
|
||||
@@ -58,11 +59,12 @@ resolver = "3"
|
||||
"serde",
|
||||
] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-beta.3" }
|
||||
napi-derive = { version = "3.4" }
|
||||
nom = "8"
|
||||
notify = { version = "8", features = ["serde"] }
|
||||
objc2 = "0.6"
|
||||
objc2-foundation = "0.3"
|
||||
ogg = "0.9"
|
||||
once_cell = "1"
|
||||
ordered-float = "5"
|
||||
parking_lot = "0.12"
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "packages/backend/server" directory of this repository, if that directory exists, is licensed under the license defined in "packages/backend/server/LICENSE".
|
||||
- All content that resides under the "packages/backend" and "packages/common/native" directory of this repository, if that directory exists, is licensed under the license defined in "packages/backend/server/LICENSE".
|
||||
- All third party components incorporated into the AFFiNE Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined in "LICENSE-MIT".
|
||||
|
||||
|
||||
@@ -193,6 +193,8 @@ We would like to express our gratitude to all the individuals who have already c
|
||||
|
||||
Begin with Docker to deploy your own feature-rich, unrestricted version of AFFiNE. Our team is diligently updating to the latest version. For more information on how to self-host AFFiNE, please refer to our [documentation](https://docs.affine.pro/self-host-affine).
|
||||
|
||||
[](https://sealos.io/products/app-store/affine)
|
||||
|
||||
[](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
|
||||
|
||||
## Hiring
|
||||
|
||||
@@ -6,15 +6,14 @@ We recommend users to always use the latest major version. Security updates will
|
||||
|
||||
| Version | Supported |
|
||||
| --------------- | ------------------ |
|
||||
| 0.24.x (stable) | :white_check_mark: |
|
||||
| < 0.24.x | :x: |
|
||||
| 0.25.x (stable) | :white_check_mark: |
|
||||
| < 0.25.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We welcome you to provide us with bug reports via and email at [security@toeverything.info](mailto:security@toeverything.info) or submit directly on [GitHub](https://github.com/toeverything/AFFiNE/security), **we encourage you to submit the relevant information directly via GitHub**. We expect your report to contain at least the following for us to evaluate and reproduce:
|
||||
|
||||
1. Using platform and version, for example:
|
||||
|
||||
- macos arm64 0.12.0-canary-202402220729-0868ac6
|
||||
- app.affine.pro 0.12.0-canary-202402220729-0868ac6
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@blocksuite/sync": "workspace:*",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -296,10 +296,10 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5",
|
||||
"version": "0.25.7",
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"msw": "^2.8.4",
|
||||
"vitest": "3.1.3"
|
||||
"msw": "^2.12.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2214,7 +2214,7 @@ describe('html to snapshot', () => {
|
||||
|
||||
test('iframe', async () => {
|
||||
const html = template(
|
||||
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&end=" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>`
|
||||
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&end=" title="YouTube video player" frameborder="0" allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>`
|
||||
);
|
||||
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc';
|
||||
import {
|
||||
DefaultTheme,
|
||||
NoteDisplayMode,
|
||||
@@ -16,12 +17,15 @@ import type {
|
||||
SliceSnapshot,
|
||||
TransformerMiddleware,
|
||||
} from '@blocksuite/store';
|
||||
import { AssetsManager, MemoryBlobCRUD } from '@blocksuite/store';
|
||||
import { AssetsManager, MemoryBlobCRUD, Schema } from '@blocksuite/store';
|
||||
import { TestWorkspace } from '@blocksuite/store/test';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { AffineSchemas } from '../../schemas.js';
|
||||
import { createJob } from '../utils/create-job.js';
|
||||
import { getProvider } from '../utils/get-provider.js';
|
||||
import { nanoidReplacement } from '../utils/nanoid-replacement.js';
|
||||
import { testStoreExtensions } from '../utils/store.js';
|
||||
|
||||
const provider = getProvider();
|
||||
|
||||
@@ -90,6 +94,39 @@ describe('snapshot to markdown', () => {
|
||||
expect(target.file).toBe(markdown);
|
||||
});
|
||||
|
||||
test('imports frontmatter metadata into doc meta', async () => {
|
||||
const schema = new Schema().register(AffineSchemas);
|
||||
const collection = new TestWorkspace();
|
||||
collection.storeExtensions = testStoreExtensions;
|
||||
collection.meta.initialize();
|
||||
|
||||
const markdown = `---
|
||||
title: Web developer
|
||||
created: 2018-04-12T09:51:00
|
||||
updated: 2018-04-12T10:00:00
|
||||
tags: [a, b]
|
||||
favorite: true
|
||||
---
|
||||
Hello world
|
||||
`;
|
||||
|
||||
const docId = await MarkdownTransformer.importMarkdownToDoc({
|
||||
collection,
|
||||
schema,
|
||||
markdown,
|
||||
fileName: 'fallback-title',
|
||||
extensions: testStoreExtensions,
|
||||
});
|
||||
|
||||
expect(docId).toBeTruthy();
|
||||
const meta = collection.meta.getDocMeta(docId!);
|
||||
expect(meta?.title).toBe('Web developer');
|
||||
expect(meta?.createDate).toBe(Date.parse('2018-04-12T09:51:00'));
|
||||
expect(meta?.updatedDate).toBe(Date.parse('2018-04-12T10:00:00'));
|
||||
expect(meta?.favorite).toBe(true);
|
||||
expect(meta?.tags).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
test('paragraph', async () => {
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
@@ -2996,6 +3033,50 @@ describe('markdown to snapshot', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('html inline color span imports to nearest supported text color', async () => {
|
||||
const markdown = `<span style="color: #00afde;">Hello</span>`;
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[0]',
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DefaultTheme.noteBackgrounColor,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[1]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'Hello',
|
||||
attributes: {
|
||||
color: 'var(--affine-v2-text-highlight-fg-blue)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mdAdapter = new MarkdownAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
|
||||
file: markdown,
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('paragraph', async () => {
|
||||
const markdown = `aaa
|
||||
|
||||
|
||||
1618
blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts
Normal file
1618
blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,9 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -41,5 +41,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.23",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
"@types/mdast": "^4.0.4",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
|
||||
import {
|
||||
type CalloutBlockModel,
|
||||
ParagraphBlockModel,
|
||||
type ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"shiki": "^3.0.0",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"shiki": "^3.19.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -48,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export const CODE_BLOCK_DEFAULT_DARK_THEME = import(
|
||||
'shiki/themes/dark-plus.mjs'
|
||||
);
|
||||
export const CODE_BLOCK_DEFAULT_LIGHT_THEME = import(
|
||||
'shiki/themes/light-plus.mjs'
|
||||
);
|
||||
export const CODE_BLOCK_DEFAULT_DARK_THEME =
|
||||
import('shiki/themes/dark-plus.mjs');
|
||||
export const CODE_BLOCK_DEFAULT_LIGHT_THEME =
|
||||
import('shiki/themes/light-plus.mjs');
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
"@types/mdast": "^4.0.4",
|
||||
"date-fns": "^4.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -48,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const ColumnClassMap: Record<string, string> = {
|
||||
typesCheckbox: 'checkbox',
|
||||
typesText: 'rich-text',
|
||||
typesTitle: 'title',
|
||||
typesDate: 'date',
|
||||
};
|
||||
|
||||
const NotionDatabaseToken = '.collection-content';
|
||||
@@ -165,7 +166,36 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche
|
||||
if (!column) {
|
||||
return;
|
||||
}
|
||||
if (HastUtils.querySelector(child, '.selected-value')) {
|
||||
|
||||
// Check for <time> element to find date field from Notion.
|
||||
if (HastUtils.querySelector(child, 'time')) {
|
||||
const timeElement = HastUtils.querySelector(child, 'time');
|
||||
let rawColumnData =
|
||||
HastUtils.getTextContent(timeElement).trim();
|
||||
|
||||
if (rawColumnData.startsWith('@')) {
|
||||
rawColumnData = rawColumnData.slice(1);
|
||||
}
|
||||
|
||||
const columnDate = new Date(rawColumnData);
|
||||
const timestamp = columnDate.getTime();
|
||||
|
||||
if (!Number.isNaN(timestamp)) {
|
||||
column.data = {};
|
||||
if (column.type !== 'date') {
|
||||
column.type = 'date';
|
||||
}
|
||||
row[column.id] = {
|
||||
columnId: column.id,
|
||||
value: timestamp,
|
||||
};
|
||||
} else {
|
||||
row[column.id] = {
|
||||
columnId: column.id,
|
||||
value: HastUtils.getTextContent(child),
|
||||
};
|
||||
}
|
||||
} else if (HastUtils.querySelector(child, '.selected-value')) {
|
||||
if (!('options' in column.data)) {
|
||||
column.data.options = [];
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ export class DatabaseTitle extends SignalWatcher(
|
||||
private readonly isFocus$ = signal(false);
|
||||
|
||||
private onPressEnterKey() {
|
||||
this.dataViewLogic.addRow?.('start');
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
get readonly$() {
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -39,5 +39,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -43,5 +43,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -49,5 +49,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
// Caches total bounds, includes all blocks and elements.
|
||||
private _cachedBounds: Bound | null = null;
|
||||
|
||||
private _hasRenderedSyncedView = false;
|
||||
private _hasInitedFitEffect = false;
|
||||
|
||||
private readonly _initEdgelessFitEffect = () => {
|
||||
const fitToContent = () => {
|
||||
if (this.isPageMode) return;
|
||||
@@ -558,8 +561,6 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
this._selectBlock();
|
||||
}
|
||||
});
|
||||
|
||||
this._initEdgelessFitEffect();
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
@@ -587,12 +588,21 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
|
||||
);
|
||||
}
|
||||
|
||||
!this._hasRenderedSyncedView && (this._hasRenderedSyncedView = true);
|
||||
|
||||
return this._renderSyncedView();
|
||||
}
|
||||
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
this.syncedDocCard?.requestUpdate();
|
||||
|
||||
if (!this._hasInitedFitEffect && this._hasRenderedSyncedView) {
|
||||
/* Register the resizeObserver AFTER syncdView viewport's own resizeObserver
|
||||
* so that viewport.onResize() use up-to-date boundingClientRect values */
|
||||
this._hasInitedFitEffect = true;
|
||||
this._initEdgelessFitEffect();
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -49,5 +49,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent<EmbedFigmaMode
|
||||
<div class="affine-embed-figma-iframe-container">
|
||||
<iframe
|
||||
src=${`https://www.figma.com/embed?embed_host=blocksuite&url=${url}`}
|
||||
allowfullscreen
|
||||
sandbox="allow-same-origin allow-scripts allow-presentation"
|
||||
allow="fullscreen"
|
||||
loading="lazy"
|
||||
credentialless
|
||||
></iframe>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import {
|
||||
type EmbedIframeUrlValidationOptions,
|
||||
validateEmbedIframeUrl,
|
||||
} from '../../utils';
|
||||
|
||||
const BILIBILI_DEFAULT_WIDTH_IN_SURFACE = 800;
|
||||
const BILIBILI_DEFAULT_HEIGHT_IN_SURFACE = 450;
|
||||
const BILIBILI_DEFAULT_HEIGHT_IN_NOTE = 450;
|
||||
const BILIBILI_DEFAULT_WIDTH_PERCENT = 100;
|
||||
|
||||
const bilibiliValidationOptions: EmbedIframeUrlValidationOptions = {
|
||||
protocols: ['https:'],
|
||||
hostnames: ['player.bilibili.com', 'www.bilibili.com', 'bilibili.com'],
|
||||
};
|
||||
|
||||
const biliPlayerValidationOptions: EmbedIframeUrlValidationOptions = {
|
||||
protocols: ['https:'],
|
||||
hostnames: ['player.bilibili.com'],
|
||||
};
|
||||
|
||||
const AV_REGEX = /av([0-9]+)/i;
|
||||
const BV_REGEX = /(BV[0-9A-Za-z]{10})/;
|
||||
|
||||
const extractAvid = (url: string) => {
|
||||
const match = url.match(AV_REGEX);
|
||||
return match ? match[1] : undefined;
|
||||
};
|
||||
|
||||
const extractBvid = (url: string) => {
|
||||
const match = url.match(BV_REGEX);
|
||||
return match ? match[1] : undefined;
|
||||
};
|
||||
|
||||
const buildBiliPlayerEmbedUrl = (url: string) => {
|
||||
// If the user pasted the embed URL directly, keep it
|
||||
if (validateEmbedIframeUrl(url, biliPlayerValidationOptions)) {
|
||||
return url;
|
||||
}
|
||||
const avid = extractAvid(url);
|
||||
if (avid) {
|
||||
const params = new URLSearchParams({
|
||||
aid: avid,
|
||||
autoplay: '0',
|
||||
});
|
||||
return `https://player.bilibili.com/player.html?${params.toString()}`;
|
||||
}
|
||||
const bvid = extractBvid(url);
|
||||
if (bvid) {
|
||||
const params = new URLSearchParams({
|
||||
bvid,
|
||||
autoplay: '0',
|
||||
});
|
||||
return `https://player.bilibili.com/player.html?${params.toString()}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const bilibiliConfig = {
|
||||
name: 'bilibili',
|
||||
match: (url: string) =>
|
||||
validateEmbedIframeUrl(url, bilibiliValidationOptions) &&
|
||||
(!!extractAvid(url) || !!extractBvid(url)),
|
||||
buildOEmbedUrl: buildBiliPlayerEmbedUrl,
|
||||
useOEmbedUrlDirectly: true,
|
||||
options: {
|
||||
widthInSurface: BILIBILI_DEFAULT_WIDTH_IN_SURFACE,
|
||||
heightInSurface: BILIBILI_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
heightInNote: BILIBILI_DEFAULT_HEIGHT_IN_NOTE,
|
||||
widthPercent: BILIBILI_DEFAULT_WIDTH_PERCENT,
|
||||
allow: 'clipboard-write; encrypted-media; picture-in-picture',
|
||||
sandbox: 'allow-same-origin allow-scripts',
|
||||
style: 'border: none; border-radius: 8px;',
|
||||
allowFullscreen: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const BilibiliEmbedConfig = EmbedIframeConfigExtension(bilibiliConfig);
|
||||
@@ -67,8 +67,9 @@ const genericConfig = {
|
||||
heightInNote: GENERIC_DEFAULT_HEIGHT_IN_NOTE,
|
||||
allowFullscreen: true,
|
||||
style: 'border: none; border-radius: 8px;',
|
||||
allow: 'clipboard-read; clipboard-write; picture-in-picture;',
|
||||
allow: '',
|
||||
referrerpolicy: 'no-referrer-when-downgrade',
|
||||
sandbox: 'allow-scripts',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BilibiliEmbedConfig } from './bilibili';
|
||||
import { ExcalidrawEmbedConfig } from './excalidraw';
|
||||
import { GenericEmbedConfig } from './generic';
|
||||
import { GoogleDocsEmbedConfig } from './google-docs';
|
||||
@@ -11,5 +12,6 @@ export const EmbedIframeConfigExtensions = [
|
||||
MiroEmbedConfig,
|
||||
ExcalidrawEmbedConfig,
|
||||
GoogleDocsEmbedConfig,
|
||||
BilibiliEmbedConfig,
|
||||
GenericEmbedConfig,
|
||||
];
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type ReadonlySignal,
|
||||
signal,
|
||||
} from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { html, nothing } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
@@ -45,6 +45,10 @@ import { safeGetIframeSrc } from './utils.js';
|
||||
|
||||
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
const TRUSTED_SANDBOX =
|
||||
'allow-same-origin allow-scripts allow-forms allow-presentation';
|
||||
const UNTRUSTED_SANDBOX = 'allow-scripts';
|
||||
|
||||
export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIframeBlockModel> {
|
||||
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
|
||||
() => ({
|
||||
@@ -89,6 +93,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
});
|
||||
|
||||
protected iframeOptions: IframeOptions | undefined = undefined;
|
||||
private currentConfigName: string | undefined;
|
||||
|
||||
get embedIframeService() {
|
||||
return this.std.get(EmbedIframeService);
|
||||
@@ -279,6 +284,10 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
const config = this.embedIframeService?.getConfig(url);
|
||||
if (config) {
|
||||
this.iframeOptions = config.options;
|
||||
this.currentConfigName = config.name;
|
||||
} else {
|
||||
this.iframeOptions = undefined;
|
||||
this.currentConfigName = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -328,26 +337,46 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
||||
referrerpolicy,
|
||||
scrolling,
|
||||
allowFullscreen,
|
||||
sandbox,
|
||||
} = this.iframeOptions ?? {};
|
||||
const width = `${widthPercent}%`;
|
||||
// if the block is in the surface, use 100% as the height
|
||||
// otherwise, use the heightInNote
|
||||
const height = this.inSurface ? '100%' : heightInNote;
|
||||
return html`
|
||||
<iframe
|
||||
const sandboxValue =
|
||||
sandbox ??
|
||||
(this.currentConfigName === 'generic'
|
||||
? UNTRUSTED_SANDBOX
|
||||
: TRUSTED_SANDBOX);
|
||||
const sourceHost = this._getSourceHost();
|
||||
return html`<iframe
|
||||
width=${width ?? DEFAULT_IFRAME_WIDTH}
|
||||
height=${height ?? DEFAULT_IFRAME_HEIGHT}
|
||||
?allowfullscreen=${allowFullscreen}
|
||||
loading="lazy"
|
||||
frameborder="0"
|
||||
credentialless
|
||||
sandbox=${sandboxValue}
|
||||
src=${ifDefined(iframeUrl)}
|
||||
allow=${ifDefined(allow)}
|
||||
referrerpolicy=${ifDefined(referrerpolicy)}
|
||||
scrolling=${ifDefined(scrolling)}
|
||||
style=${ifDefined(style)}
|
||||
></iframe>
|
||||
`;
|
||||
${sourceHost
|
||||
? html`<div class="affine-embed-iframe-source">${sourceHost}</div>`
|
||||
: nothing}`;
|
||||
};
|
||||
|
||||
private readonly _getSourceHost = () => {
|
||||
const url = this.model.props.url ?? this.model.props.iframeUrl;
|
||||
if (!url) return null;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.hostname;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _renderContent = () => {
|
||||
|
||||
@@ -23,6 +23,19 @@ export const embedIframeBlockStyles = css`
|
||||
height: 100%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.affine-embed-iframe-source {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.affine-embed-iframe-block-overlay.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,8 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent<
|
||||
<iframe
|
||||
src=${`https://www.loom.com/embed/${videoId}?hide_title=true`}
|
||||
frameborder="0"
|
||||
allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
|
||||
sandbox="allow-scripts allow-same-origin allow-presentation"
|
||||
loading="lazy"
|
||||
credentialless
|
||||
></iframe>
|
||||
|
||||
@@ -148,8 +148,8 @@ export class EmbedYoutubeBlockComponent extends EmbedBlockComponent<
|
||||
type="text/html"
|
||||
src=${`https://www.youtube.com/embed/${videoId}`}
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
|
||||
sandbox="allow-scripts allow-same-origin allow-presentation"
|
||||
loading="lazy"
|
||||
credentialless
|
||||
></iframe>
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -44,5 +44,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"katex": "^0.16.11",
|
||||
"katex": "^0.16.27",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ListBlockModel } from '@blocksuite/affine-model';
|
||||
import { getNumberPrefix } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
BulletedList01Icon,
|
||||
BulletedList02Icon,
|
||||
@@ -11,8 +12,6 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { getNumberPrefix } from './get-number-prefix.js';
|
||||
|
||||
const getListDeep = (model: ListBlockModel): number => {
|
||||
let deep = 0;
|
||||
let parent = model.store.getParent(model);
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -49,5 +49,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -82,12 +82,13 @@ function createConversionItem(
|
||||
config: TextConversionConfig,
|
||||
group?: SlashMenuItem['group']
|
||||
): SlashMenuActionItem {
|
||||
const { name, description, icon, flavour, type } = config;
|
||||
const { name, description, icon, flavour, type, searchAlias = [] } = config;
|
||||
return {
|
||||
name,
|
||||
group,
|
||||
description,
|
||||
icon,
|
||||
searchAlias,
|
||||
tooltip: tooltips[name],
|
||||
when: ({ model }) => model.store.schema.flavourSchemaMap.has(flavour),
|
||||
action: ({ std }) => {
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"minimatch": "^10.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -67,5 +67,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^5.0.7",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
"nanoid": "^5.1.6",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -45,5 +45,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^5.0.7",
|
||||
"nanoid": "^5.1.6",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -46,5 +46,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class CanvasRenderer {
|
||||
* It is not recommended to set width and height to 100%.
|
||||
*/
|
||||
private _canvasSizeUpdater(dpr = window.devicePixelRatio) {
|
||||
const { width, height } = this.viewport;
|
||||
const { width, height, viewScale } = this.viewport;
|
||||
const actualWidth = Math.ceil(width * dpr);
|
||||
const actualHeight = Math.ceil(height * dpr);
|
||||
|
||||
@@ -124,6 +124,8 @@ export class CanvasRenderer {
|
||||
update(canvas: HTMLCanvasElement) {
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
canvas.style.transform = `scale(${1 / viewScale})`;
|
||||
canvas.style.transformOrigin = `top left`;
|
||||
canvas.width = actualWidth;
|
||||
canvas.height = actualHeight;
|
||||
},
|
||||
|
||||
@@ -8,9 +8,8 @@ import type { RoughCanvas } from '../../index.js';
|
||||
import type { CanvasRenderer } from '../canvas-renderer.js';
|
||||
|
||||
export type ElementRenderer<
|
||||
T extends
|
||||
| GfxPrimitiveElementModel
|
||||
| GfxLocalElementModel = GfxPrimitiveElementModel,
|
||||
T extends GfxPrimitiveElementModel | GfxLocalElementModel =
|
||||
GfxPrimitiveElementModel,
|
||||
> = (
|
||||
model: T,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-inline-preset": "workspace:*",
|
||||
@@ -27,9 +27,9 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.24.1"
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -42,5 +42,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -418,6 +418,7 @@ export class TableCell extends SignalWatcher(
|
||||
name: 'Paste',
|
||||
prefix: PasteIcon(),
|
||||
select: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
navigator.clipboard.readText().then(text => {
|
||||
this.selectionController.doPaste(text, selected);
|
||||
});
|
||||
|
||||
@@ -28,15 +28,15 @@
|
||||
"@types/mdast": "^4.0.4",
|
||||
"collapse-white-space": "^2.1.0",
|
||||
"date-fns": "^4.0.0",
|
||||
"katex": "^0.16.11",
|
||||
"katex": "^0.16.27",
|
||||
"lit": "^3.2.0",
|
||||
"lit-html": "^3.2.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"remark-math": "^6.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"shiki": "^3.0.0",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"rxjs": "^7.8.2",
|
||||
"shiki": "^3.19.0",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -82,5 +82,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export type MenuButtonData = {
|
||||
select: (ele: HTMLElement) => void | false;
|
||||
onHover?: (hover: boolean) => void;
|
||||
testId?: string;
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export class MenuButton extends MenuFocusable {
|
||||
@@ -85,7 +86,9 @@ export class MenuButton extends MenuFocusable {
|
||||
onClick() {
|
||||
if (this.data.select(this) !== false) {
|
||||
this.menu.options.onComplete?.();
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +153,9 @@ export class MobileMenuButton extends MenuFocusable {
|
||||
onClick() {
|
||||
if (this.data.select(this) !== false) {
|
||||
this.menu.options.onComplete?.();
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +205,7 @@ export const menuButtonItems = {
|
||||
select: (ele: HTMLElement) => void | false;
|
||||
onHover?: (hover: boolean) => void;
|
||||
class?: MenuClass;
|
||||
closeOnSelect?: boolean;
|
||||
hide?: () => boolean;
|
||||
testId?: string;
|
||||
}) =>
|
||||
@@ -219,6 +225,7 @@ export const menuButtonItems = {
|
||||
},
|
||||
onHover: config.onHover,
|
||||
select: config.select,
|
||||
closeOnSelect: config.closeOnSelect,
|
||||
class: {
|
||||
'selected-item': config.isSelected ?? false,
|
||||
...config.class,
|
||||
|
||||
@@ -17,6 +17,7 @@ export type MenuInputData = {
|
||||
class?: string;
|
||||
onComplete?: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
onBlur?: (value: string) => void;
|
||||
disableAutoFocus?: boolean;
|
||||
};
|
||||
|
||||
@@ -49,6 +50,10 @@ export class MenuInput extends MenuFocusable {
|
||||
this.data.onChange?.(this.inputRef.value);
|
||||
};
|
||||
|
||||
private readonly onBlur = () => {
|
||||
this.data.onBlur?.(this.inputRef.value);
|
||||
};
|
||||
|
||||
private readonly onInput = (e: InputEvent) => {
|
||||
e.stopPropagation();
|
||||
if (e.isComposing) return;
|
||||
@@ -109,6 +114,7 @@ export class MenuInput extends MenuFocusable {
|
||||
@focus="${() => {
|
||||
this.menu.setFocusOnly(this);
|
||||
}}"
|
||||
@blur="${this.onBlur}"
|
||||
@input="${this.onInput}"
|
||||
placeholder="${this.data.placeholder ?? ''}"
|
||||
@keypress="${this.stopPropagation}"
|
||||
@@ -215,6 +221,7 @@ export const menuInputItems = {
|
||||
prefix?: TemplateResult;
|
||||
onComplete?: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
onBlur?: (value: string) => void;
|
||||
class?: string;
|
||||
style?: Readonly<StyleInfo>;
|
||||
}) =>
|
||||
@@ -228,6 +235,7 @@ export const menuInputItems = {
|
||||
class: config.class,
|
||||
onComplete: config.onComplete,
|
||||
onChange: config.onChange,
|
||||
onBlur: config.onBlur,
|
||||
};
|
||||
const style = styleMap({
|
||||
display: 'flex',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
computePosition,
|
||||
type Middleware,
|
||||
offset,
|
||||
type Placement,
|
||||
type ReferenceElement,
|
||||
shift,
|
||||
} from '@floating-ui/dom';
|
||||
@@ -37,7 +38,9 @@ export class MenuComponent
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
min-width: 180px;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
max-height: 700px;
|
||||
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
||||
border-radius: 4px;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
@@ -439,6 +442,7 @@ export const createPopup = (
|
||||
onClose?: () => void;
|
||||
middleware?: Array<Middleware | null | undefined | false>;
|
||||
container?: HTMLElement;
|
||||
placement?: Placement;
|
||||
}
|
||||
) => {
|
||||
const close = () => {
|
||||
@@ -448,6 +452,7 @@ export const createPopup = (
|
||||
const modal = createModal(target.root);
|
||||
autoUpdate(target.targetRect, content, () => {
|
||||
computePosition(target.targetRect, content, {
|
||||
placement: options?.placement,
|
||||
middleware: options?.middleware ?? [shift({ crossAxis: true })],
|
||||
})
|
||||
.then(({ x, y }) => {
|
||||
@@ -520,6 +525,7 @@ export const popMenu = (
|
||||
options: MenuOptions;
|
||||
middleware?: Array<Middleware | null | undefined | false>;
|
||||
container?: HTMLElement;
|
||||
placement?: Placement;
|
||||
}
|
||||
): MenuHandler => {
|
||||
if (IS_MOBILE) {
|
||||
@@ -551,6 +557,7 @@ export const popMenu = (
|
||||
offset(4),
|
||||
],
|
||||
container: props.container,
|
||||
placement: props.placement,
|
||||
});
|
||||
return {
|
||||
close: closePopup,
|
||||
@@ -563,12 +570,14 @@ export const popMenu = (
|
||||
export const popFilterableSimpleMenu = (
|
||||
target: PopupTarget,
|
||||
options: MenuConfig[],
|
||||
onClose?: () => void
|
||||
onClose?: () => void,
|
||||
placement: Placement = 'bottom-start'
|
||||
) => {
|
||||
popMenu(target, {
|
||||
options: {
|
||||
items: options,
|
||||
onClose,
|
||||
},
|
||||
placement,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,12 +4,15 @@ import {
|
||||
autoPlacement,
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
type Middleware,
|
||||
offset,
|
||||
shift,
|
||||
} from '@floating-ui/dom';
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import { MenuButton } from './button.js';
|
||||
import { MenuFocusable } from './focusable.js';
|
||||
import { Menu, type MenuOptions } from './menu.js';
|
||||
import { popMenu, popupTargetFromElement } from './menu-renderer.js';
|
||||
@@ -20,29 +23,55 @@ export type MenuSubMenuData = {
|
||||
options: MenuOptions;
|
||||
select?: () => void;
|
||||
class?: string;
|
||||
openOnHover?: boolean;
|
||||
middleware?: Middleware[];
|
||||
autoHeight?: boolean;
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
export const subMenuOffset = offset({
|
||||
mainAxis: 16,
|
||||
crossAxis: -8.5,
|
||||
crossAxis: 0,
|
||||
});
|
||||
export const subMenuPlacements = autoPlacement({
|
||||
allowedPlacements: ['right-start', 'left-start', 'right-end', 'left-end'],
|
||||
allowedPlacements: ['bottom-end'],
|
||||
});
|
||||
export const subMenuMiddleware = [subMenuOffset, subMenuPlacements];
|
||||
|
||||
export const dropdownSubMenuMiddleware = [
|
||||
autoPlacement({ allowedPlacements: ['bottom-end'] }),
|
||||
offset({ mainAxis: 8, crossAxis: 0 }),
|
||||
shift({ crossAxis: true }),
|
||||
];
|
||||
|
||||
export class MenuSubMenu extends MenuFocusable {
|
||||
static override styles = [
|
||||
MenuButton.styles,
|
||||
css`
|
||||
.affine-menu-button svg:last-child {
|
||||
transition: transform 150ms cubic-bezier(0.42, 0, 1, 1);
|
||||
}
|
||||
affine-menu-sub-menu.active .affine-menu-button svg:last-child {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
createTime = 0;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.createTime = Date.now();
|
||||
this.disposables.addFromEvent(this, 'mouseenter', this.onMouseEnter);
|
||||
if (this.data.openOnHover !== false) {
|
||||
this.disposables.addFromEvent(this, 'mouseenter', this.onMouseEnter);
|
||||
}
|
||||
this.disposables.addFromEvent(this, 'click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.data.select) {
|
||||
this.data.select();
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
} else {
|
||||
this.openSubMenu();
|
||||
}
|
||||
@@ -60,11 +89,38 @@ export class MenuSubMenu extends MenuFocusable {
|
||||
}
|
||||
|
||||
openSubMenu() {
|
||||
if (this.data.openOnHover === false) {
|
||||
const { menu } = popMenu(popupTargetFromElement(this), {
|
||||
options: {
|
||||
...this.data.options,
|
||||
onComplete: () => {
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
menu.menuElement.remove();
|
||||
this.data.options.onClose?.();
|
||||
},
|
||||
},
|
||||
middleware: this.data.middleware,
|
||||
});
|
||||
if (this.data.autoHeight) {
|
||||
menu.menuElement.style.minHeight = 'fit-content';
|
||||
menu.menuElement.style.maxHeight = 'fit-content';
|
||||
}
|
||||
menu.menuElement.style.minWidth = '200px';
|
||||
this.menu.openSubMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
const focus = this.menu.currentFocused$.value;
|
||||
const menu = new Menu({
|
||||
...this.data.options,
|
||||
onComplete: () => {
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
menu.menuElement.remove();
|
||||
@@ -74,9 +130,14 @@ export class MenuSubMenu extends MenuFocusable {
|
||||
},
|
||||
});
|
||||
this.menu.menuElement.parentElement?.append(menu.menuElement);
|
||||
if (this.data.autoHeight) {
|
||||
menu.menuElement.style.minHeight = 'fit-content';
|
||||
menu.menuElement.style.maxHeight = 'fit-content';
|
||||
}
|
||||
menu.menuElement.style.minWidth = '200px';
|
||||
const unsub = autoUpdate(this, menu.menuElement, () => {
|
||||
computePosition(this, menu.menuElement, {
|
||||
middleware: subMenuMiddleware,
|
||||
middleware: this.data.middleware ?? subMenuMiddleware,
|
||||
})
|
||||
.then(({ x, y }) => {
|
||||
menu.menuElement.style.left = `${x}px`;
|
||||
@@ -125,14 +186,22 @@ export class MobileSubMenu extends MenuFocusable {
|
||||
options: {
|
||||
...this.data.options,
|
||||
onComplete: () => {
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
menu.menuElement.remove();
|
||||
this.data.options.onClose?.();
|
||||
},
|
||||
},
|
||||
middleware: this.data.middleware,
|
||||
});
|
||||
if (this.data.autoHeight) {
|
||||
menu.menuElement.style.minHeight = 'fit-content';
|
||||
menu.menuElement.style.maxHeight = 'fit-content';
|
||||
}
|
||||
menu.menuElement.style.minWidth = '200px';
|
||||
this.menu.openSubMenu(menu);
|
||||
}
|
||||
|
||||
@@ -175,6 +244,10 @@ export const subMenuItems = {
|
||||
options: MenuOptions;
|
||||
disableArrow?: boolean;
|
||||
hide?: () => boolean;
|
||||
openOnHover?: boolean;
|
||||
middleware?: Middleware[];
|
||||
autoHeight?: boolean;
|
||||
closeOnSelect?: boolean;
|
||||
}) =>
|
||||
menu => {
|
||||
if (config.hide?.() || !menu.search(config.name)) {
|
||||
@@ -190,6 +263,10 @@ export const subMenuItems = {
|
||||
${config.disableArrow ? nothing : ArrowRightSmallIcon()} `,
|
||||
class: config.class,
|
||||
options: config.options,
|
||||
openOnHover: config.openOnHover,
|
||||
middleware: config.middleware,
|
||||
autoHeight: config.autoHeight,
|
||||
closeOnSelect: config.closeOnSelect,
|
||||
};
|
||||
return renderSubMenu(data, menu);
|
||||
},
|
||||
|
||||
@@ -27,10 +27,12 @@
|
||||
"date-fns": "^4.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rxjs": "^7.8.1",
|
||||
"vitest": "^3.2.3",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
"rxjs": "^7.8.2",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
@@ -46,5 +48,5 @@
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.25.5"
|
||||
"version": "0.25.7"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { compareDateKeys } from '../core/group-by/compare-date-keys.js';
|
||||
|
||||
describe('compareDateKeys', () => {
|
||||
it('sorts relative keys ascending', () => {
|
||||
const cmp = compareDateKeys('date-relative', true);
|
||||
const keys = ['today', 'last7', 'yesterday', 'last30'];
|
||||
const sorted = [...keys].sort(cmp);
|
||||
expect(sorted).toEqual(['last30', 'last7', 'yesterday', 'today']);
|
||||
});
|
||||
|
||||
it('sorts relative keys descending', () => {
|
||||
const cmp = compareDateKeys('date-relative', false);
|
||||
const keys = ['today', 'last7', 'yesterday', 'last30'];
|
||||
const sorted = [...keys].sort(cmp);
|
||||
expect(sorted).toEqual(['today', 'yesterday', 'last7', 'last30']);
|
||||
});
|
||||
|
||||
it('sorts numeric keys correctly', () => {
|
||||
const asc = compareDateKeys('date-day', true);
|
||||
const desc = compareDateKeys('date-day', false);
|
||||
const keys = ['3', '1', '2'];
|
||||
expect([...keys].sort(asc)).toEqual(['1', '2', '3']);
|
||||
expect([...keys].sort(desc)).toEqual(['3', '2', '1']);
|
||||
});
|
||||
|
||||
it('handles mixed relative and numeric keys', () => {
|
||||
const cmp = compareDateKeys('date-relative', true);
|
||||
const keys = ['today', '1', 'yesterday', '2'];
|
||||
const sorted = [...keys].sort(cmp);
|
||||
expect(sorted[0]).toBe('1');
|
||||
expect(sorted[sorted.length - 1]).toBe('today');
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { InvisibleIcon, ViewIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import type { Middleware } from '@floating-ui/dom';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
@@ -235,13 +236,16 @@ export const popPropertiesSetting = (
|
||||
view: SingleView;
|
||||
onClose?: () => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
},
|
||||
middleware?: Array<Middleware | null | undefined | false>
|
||||
) => {
|
||||
popMenu(target, {
|
||||
const handler = popMenu(target, {
|
||||
middleware,
|
||||
options: {
|
||||
title: {
|
||||
text: 'Properties',
|
||||
onBack: props.onBack,
|
||||
onClose: props.onClose,
|
||||
postfix: () => {
|
||||
const items = props.view.propertiesRaw$.value;
|
||||
const isAllShowed = items.every(property => !property.hide$.value);
|
||||
@@ -270,8 +274,10 @@ export const popPropertiesSetting = (
|
||||
],
|
||||
}),
|
||||
],
|
||||
onClose: props.onClose,
|
||||
},
|
||||
});
|
||||
handler.menu.menuElement.style.minHeight = '550px';
|
||||
|
||||
// const view = new DataViewPropertiesSettingView();
|
||||
// view.view = props.view;
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
import { menu } from '@blocksuite/affine-components/context-menu';
|
||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { renderUniLit } from '../utils/uni-component/index.js';
|
||||
import type { Property } from '../view-manager/property.js';
|
||||
|
||||
export const inputConfig = (property: Property) => {
|
||||
if (IS_MOBILE) {
|
||||
return menu.input({
|
||||
prefix: html`
|
||||
<div class="affine-database-column-type-menu-icon">
|
||||
${renderUniLit(property.icon)}
|
||||
</div>
|
||||
`,
|
||||
initialValue: property.name$.value,
|
||||
placeholder: 'Property name',
|
||||
onChange: text => {
|
||||
property.nameSet(text);
|
||||
},
|
||||
});
|
||||
}
|
||||
return menu.input({
|
||||
prefix: html`
|
||||
<div class="affine-database-column-type-menu-icon">
|
||||
@@ -28,7 +13,7 @@ export const inputConfig = (property: Property) => {
|
||||
`,
|
||||
initialValue: property.name$.value,
|
||||
placeholder: 'Property name',
|
||||
onComplete: text => {
|
||||
onBlur: text => {
|
||||
property.nameSet(text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ export type GroupBy = {
|
||||
type: 'groupBy';
|
||||
columnId: string;
|
||||
name: string;
|
||||
hideEmpty?: boolean;
|
||||
sort?: {
|
||||
desc: boolean;
|
||||
};
|
||||
|
||||
@@ -484,6 +484,18 @@ const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
|
||||
const onInput = (e: InputEvent) => {
|
||||
tagManager.text$.value = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
const onKeydown = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
const inputValue = (e.target as HTMLInputElement).value.trim();
|
||||
if (e.key === 'Backspace' && inputValue === '') {
|
||||
const values = tagManager.value$.value;
|
||||
const lastId = values[values.length - 1];
|
||||
if (lastId) {
|
||||
e.preventDefault();
|
||||
tagManager.deleteTag(lastId);
|
||||
}
|
||||
}
|
||||
};
|
||||
return popMenu(target, {
|
||||
options: {
|
||||
onClose: () => {
|
||||
@@ -511,11 +523,21 @@ const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
|
||||
});
|
||||
return html` <div class="${tagContainerStyle}" style=${style}>
|
||||
<div class="${tagTextStyle}">${option.value}</div>
|
||||
<div
|
||||
class="${tagDeleteIconStyle}"
|
||||
@click="${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
tagManager.deleteTag(id);
|
||||
}}"
|
||||
>
|
||||
${CloseIcon()}
|
||||
</div>
|
||||
</div>`;
|
||||
})}
|
||||
<input
|
||||
.value="${tagManager.text$.value}"
|
||||
@input="${onInput}"
|
||||
@keydown="${onKeydown}"
|
||||
placeholder="Type here..."
|
||||
type="text"
|
||||
style="outline: none;border: none;flex:1;min-width: 10px"
|
||||
|
||||
@@ -24,7 +24,7 @@ export const popCreateFilter = (
|
||||
middleware?: Middleware[];
|
||||
}
|
||||
) => {
|
||||
popMenu(target, {
|
||||
const subHandler = popMenu(target, {
|
||||
middleware: ops?.middleware,
|
||||
options: {
|
||||
onClose: props.onClose,
|
||||
@@ -64,4 +64,5 @@ export const popCreateFilter = (
|
||||
],
|
||||
},
|
||||
});
|
||||
subHandler.menu.menuElement.style.minHeight = '550px';
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export const allLiteralConfig: LiteralItemsConfig[] = [
|
||||
() => {
|
||||
return html` <date-picker
|
||||
.padding="${8}"
|
||||
.size="${20}"
|
||||
.value="${value.value}"
|
||||
.onChange="${(date: Date) => {
|
||||
onChange(date.getTime());
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
export const RELATIVE_ASC = [
|
||||
'last30',
|
||||
'last7',
|
||||
'yesterday',
|
||||
'today',
|
||||
'tomorrow',
|
||||
'next7',
|
||||
'next30',
|
||||
] as const;
|
||||
export const RELATIVE_DESC = [...RELATIVE_ASC].reverse();
|
||||
|
||||
/**
|
||||
* Sorts relative date keys in chronological order
|
||||
*/
|
||||
export function sortRelativeKeys(a: string, b: string, asc: boolean): number {
|
||||
const order: readonly string[] = asc ? RELATIVE_ASC : RELATIVE_DESC;
|
||||
const idxA = order.indexOf(a);
|
||||
const idxB = order.indexOf(b);
|
||||
|
||||
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
||||
if (idxA !== -1) return asc ? 1 : -1;
|
||||
if (idxB !== -1) return asc ? -1 : 1;
|
||||
|
||||
return 0; // Both not found
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts numeric date keys (timestamps)
|
||||
*/
|
||||
export function sortNumericKeys(a: string, b: string, asc: boolean): number {
|
||||
const na = Number(a);
|
||||
const nb = Number(b);
|
||||
|
||||
if (Number.isFinite(na) && Number.isFinite(nb)) {
|
||||
return asc ? na - nb : nb - na;
|
||||
}
|
||||
|
||||
return 0; // Not both numeric
|
||||
}
|
||||
|
||||
export function compareDateKeys(mode: string | undefined, asc: boolean) {
|
||||
return (a: string, b: string) => {
|
||||
if (mode === 'date-relative') {
|
||||
// Try relative key sorting first
|
||||
const relativeResult = sortRelativeKeys(a, b, asc);
|
||||
if (relativeResult !== 0) return relativeResult;
|
||||
|
||||
// Try numeric sorting second
|
||||
const numericResult = sortNumericKeys(a, b, asc);
|
||||
if (numericResult !== 0) return numericResult;
|
||||
|
||||
// Fallback to lexicographic order for mixed cases
|
||||
return asc ? a.localeCompare(b) : b.localeCompare(a);
|
||||
}
|
||||
|
||||
// Standard numeric/lexicographic comparison for other date modes
|
||||
return (
|
||||
sortNumericKeys(a, b, asc) ||
|
||||
(asc ? a.localeCompare(b) : b.localeCompare(a))
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export const defaultGroupBy = (
|
||||
type: 'groupBy',
|
||||
columnId: propertyId,
|
||||
name: name,
|
||||
hideEmpty: true,
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import hash from '@emotion/hash';
|
||||
import {
|
||||
addDays,
|
||||
differenceInCalendarDays,
|
||||
format as fmt,
|
||||
isToday,
|
||||
isTomorrow,
|
||||
isYesterday,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
} from 'date-fns';
|
||||
|
||||
import type { TypeInstance } from '../logical/type.js';
|
||||
import { t } from '../logical/type-presets.js';
|
||||
import { createUniComponentFromWebComponent } from '../utils/uni-component/uni-component.js';
|
||||
import { BooleanGroupView } from './renderer/boolean-group.js';
|
||||
import { DateGroupView } from './renderer/date-group.js';
|
||||
import { NumberGroupView } from './renderer/number-group.js';
|
||||
import { SelectGroupView } from './renderer/select-group.js';
|
||||
import { StringGroupView } from './renderer/string-group.js';
|
||||
@@ -15,171 +28,239 @@ export const createGroupByConfig = <
|
||||
GroupValue = unknown,
|
||||
>(
|
||||
config: GroupByConfig<Data, MatchType, GroupValue>
|
||||
): GroupByConfig => {
|
||||
return config as never as GroupByConfig;
|
||||
};
|
||||
): GroupByConfig => config as never;
|
||||
|
||||
export const ungroups = {
|
||||
key: 'Ungroups',
|
||||
value: null,
|
||||
};
|
||||
export const groupByMatchers = [
|
||||
|
||||
const WEEK_OPTS_MON = { weekStartsOn: 1 } as const;
|
||||
const WEEK_OPTS_SUN = { weekStartsOn: 0 } as const;
|
||||
|
||||
const rangeLabel = (a: Date, b: Date) =>
|
||||
`${fmt(a, 'MMM d yyyy')} – ${fmt(b, 'MMM d yyyy')}`;
|
||||
|
||||
function buildDateCfg(
|
||||
name: string,
|
||||
grouper: (ms: number | null) => { key: string; value: number | null }[],
|
||||
groupName: (v: number | null) => string
|
||||
): GroupByConfig {
|
||||
return createGroupByConfig({
|
||||
name,
|
||||
matchType: t.date.instance(),
|
||||
groupName: (_t, v) => groupName(v),
|
||||
defaultKeys: _t => [ungroups],
|
||||
valuesGroup: (v: number | null, _t) => grouper(v),
|
||||
addToGroup: (grp: number | null, _old: number | null) => grp,
|
||||
view: createUniComponentFromWebComponent(DateGroupView),
|
||||
});
|
||||
}
|
||||
|
||||
const dateRelativeCfg = buildDateCfg(
|
||||
'date-relative',
|
||||
v => {
|
||||
if (v == null) return [ungroups];
|
||||
const d = startOfDay(new Date(v));
|
||||
const today = startOfDay(new Date());
|
||||
const daysDiff = differenceInCalendarDays(d, today);
|
||||
|
||||
// Handle specific days
|
||||
if (isToday(d)) return [{ key: 'today', value: +d }];
|
||||
if (isTomorrow(d)) return [{ key: 'tomorrow', value: +d }];
|
||||
if (isYesterday(d)) return [{ key: 'yesterday', value: +d }];
|
||||
|
||||
// Handle future dates
|
||||
if (daysDiff > 0) {
|
||||
if (daysDiff <= 7) return [{ key: 'next7', value: +d }];
|
||||
if (daysDiff <= 30) return [{ key: 'next30', value: +d }];
|
||||
// Group by month for future dates beyond 30 days
|
||||
const m = startOfMonth(d);
|
||||
return [{ key: `${+m}`, value: +m }];
|
||||
}
|
||||
|
||||
// Handle past dates
|
||||
const daysAgo = -daysDiff;
|
||||
if (daysAgo <= 7) return [{ key: 'last7', value: +d }];
|
||||
if (daysAgo <= 30) return [{ key: 'last30', value: +d }];
|
||||
// Group by month for past dates beyond 30 days
|
||||
const m = startOfMonth(d);
|
||||
return [{ key: `${+m}`, value: +m }];
|
||||
},
|
||||
v => {
|
||||
if (v == null) return '';
|
||||
const d = startOfDay(new Date(v));
|
||||
const today = startOfDay(new Date());
|
||||
const daysDiff = differenceInCalendarDays(d, today);
|
||||
|
||||
// Handle specific days
|
||||
if (isToday(d)) return 'Today';
|
||||
if (isTomorrow(d)) return 'Tomorrow';
|
||||
if (isYesterday(d)) return 'Yesterday';
|
||||
|
||||
// Handle future dates
|
||||
if (daysDiff > 0) {
|
||||
if (daysDiff <= 7) return 'Next 7 days';
|
||||
if (daysDiff <= 30) return 'Next 30 days';
|
||||
// Show month/year for future dates beyond 30 days
|
||||
return fmt(new Date(v), 'MMM yyyy');
|
||||
}
|
||||
|
||||
// Handle past dates
|
||||
const daysAgo = -daysDiff;
|
||||
if (daysAgo <= 7) return 'Last 7 days';
|
||||
if (daysAgo <= 30) return 'Last 30 days';
|
||||
// Show month/year for past dates beyond 30 days
|
||||
return fmt(new Date(v), 'MMM yyyy');
|
||||
}
|
||||
);
|
||||
|
||||
const dateDayCfg = buildDateCfg(
|
||||
'date-day',
|
||||
v => {
|
||||
if (v == null) return [ungroups];
|
||||
const d = startOfDay(new Date(v));
|
||||
return [{ key: `${+d}`, value: +d }];
|
||||
},
|
||||
v => (v ? fmt(new Date(v), 'MMM d yyyy') : '')
|
||||
);
|
||||
|
||||
const dateWeekSunCfg = buildDateCfg(
|
||||
'date-week-sun',
|
||||
v => {
|
||||
if (v == null) return [ungroups];
|
||||
const w = startOfWeek(new Date(v), WEEK_OPTS_SUN);
|
||||
return [{ key: `${+w}`, value: +w }];
|
||||
},
|
||||
v => (v ? rangeLabel(new Date(v), addDays(new Date(v), 6)) : '')
|
||||
);
|
||||
|
||||
const dateWeekMonCfg = buildDateCfg(
|
||||
'date-week-mon',
|
||||
v => {
|
||||
if (v == null) return [ungroups];
|
||||
const w = startOfWeek(new Date(v), WEEK_OPTS_MON);
|
||||
return [{ key: `${+w}`, value: +w }];
|
||||
},
|
||||
v => (v ? rangeLabel(new Date(v), addDays(new Date(v), 6)) : '')
|
||||
);
|
||||
|
||||
const dateMonthCfg = buildDateCfg(
|
||||
'date-month',
|
||||
v => {
|
||||
if (v == null) return [ungroups];
|
||||
const m = startOfMonth(new Date(v));
|
||||
return [{ key: `${+m}`, value: +m }];
|
||||
},
|
||||
v => (v ? fmt(new Date(v), 'MMM yyyy') : '')
|
||||
);
|
||||
|
||||
const dateYearCfg = buildDateCfg(
|
||||
'date-year',
|
||||
v => {
|
||||
if (v == null) return [ungroups];
|
||||
const y = startOfYear(new Date(v));
|
||||
return [{ key: `${+y}`, value: +y }];
|
||||
},
|
||||
v => (v ? fmt(new Date(v), 'yyyy') : '')
|
||||
);
|
||||
|
||||
export const groupByMatchers: GroupByConfig[] = [
|
||||
createGroupByConfig({
|
||||
name: 'select',
|
||||
matchType: t.tag.instance(),
|
||||
groupName: (type, value: string | null) => {
|
||||
if (t.tag.is(type) && type.data) {
|
||||
if (t.tag.is(type) && type.data)
|
||||
return type.data.find(v => v.id === value)?.value ?? '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
defaultKeys: type => {
|
||||
if (t.tag.is(type) && type.data) {
|
||||
return [
|
||||
ungroups,
|
||||
...type.data.map(v => ({
|
||||
key: v.id,
|
||||
value: v.id,
|
||||
})),
|
||||
];
|
||||
}
|
||||
return [ungroups];
|
||||
},
|
||||
valuesGroup: (value, _type) => {
|
||||
if (value == null) {
|
||||
return [ungroups];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: `${value}`,
|
||||
value: value.toString(),
|
||||
},
|
||||
];
|
||||
},
|
||||
addToGroup: v => v,
|
||||
defaultKeys: type =>
|
||||
t.tag.is(type) && type.data
|
||||
? [ungroups, ...type.data.map(v => ({ key: v.id, value: v.id }))]
|
||||
: [ungroups],
|
||||
valuesGroup: (value, _t) =>
|
||||
value == null ? [ungroups] : [{ key: `${value}`, value }],
|
||||
addToGroup: (v: string | null, _old: string | null) => v,
|
||||
view: createUniComponentFromWebComponent(SelectGroupView),
|
||||
}),
|
||||
|
||||
createGroupByConfig({
|
||||
name: 'multi-select',
|
||||
matchType: t.array.instance(t.tag.instance()),
|
||||
groupName: (type, value: string | null) => {
|
||||
if (t.array.is(type) && t.tag.is(type.element) && type.element.data) {
|
||||
if (t.array.is(type) && t.tag.is(type.element) && type.element.data)
|
||||
return type.element.data.find(v => v.id === value)?.value ?? '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
defaultKeys: type => {
|
||||
if (t.array.is(type) && t.tag.is(type.element) && type.element.data) {
|
||||
return [
|
||||
ungroups,
|
||||
...type.element.data.map(v => ({
|
||||
key: v.id,
|
||||
value: v.id,
|
||||
})),
|
||||
];
|
||||
}
|
||||
defaultKeys: type =>
|
||||
t.array.is(type) && t.tag.is(type.element) && type.element.data
|
||||
? [
|
||||
ungroups,
|
||||
...type.element.data.map(v => ({ key: v.id, value: v.id })),
|
||||
]
|
||||
: [ungroups],
|
||||
valuesGroup: (value, _t) => {
|
||||
if (value == null) return [ungroups];
|
||||
if (Array.isArray(value) && value.length)
|
||||
return value.map(id => ({ key: `${id}`, value: id }));
|
||||
return [ungroups];
|
||||
},
|
||||
valuesGroup: (value, _type) => {
|
||||
if (value == null) {
|
||||
return [ungroups];
|
||||
}
|
||||
if (Array.isArray(value) && value.length) {
|
||||
return value.map(id => ({
|
||||
key: `${id}`,
|
||||
value: id,
|
||||
}));
|
||||
}
|
||||
return [ungroups];
|
||||
},
|
||||
addToGroup: (value, old) => {
|
||||
if (value == null) {
|
||||
return old;
|
||||
}
|
||||
addToGroup: (
|
||||
value: string | null,
|
||||
old: string[] | null
|
||||
): string[] | null => {
|
||||
if (value == null) return old;
|
||||
return Array.isArray(old) ? [...old, value] : [value];
|
||||
},
|
||||
removeFromGroup: (value, old) => {
|
||||
if (Array.isArray(old)) {
|
||||
return old.filter(v => v !== value);
|
||||
}
|
||||
return old;
|
||||
},
|
||||
removeFromGroup: (value, old) =>
|
||||
Array.isArray(old) ? old.filter(v => v !== value) : old,
|
||||
view: createUniComponentFromWebComponent(SelectGroupView),
|
||||
}),
|
||||
|
||||
createGroupByConfig({
|
||||
name: 'text',
|
||||
matchType: t.string.instance(),
|
||||
groupName: (_type, value: string | null) => {
|
||||
return `${value ?? ''}`;
|
||||
},
|
||||
defaultKeys: _type => {
|
||||
return [ungroups];
|
||||
},
|
||||
valuesGroup: (value, _type) => {
|
||||
if (typeof value !== 'string' || !value) {
|
||||
return [ungroups];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: hash(value),
|
||||
value,
|
||||
},
|
||||
];
|
||||
},
|
||||
addToGroup: v => v,
|
||||
groupName: (_t, v) => `${v ?? ''}`,
|
||||
defaultKeys: _t => [ungroups],
|
||||
valuesGroup: (v, _t) =>
|
||||
typeof v !== 'string' || !v ? [ungroups] : [{ key: hash(v), value: v }],
|
||||
addToGroup: (v: string | null, _old: string | null) => v,
|
||||
view: createUniComponentFromWebComponent(StringGroupView),
|
||||
}),
|
||||
|
||||
createGroupByConfig({
|
||||
name: 'number',
|
||||
matchType: t.number.instance(),
|
||||
groupName: (_type, value: number | null) => {
|
||||
return `${value ?? ''}`;
|
||||
},
|
||||
defaultKeys: _type => {
|
||||
return [ungroups];
|
||||
},
|
||||
valuesGroup: (value: number | null, _type) => {
|
||||
if (typeof value !== 'number') {
|
||||
return [ungroups];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: `g:${Math.floor(value / 10)}`,
|
||||
value: Math.floor(value / 10),
|
||||
},
|
||||
];
|
||||
},
|
||||
addToGroup: value => (typeof value === 'number' ? value * 10 : null),
|
||||
groupName: (_t, v) => `${v ?? ''}`,
|
||||
defaultKeys: _t => [ungroups],
|
||||
valuesGroup: (v, _t) =>
|
||||
typeof v !== 'number'
|
||||
? [ungroups]
|
||||
: [{ key: `g:${Math.floor(v / 10)}`, value: Math.floor(v / 10) }],
|
||||
addToGroup: (v: number | null, _old: number | null) =>
|
||||
typeof v === 'number' ? v * 10 : null,
|
||||
view: createUniComponentFromWebComponent(NumberGroupView),
|
||||
}),
|
||||
|
||||
createGroupByConfig({
|
||||
name: 'boolean',
|
||||
matchType: t.boolean.instance(),
|
||||
groupName: (_type, value: boolean | null) => {
|
||||
return `${value?.toString() ?? ''}`;
|
||||
},
|
||||
defaultKeys: _type => {
|
||||
return [
|
||||
{ key: 'true', value: true },
|
||||
{ key: 'false', value: false },
|
||||
];
|
||||
},
|
||||
valuesGroup: (value, _type) => {
|
||||
if (typeof value !== 'boolean') {
|
||||
return [
|
||||
{
|
||||
key: 'false',
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: value.toString(),
|
||||
value: value,
|
||||
},
|
||||
];
|
||||
},
|
||||
addToGroup: v => v,
|
||||
groupName: (_t, v) => `${v?.toString() ?? ''}`,
|
||||
defaultKeys: _t => [
|
||||
ungroups,
|
||||
{ key: 'true', value: true },
|
||||
{ key: 'false', value: false },
|
||||
],
|
||||
valuesGroup: (v, _t) =>
|
||||
typeof v !== 'boolean' ? [ungroups] : [{ key: v.toString(), value: v }],
|
||||
addToGroup: (v: boolean | null, _old: boolean | null) => v,
|
||||
view: createUniComponentFromWebComponent(BooleanGroupView),
|
||||
}),
|
||||
|
||||
dateRelativeCfg,
|
||||
dateDayCfg,
|
||||
dateWeekSunCfg,
|
||||
dateWeekMonCfg,
|
||||
dateMonthCfg,
|
||||
dateYearCfg,
|
||||
];
|
||||
|
||||
@@ -9,6 +9,18 @@ export const createGroupByMatcher = (list: GroupByConfig[]) => {
|
||||
return new Matcher_(list, v => v.matchType);
|
||||
};
|
||||
|
||||
export const findGroupByConfigByName = (
|
||||
dataSource: DataSource,
|
||||
name: string
|
||||
): GroupByConfig | undefined => {
|
||||
const svc = getGroupByService(dataSource);
|
||||
const all: GroupByConfig[] = [
|
||||
...svc.allExternalGroupByConfig(),
|
||||
...groupByMatchers,
|
||||
];
|
||||
return all.find(c => c.name === name);
|
||||
};
|
||||
|
||||
export class GroupByService {
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
|
||||
@@ -16,6 +16,14 @@ export class BooleanGroupView extends BaseGroup<boolean, NonNullable<unknown>> {
|
||||
`;
|
||||
|
||||
protected override render(): unknown {
|
||||
// Handle null/undefined values
|
||||
if (this.value == null) {
|
||||
const displayName = `No ${this.group.property.name$.value ?? 'value'}`;
|
||||
return html` <div class="data-view-group-title-boolean-view">
|
||||
${displayName}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html` <div class="data-view-group-title-boolean-view">
|
||||
${this.value
|
||||
? CheckBoxCheckSolidIcon({ style: `color:#1E96EB` })
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { Group } from '../trait.js';
|
||||
|
||||
export class DateGroupView extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.dv-date-group {
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
width: max-content;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.dv-date-group:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
.counter {
|
||||
flex-shrink: 0;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
background: var(--affine-background-secondary-color);
|
||||
color: var(--affine-text-secondary-color);
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group!: Group;
|
||||
|
||||
protected override render() {
|
||||
const name = this.group.name$.value;
|
||||
// Use contextual name based on the property when value is null
|
||||
const displayName =
|
||||
name ||
|
||||
(this.group.value === null
|
||||
? `No ${this.group.property.name$.value}`
|
||||
: 'Ungroups');
|
||||
return html`<div class="dv-date-group">
|
||||
<span>${displayName}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
customElements.define('data-view-date-group-view', DateGroupView);
|
||||
@@ -45,7 +45,8 @@ export class NumberGroupView extends BaseGroup<number, NonNullable<unknown>> {
|
||||
|
||||
protected override render(): unknown {
|
||||
if (this.value == null) {
|
||||
return html` <div>Ungroups</div>`;
|
||||
const displayName = `No ${this.group.property.name$.value}`;
|
||||
return html` <div>${displayName}</div>`;
|
||||
}
|
||||
if (this.value >= 10) {
|
||||
return html` <div
|
||||
|
||||
@@ -84,10 +84,11 @@ export class SelectGroupView extends BaseGroup<
|
||||
protected override render(): unknown {
|
||||
const tag = this.tag;
|
||||
if (!tag) {
|
||||
const displayName = `No ${this.group.property.name$.value}`;
|
||||
return html` <div
|
||||
style="font-size: 14px;color: var(--affine-text-primary-color);line-height: 22px;"
|
||||
>
|
||||
Ungroups
|
||||
${displayName}
|
||||
</div>`;
|
||||
}
|
||||
const style = styleMap({
|
||||
|
||||
@@ -41,7 +41,8 @@ export class StringGroupView extends BaseGroup<string, NonNullable<unknown>> {
|
||||
|
||||
protected override render(): unknown {
|
||||
if (!this.value) {
|
||||
return html` <div>Ungroups</div>`;
|
||||
const displayName = `No ${this.group.property.name$.value}`;
|
||||
return html` <div>${displayName}</div>`;
|
||||
}
|
||||
return html` <div
|
||||
@click="${this._click}"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
dropdownSubMenuMiddleware,
|
||||
menu,
|
||||
type MenuConfig,
|
||||
type MenuOptions,
|
||||
@@ -6,9 +7,12 @@ import {
|
||||
type PopupTarget,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { DeleteIcon } from '@blocksuite/icons/lit';
|
||||
import { DeleteIcon, InvisibleIcon, ViewIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import type { Middleware } from '@floating-ui/dom';
|
||||
import { autoPlacement, offset, shift } from '@floating-ui/dom';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
@@ -28,6 +32,24 @@ import { getGroupByService } from './matcher.js';
|
||||
import type { GroupTrait } from './trait.js';
|
||||
import type { GroupRenderProps } from './types.js';
|
||||
|
||||
const dateModeLabel = (key?: string) => {
|
||||
switch (key) {
|
||||
case 'date-relative':
|
||||
return 'Relative';
|
||||
case 'date-day':
|
||||
return 'Day';
|
||||
case 'date-week-mon':
|
||||
case 'date-week-sun':
|
||||
return 'Week';
|
||||
case 'date-month':
|
||||
return 'Month';
|
||||
case 'date-year':
|
||||
return 'Year';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export class GroupSetting extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
@@ -39,13 +61,44 @@ export class GroupSetting extends SignalWatcher(
|
||||
${unsafeCSS(dataViewCssVariable())};
|
||||
}
|
||||
|
||||
.group-sort-setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
z-index: 1;
|
||||
max-height: 200px;
|
||||
overflow: hidden auto;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* WebKit-based browser scrollbar styling */
|
||||
.group-sort-setting::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.group-sort-setting::-webkit-scrollbar-thumb {
|
||||
background-color: #b0b0b0; /* Grey slider */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.group-sort-setting::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.group-sort-setting {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #b0b0b0 transparent;
|
||||
}
|
||||
.group-hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.group-item {
|
||||
display: flex;
|
||||
padding: 4px 12px;
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.group-item-drag-bar {
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
@@ -57,18 +110,49 @@ export class GroupSetting extends SignalWatcher(
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.group-item:hover .group-item-drag-bar {
|
||||
background-color: #c0bfc1;
|
||||
}
|
||||
.group-item-op-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.group-item-op-icon:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
.group-item-op-icon svg {
|
||||
fill: var(--affine-icon-color);
|
||||
color: var(--affine-icon-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.group-item-name {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.properties-group-op {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${unsafeCSS(cssVarV2.button.primary)};
|
||||
}
|
||||
|
||||
.properties-group-op:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor groupTrait!: GroupTrait;
|
||||
|
||||
groups$ = computed(() => {
|
||||
return this.groupTrait.groupsDataList$.value;
|
||||
});
|
||||
groups$ = computed(() => this.groupTrait.groupsDataListAll$.value);
|
||||
|
||||
sortContext = createSortContext({
|
||||
activators: defaultActivators,
|
||||
@@ -78,99 +162,101 @@ export class GroupSetting extends SignalWatcher(
|
||||
const activeId = evt.active.id;
|
||||
const groups = this.groups$.value;
|
||||
if (over && over.id !== activeId && groups) {
|
||||
const activeIndex = groups.findIndex(data => data?.key === activeId);
|
||||
const overIndex = groups.findIndex(data => data?.key === over.id);
|
||||
|
||||
const aIndex = groups.findIndex(g => g?.key === activeId);
|
||||
const oIndex = groups.findIndex(g => g?.key === over.id);
|
||||
this.groupTrait.moveGroupTo(
|
||||
activeId,
|
||||
activeIndex > overIndex
|
||||
? {
|
||||
before: true,
|
||||
id: over.id,
|
||||
}
|
||||
: {
|
||||
before: false,
|
||||
id: over.id,
|
||||
}
|
||||
aIndex > oIndex
|
||||
? { before: true, id: over.id }
|
||||
: { before: false, id: over.id }
|
||||
);
|
||||
}
|
||||
},
|
||||
modifiers: [
|
||||
({ transform }) => {
|
||||
return {
|
||||
...transform,
|
||||
x: 0,
|
||||
};
|
||||
},
|
||||
],
|
||||
items: computed(() => {
|
||||
return (
|
||||
this.groupTrait.groupsDataList$.value?.map(
|
||||
v => v?.key ?? 'default key'
|
||||
) ?? []
|
||||
);
|
||||
}),
|
||||
modifiers: [({ transform }) => ({ ...transform, x: 0 })],
|
||||
items: computed(
|
||||
() =>
|
||||
this.groupTrait.groupsDataListAll$.value?.map(v => v?.key ?? '') ?? []
|
||||
),
|
||||
strategy: verticalListSortingStrategy,
|
||||
});
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._disposables.addFromEvent(this, 'pointerdown', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
this._disposables.addFromEvent(this, 'pointerdown', e =>
|
||||
e.stopPropagation()
|
||||
);
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
const groups = this.groupTrait.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return;
|
||||
}
|
||||
protected override render() {
|
||||
const groups = this.groupTrait.groupsDataListAll$.value;
|
||||
if (!groups) return;
|
||||
const map = this.groupTrait.groupDataMap$.value;
|
||||
const isAllShowed = map
|
||||
? Object.keys(map).every(k => !this.groupTrait.isGroupHidden(k))
|
||||
: true;
|
||||
const clickChangeAll = () => {
|
||||
if (!map) return;
|
||||
Object.keys(map).forEach(key => {
|
||||
this.groupTrait.setGroupHide(key, isAllShowed);
|
||||
});
|
||||
};
|
||||
return html`
|
||||
<div style="padding: 7px 0;">
|
||||
<div
|
||||
style="padding:7px 0;display:flex;justify-content:space-between;align-items:center;"
|
||||
>
|
||||
<div
|
||||
style="padding: 0 4px; font-size: 12px;color: var(--affine-text-secondary-color);line-height: 20px;"
|
||||
style="padding:0 4px;font-size:12px;color:var(--affine-text-secondary-color);line-height:20px;"
|
||||
>
|
||||
Groups
|
||||
</div>
|
||||
<div></div>
|
||||
<div class="properties-group-op" @click="${clickChangeAll}">
|
||||
${isAllShowed ? 'Hide All' : 'Show All'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="display:flex;flex-direction: column;gap: 4px;"
|
||||
class="group-sort-setting"
|
||||
>
|
||||
|
||||
<div class="group-sort-setting">
|
||||
${repeat(
|
||||
groups,
|
||||
group => group?.key ?? 'default key',
|
||||
group => {
|
||||
const type = group.property.dataType$.value;
|
||||
g => g?.key ?? 'k',
|
||||
g => {
|
||||
if (!g) return;
|
||||
const type = g.property.dataType$.value;
|
||||
if (!type) return;
|
||||
const props: GroupRenderProps = {
|
||||
group,
|
||||
readonly: true,
|
||||
};
|
||||
return html` <div
|
||||
${sortable(group.key)}
|
||||
${dragHandler(group.key)}
|
||||
class="dv-hover dv-round-4 group-item"
|
||||
>
|
||||
<div class="group-item-drag-bar"></div>
|
||||
const props: GroupRenderProps = { group: g, readonly: true };
|
||||
const icon = g.hide$.value ? InvisibleIcon() : ViewIcon();
|
||||
return html`
|
||||
<div
|
||||
style="padding: 0 4px;position:relative;pointer-events: none;max-width: 330px"
|
||||
${sortable(g.key)}
|
||||
${dragHandler(g.key)}
|
||||
class="dv-hover dv-round-4 group-item ${g.hide$.value
|
||||
? 'group-hidden'
|
||||
: ''}"
|
||||
>
|
||||
${renderUniLit(group.view, props)}
|
||||
<div class="group-item-drag-bar"></div>
|
||||
<div
|
||||
style="position:absolute;left: 0;top: 0;right: 0;bottom: 0;"
|
||||
></div>
|
||||
class="group-item-name"
|
||||
style="padding:0 4px;position:relative;pointer-events:none;max-width:330px;"
|
||||
>
|
||||
${renderUniLit(g.view, props)}
|
||||
<div
|
||||
style="position:absolute;left:0;top:0;right:0;bottom:0;"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="group-item-op-icon"
|
||||
@click="${() => g.hideSet(!g.hide$.value)}"
|
||||
>
|
||||
${icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.group-sort-setting')
|
||||
accessor groupContainer!: HTMLElement;
|
||||
@query('.group-sort-setting') accessor groupContainer!: HTMLElement;
|
||||
}
|
||||
|
||||
export const selectGroupByProperty = (
|
||||
@@ -184,10 +270,7 @@ export const selectGroupByProperty = (
|
||||
const view = group.view;
|
||||
return {
|
||||
onClose: ops?.onClose,
|
||||
title: {
|
||||
text: 'Group by',
|
||||
onBack: ops?.onBack,
|
||||
},
|
||||
title: { text: 'Group by', onBack: ops?.onBack, onClose: ops?.onClose },
|
||||
items: [
|
||||
menu.group({
|
||||
items: view.propertiesRaw$.value
|
||||
@@ -219,7 +302,7 @@ export const selectGroupByProperty = (
|
||||
menu.action({
|
||||
prefix: DeleteIcon(),
|
||||
hide: () =>
|
||||
view instanceof KanbanSingleView || group.property$.value == null,
|
||||
view instanceof KanbanSingleView || !group.property$.value,
|
||||
class: { 'delete-item': true },
|
||||
name: 'Remove Grouping',
|
||||
select: () => {
|
||||
@@ -232,77 +315,305 @@ export const selectGroupByProperty = (
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const popSelectGroupByProperty = (
|
||||
target: PopupTarget,
|
||||
group: GroupTrait,
|
||||
ops?: {
|
||||
onSelect?: () => void;
|
||||
onClose?: () => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
ops?: { onSelect?: () => void; onClose?: () => void; onBack?: () => void },
|
||||
middleware?: Array<Middleware | null | undefined | false>
|
||||
) => {
|
||||
popMenu(target, {
|
||||
const handler = popMenu(target, {
|
||||
options: selectGroupByProperty(group, ops),
|
||||
middleware,
|
||||
});
|
||||
handler.menu.menuElement.style.minHeight = '550px';
|
||||
};
|
||||
|
||||
export const popGroupSetting = (
|
||||
target: PopupTarget,
|
||||
group: GroupTrait,
|
||||
onBack: () => void
|
||||
onBack: () => void,
|
||||
onClose?: () => void,
|
||||
middleware?: Array<Middleware | null | undefined | false>
|
||||
) => {
|
||||
const view = group.view;
|
||||
const groupProperty = group.property$.value;
|
||||
if (groupProperty == null) {
|
||||
return;
|
||||
}
|
||||
const type = groupProperty.type$.value;
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
const icon = groupProperty.icon;
|
||||
const gProp = group.property$.value;
|
||||
if (!gProp) return;
|
||||
const type = gProp.type$.value;
|
||||
if (!type) return;
|
||||
|
||||
const icon = gProp.icon;
|
||||
const menuHandler = popMenu(target, {
|
||||
options: {
|
||||
title: {
|
||||
text: 'Group',
|
||||
onBack: onBack,
|
||||
onBack,
|
||||
onClose,
|
||||
},
|
||||
items: [
|
||||
menu.group({
|
||||
items: [
|
||||
menu.subMenu({
|
||||
menu.action({
|
||||
name: 'Group By',
|
||||
postfix: html`
|
||||
<div
|
||||
style="display:flex;align-items:center;gap: 4px;font-size: 12px;line-height: 20px;color: var(--affine-text-secondary-color);margin-right: 4px;margin-left: 8px;"
|
||||
style="display:flex;align-items:center;gap:4px;font-size:14px;line-height:20px;color:var(--affine-text-secondary-color);margin-left:8px;"
|
||||
class="dv-icon-16"
|
||||
>
|
||||
${renderUniLit(icon, {})} ${groupProperty.name$.value}
|
||||
${renderUniLit(icon, {})} ${gProp.name$.value}
|
||||
</div>
|
||||
`,
|
||||
label: () => html`
|
||||
<div style="color: var(--affine-text-secondary-color);">
|
||||
Group By
|
||||
</div>
|
||||
`,
|
||||
options: selectGroupByProperty(group, {
|
||||
onSelect: () => {
|
||||
menuHandler.close();
|
||||
popGroupSetting(target, group, onBack);
|
||||
select: () => {
|
||||
const subHandler = popMenu(target, {
|
||||
options: selectGroupByProperty(group, {
|
||||
onSelect: () => {
|
||||
menuHandler.close();
|
||||
popGroupSetting(
|
||||
target,
|
||||
group,
|
||||
onBack,
|
||||
onClose,
|
||||
middleware
|
||||
);
|
||||
},
|
||||
onBack: () => {
|
||||
menuHandler.close();
|
||||
popGroupSetting(
|
||||
target,
|
||||
group,
|
||||
onBack,
|
||||
onClose,
|
||||
middleware
|
||||
);
|
||||
},
|
||||
onClose,
|
||||
}),
|
||||
middleware: [
|
||||
autoPlacement({
|
||||
allowedPlacements: ['bottom-start', 'top-start'],
|
||||
}),
|
||||
offset({ mainAxis: 15, crossAxis: -162 }),
|
||||
shift({ crossAxis: true }),
|
||||
],
|
||||
});
|
||||
subHandler.menu.menuElement.style.minHeight = '550px';
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
...(type === 'date'
|
||||
? [
|
||||
menu.group({
|
||||
items: [
|
||||
menu.dynamic(() => [
|
||||
menu.subMenu({
|
||||
name: 'Date by',
|
||||
openOnHover: false,
|
||||
middleware: dropdownSubMenuMiddleware,
|
||||
autoHeight: true,
|
||||
postfix: html`
|
||||
<div
|
||||
style="display:flex;align-items:center;gap:4px;font-size:14px;line-height:20px;color:var(--affine-text-secondary-color);margin-left:30px;"
|
||||
>
|
||||
${dateModeLabel(group.groupInfo$.value?.config.name)}
|
||||
</div>
|
||||
`,
|
||||
options: {
|
||||
items: [
|
||||
menu.dynamic(() =>
|
||||
(
|
||||
[
|
||||
['Relative', 'date-relative'],
|
||||
['Day', 'date-day'],
|
||||
[
|
||||
'Week',
|
||||
group.groupInfo$.value?.config.name ===
|
||||
'date-week-mon'
|
||||
? 'date-week-mon'
|
||||
: 'date-week-sun',
|
||||
],
|
||||
['Month', 'date-month'],
|
||||
['Year', 'date-year'],
|
||||
] as [string, string][]
|
||||
).map(
|
||||
([label, key]): MenuConfig =>
|
||||
menu.action({
|
||||
name: label,
|
||||
label: () => {
|
||||
const isSelected =
|
||||
group.groupInfo$.value?.config.name ===
|
||||
key;
|
||||
return html`<span
|
||||
style="font-size:14px;color:${isSelected
|
||||
? 'var(--affine-text-emphasis-color)'
|
||||
: 'var(--affine-text-secondary-color)'}"
|
||||
>${label}</span
|
||||
>`;
|
||||
},
|
||||
isSelected:
|
||||
group.groupInfo$.value?.config.name === key,
|
||||
select: () => {
|
||||
group.changeGroupMode(key);
|
||||
return false;
|
||||
},
|
||||
})
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
],
|
||||
}),
|
||||
|
||||
...(group.groupInfo$.value?.config.name?.startsWith('date-week')
|
||||
? [
|
||||
menu.group({
|
||||
items: [
|
||||
menu.dynamic(() => [
|
||||
menu.subMenu({
|
||||
name: 'Start week on',
|
||||
postfix: html`
|
||||
<div
|
||||
style="display:flex;align-items:center;gap:4px;font-size:14px;line-height:20px;color:var(--affine-text-secondary-color);margin-left:8px;"
|
||||
>
|
||||
${group.groupInfo$.value?.config.name ===
|
||||
'date-week-mon'
|
||||
? 'Monday'
|
||||
: 'Sunday'}
|
||||
</div>
|
||||
`,
|
||||
options: {
|
||||
items: [
|
||||
menu.dynamic(() =>
|
||||
(
|
||||
[
|
||||
['Monday', 'date-week-mon'],
|
||||
['Sunday', 'date-week-sun'],
|
||||
] as [string, string][]
|
||||
).map(([label, key]) =>
|
||||
menu.action({
|
||||
name: label,
|
||||
label: () => {
|
||||
const isSelected =
|
||||
group.groupInfo$.value?.config
|
||||
.name === key;
|
||||
return html`<span
|
||||
style="font-size:14px;color:${isSelected
|
||||
? 'var(--affine-text-emphasis-color)'
|
||||
: 'var(--affine-text-secondary-color)'}"
|
||||
>${label}</span
|
||||
>`;
|
||||
},
|
||||
isSelected:
|
||||
group.groupInfo$.value?.config.name ===
|
||||
key,
|
||||
select: () => {
|
||||
group.changeGroupMode(key);
|
||||
return false;
|
||||
},
|
||||
})
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
menu.group({
|
||||
items: [
|
||||
menu.dynamic(() => [
|
||||
menu.subMenu({
|
||||
name: 'Sort',
|
||||
openOnHover: false,
|
||||
middleware: dropdownSubMenuMiddleware,
|
||||
autoHeight: true,
|
||||
postfix: html`
|
||||
<div
|
||||
style="display:flex;align-items:center;gap:4px;font-size:14px;line-height:20px;color:var(--affine-text-secondary-color);margin-left:8px;"
|
||||
>
|
||||
${group.sortAsc$.value
|
||||
? 'Oldest first'
|
||||
: 'Newest first'}
|
||||
</div>
|
||||
`,
|
||||
options: {
|
||||
items: [
|
||||
menu.dynamic(() => [
|
||||
menu.action({
|
||||
name: 'Oldest first',
|
||||
label: () => {
|
||||
const isSelected = group.sortAsc$.value;
|
||||
return html`<span
|
||||
style="font-size:14px;color:${isSelected
|
||||
? 'var(--affine-text-emphasis-color)'
|
||||
: 'var(--affine-text-secondary-color)'}"
|
||||
>Oldest first</span
|
||||
>`;
|
||||
},
|
||||
isSelected: group.sortAsc$.value,
|
||||
select: () => {
|
||||
group.setDateSortOrder(true);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Newest first',
|
||||
label: () => {
|
||||
const isSelected = !group.sortAsc$.value;
|
||||
return html`<span
|
||||
style="font-size:14px;color:${isSelected
|
||||
? 'var(--affine-text-emphasis-color)'
|
||||
: 'var(--affine-text-secondary-color)'}"
|
||||
>Newest first</span
|
||||
>`;
|
||||
},
|
||||
isSelected: !group.sortAsc$.value,
|
||||
select: () => {
|
||||
group.setDateSortOrder(false);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
]),
|
||||
],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
|
||||
menu.group({
|
||||
items: [
|
||||
menu.dynamic(() => [
|
||||
menu.action({
|
||||
name: 'Hide empty groups',
|
||||
isSelected: group.hideEmpty$.value,
|
||||
select: () => {
|
||||
group.setHideEmpty(!group.hideEmpty$.value);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
],
|
||||
}),
|
||||
menu.group({
|
||||
items: [
|
||||
menu =>
|
||||
html` <data-view-group-setting
|
||||
@mouseenter="${() => menu.closeSubMenu()}"
|
||||
.groupTrait="${group}"
|
||||
.columnId="${groupProperty.id}"
|
||||
></data-view-group-setting>`,
|
||||
menu => html`
|
||||
<data-view-group-setting
|
||||
@mouseenter=${() => menu.closeSubMenu()}
|
||||
.groupTrait=${group}
|
||||
.columnId=${gProp.id}
|
||||
></data-view-group-setting>
|
||||
`,
|
||||
],
|
||||
}),
|
||||
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
@@ -312,11 +623,14 @@ export const popGroupSetting = (
|
||||
hide: () => !(view instanceof TableSingleView),
|
||||
select: () => {
|
||||
group.changeGroup(undefined);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
middleware,
|
||||
});
|
||||
menuHandler.menu.menuElement.style.minHeight = '550px';
|
||||
};
|
||||
|
||||
@@ -2,7 +2,12 @@ import {
|
||||
insertPositionToIndex,
|
||||
type InsertToPosition,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import {
|
||||
computed,
|
||||
effect,
|
||||
type ReadonlySignal,
|
||||
signal,
|
||||
} from '@preact/signals-core';
|
||||
|
||||
import type { GroupBy, GroupProperty } from '../common/types.js';
|
||||
import type { TypeInstance } from '../logical/type.js';
|
||||
@@ -11,8 +16,10 @@ import { computedLock } from '../utils/lock.js';
|
||||
import type { Property } from '../view-manager/property.js';
|
||||
import type { Row } from '../view-manager/row.js';
|
||||
import type { SingleView } from '../view-manager/single-view.js';
|
||||
import { compareDateKeys } from './compare-date-keys.js';
|
||||
import { defaultGroupBy } from './default.js';
|
||||
import { getGroupByService } from './matcher.js';
|
||||
import { findGroupByConfigByName, getGroupByService } from './matcher.js';
|
||||
// Test
|
||||
import type { GroupByConfig } from './types.js';
|
||||
|
||||
export type GroupInfo<
|
||||
@@ -42,138 +49,71 @@ export class Group<
|
||||
get property() {
|
||||
return this.groupInfo.property;
|
||||
}
|
||||
|
||||
name$ = computed(() => {
|
||||
const type = this.property.dataType$.value;
|
||||
if (!type) {
|
||||
return '';
|
||||
}
|
||||
return this.groupInfo.config.groupName(type, this.value);
|
||||
return type ? this.groupInfo.config.groupName(type, this.value) : '';
|
||||
});
|
||||
|
||||
private get config() {
|
||||
return this.groupInfo.config;
|
||||
}
|
||||
|
||||
get tType() {
|
||||
return this.groupInfo.tType;
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.config.view;
|
||||
}
|
||||
|
||||
hide$ = computed(() => {
|
||||
const groupHide =
|
||||
this.manager.groupPropertiesMap$.value[this.key]?.hide ?? false;
|
||||
const emptyHidden = this.manager.hideEmpty$.value && this.rows.length === 0;
|
||||
return groupHide || emptyHidden;
|
||||
});
|
||||
|
||||
hideSet(hide: boolean) {
|
||||
this.manager.setGroupHide(this.key, hide);
|
||||
}
|
||||
}
|
||||
|
||||
function hasGroupProperties(
|
||||
data: unknown
|
||||
): data is { groupProperties?: GroupProperty[] } {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return false;
|
||||
}
|
||||
if (!('groupProperties' in data)) {
|
||||
return false;
|
||||
}
|
||||
const value = (data as { groupProperties?: unknown }).groupProperties;
|
||||
return value === undefined || Array.isArray(value);
|
||||
}
|
||||
|
||||
export class GroupTrait {
|
||||
groupInfo$ = computed<GroupInfo | undefined>(() => {
|
||||
const groupBy = this.groupBy$.value;
|
||||
if (!groupBy) {
|
||||
return;
|
||||
}
|
||||
const property = this.view.propertyGetOrCreate(groupBy.columnId);
|
||||
if (!property) {
|
||||
return;
|
||||
}
|
||||
const tType = property.dataType$.value;
|
||||
if (!tType) {
|
||||
return;
|
||||
}
|
||||
const groupByService = getGroupByService(this.view.manager.dataSource);
|
||||
const result = groupByService?.matcher.match(tType);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
config: result,
|
||||
property,
|
||||
tType: tType,
|
||||
};
|
||||
hideEmpty$ = signal<boolean>(true);
|
||||
sortAsc$ = signal<boolean>(true);
|
||||
|
||||
groupProperties$ = computed(() => {
|
||||
const data = this.view.data$.value;
|
||||
return hasGroupProperties(data) ? (data.groupProperties ?? []) : [];
|
||||
});
|
||||
|
||||
staticInfo$ = computed(() => {
|
||||
const groupInfo = this.groupInfo$.value;
|
||||
if (!groupInfo) {
|
||||
return;
|
||||
}
|
||||
const staticMap = Object.fromEntries(
|
||||
groupInfo.config
|
||||
.defaultKeys(groupInfo.tType)
|
||||
.map(({ key, value }) => [key, new Group(key, value, groupInfo, this)])
|
||||
);
|
||||
return {
|
||||
staticMap,
|
||||
groupInfo,
|
||||
};
|
||||
});
|
||||
|
||||
groupDataMap$ = computed(() => {
|
||||
const staticInfo = this.staticInfo$.value;
|
||||
if (!staticInfo) {
|
||||
return;
|
||||
}
|
||||
const { staticMap, groupInfo } = staticInfo;
|
||||
const groupMap: Record<string, Group> = {};
|
||||
Object.entries(staticMap).forEach(([key, group]) => {
|
||||
groupMap[key] = new Group(key, group.value, groupInfo, this);
|
||||
groupPropertiesMap$ = computed(() => {
|
||||
const map: Record<string, GroupProperty> = {};
|
||||
this.groupProperties$.value.forEach(g => {
|
||||
map[g.key] = g;
|
||||
});
|
||||
this.view.rows$.value.forEach(row => {
|
||||
const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id)
|
||||
.jsonValue$.value;
|
||||
const keys = groupInfo.config.valuesGroup(value, groupInfo.tType);
|
||||
keys.forEach(({ key, value }) => {
|
||||
if (!groupMap[key]) {
|
||||
groupMap[key] = new Group(key, value, groupInfo, this);
|
||||
}
|
||||
groupMap[key].rows.push(row);
|
||||
});
|
||||
});
|
||||
return groupMap;
|
||||
});
|
||||
|
||||
groupsDataList$ = computedLock(
|
||||
computed(() => {
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
}
|
||||
const sortedGroup = this.ops.sortGroup(Object.keys(groupMap));
|
||||
sortedGroup.forEach(key => {
|
||||
if (!groupMap[key]) return;
|
||||
groupMap[key].rows = this.ops.sortRow(key, groupMap[key].rows);
|
||||
});
|
||||
return sortedGroup
|
||||
.map(key => groupMap[key])
|
||||
.filter((v): v is Group => v != null);
|
||||
}),
|
||||
this.view.isLocked$
|
||||
);
|
||||
|
||||
updateData = (data: NonNullable<unknown>) => {
|
||||
const property = this.property$.value;
|
||||
if (!property) {
|
||||
return;
|
||||
}
|
||||
this.view.propertyGetOrCreate(property.id).dataUpdate(() => data);
|
||||
};
|
||||
|
||||
get addGroup() {
|
||||
return this.property$.value?.meta$.value?.config.addGroup;
|
||||
}
|
||||
|
||||
property$ = computed(() => {
|
||||
const groupInfo = this.groupInfo$.value;
|
||||
if (!groupInfo) {
|
||||
return;
|
||||
}
|
||||
return groupInfo.property;
|
||||
return map;
|
||||
});
|
||||
|
||||
/**
|
||||
* Synchronize sortAsc$ with the GroupBy sort descriptor
|
||||
*/
|
||||
constructor(
|
||||
private readonly groupBy$: ReadonlySignal<GroupBy | undefined>,
|
||||
public view: SingleView,
|
||||
private readonly ops: {
|
||||
groupBySet: (groupBy: GroupBy | undefined) => void;
|
||||
sortGroup: (keys: string[]) => string[];
|
||||
groupBySet: (g: GroupBy | undefined) => void;
|
||||
sortGroup: (keys: string[], asc?: boolean) => string[];
|
||||
sortRow: (groupKey: string, rows: Row[]) => Row[];
|
||||
changeGroupSort: (keys: string[]) => void;
|
||||
changeRowSort: (
|
||||
@@ -181,11 +121,188 @@ export class GroupTrait {
|
||||
groupKey: string,
|
||||
keys: string[]
|
||||
) => void;
|
||||
changeGroupHide?: (key: string, hide: boolean) => void;
|
||||
}
|
||||
) {}
|
||||
) {
|
||||
effect(() => {
|
||||
const desc = this.groupBy$.value?.sort?.desc;
|
||||
if (desc != null && this.sortAsc$.value === desc) {
|
||||
this.sortAsc$.value = !desc;
|
||||
}
|
||||
});
|
||||
|
||||
// Sync hideEmpty state with GroupBy data
|
||||
effect(() => {
|
||||
const hide = this.groupBy$.value?.hideEmpty;
|
||||
if (hide != null && this.hideEmpty$.value !== hide) {
|
||||
this.hideEmpty$.value = hide;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
groupInfo$ = computed<GroupInfo | undefined>(() => {
|
||||
const groupBy = this.groupBy$.value;
|
||||
if (!groupBy) return;
|
||||
|
||||
const property = this.view.propertyGetOrCreate(groupBy.columnId);
|
||||
if (!property) return;
|
||||
|
||||
const tType = property.dataType$.value;
|
||||
if (!tType) return;
|
||||
|
||||
const svc = getGroupByService(this.view.manager.dataSource);
|
||||
const res =
|
||||
groupBy.name != null
|
||||
? (findGroupByConfigByName(
|
||||
this.view.manager.dataSource,
|
||||
groupBy.name
|
||||
) ?? svc?.matcher.match(tType))
|
||||
: svc?.matcher.match(tType);
|
||||
|
||||
if (!res) return;
|
||||
return { config: res, property, tType };
|
||||
});
|
||||
|
||||
staticInfo$ = computed(() => {
|
||||
const info = this.groupInfo$.value;
|
||||
if (!info) return;
|
||||
const staticMap = Object.fromEntries(
|
||||
info.config
|
||||
.defaultKeys(info.tType)
|
||||
.map(({ key, value }) => [key, new Group(key, value, info, this)])
|
||||
);
|
||||
return { staticMap, groupInfo: info };
|
||||
});
|
||||
|
||||
groupDataMap$ = computed(() => {
|
||||
const si = this.staticInfo$.value;
|
||||
if (!si) return;
|
||||
const { staticMap, groupInfo } = si;
|
||||
// Create fresh Group instances with empty rows arrays
|
||||
const map: Record<string, Group> = {};
|
||||
Object.entries(staticMap).forEach(([key, group]) => {
|
||||
map[key] = new Group(key, group.value, groupInfo, this);
|
||||
});
|
||||
// Assign rows to their respective groups
|
||||
this.view.rows$.value.forEach(row => {
|
||||
const cell = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id);
|
||||
const jv = cell.jsonValue$.value;
|
||||
const keys = groupInfo.config.valuesGroup(jv, groupInfo.tType);
|
||||
keys.forEach(({ key, value }) => {
|
||||
if (!map[key]) map[key] = new Group(key, value, groupInfo, this);
|
||||
map[key].rows.push(row);
|
||||
});
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
groupsDataList$ = computedLock(
|
||||
computed(() => {
|
||||
const map = this.groupDataMap$.value;
|
||||
if (!map) return;
|
||||
|
||||
const gi = this.groupInfo$.value;
|
||||
let ordered: string[];
|
||||
|
||||
if (gi?.config.matchType.name === 'Date') {
|
||||
ordered = Object.keys(map).sort(
|
||||
compareDateKeys(gi.config.name, this.sortAsc$.value)
|
||||
);
|
||||
} else {
|
||||
ordered = this.ops.sortGroup(Object.keys(map), this.sortAsc$.value);
|
||||
}
|
||||
|
||||
return ordered
|
||||
.map(k => map[k])
|
||||
.filter(
|
||||
(g): g is Group =>
|
||||
!!g &&
|
||||
!this.isGroupHidden(g.key) &&
|
||||
(!this.hideEmpty$.value || g.rows.length > 0)
|
||||
);
|
||||
}),
|
||||
this.view.isLocked$
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed list of groups including hidden ones, used by settings UI.
|
||||
*/
|
||||
groupsDataListAll$ = computedLock(
|
||||
computed(() => {
|
||||
const map = this.groupDataMap$.value;
|
||||
const info = this.groupInfo$.value;
|
||||
if (!map || !info) return;
|
||||
|
||||
let orderedKeys: string[];
|
||||
if (info.config.matchType.name === 'Date') {
|
||||
orderedKeys = Object.keys(map).sort(
|
||||
compareDateKeys(info.config.name, this.sortAsc$.value)
|
||||
);
|
||||
} else {
|
||||
orderedKeys = this.ops.sortGroup(Object.keys(map), this.sortAsc$.value);
|
||||
}
|
||||
|
||||
const visible: Group[] = [];
|
||||
const hidden: Group[] = [];
|
||||
orderedKeys
|
||||
.map(key => map[key])
|
||||
.filter((g): g is Group => g != null)
|
||||
.forEach(g => {
|
||||
if (g.hide$.value) {
|
||||
hidden.push(g);
|
||||
} else {
|
||||
visible.push(g);
|
||||
}
|
||||
});
|
||||
return [...visible, ...hidden];
|
||||
}),
|
||||
this.view.isLocked$
|
||||
);
|
||||
|
||||
/** Whether all groups are currently hidden */
|
||||
allHidden$ = computed(() => {
|
||||
const map = this.groupDataMap$.value;
|
||||
if (!map) return false;
|
||||
return Object.keys(map).every(key => this.isGroupHidden(key));
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle hiding of empty groups.
|
||||
*/
|
||||
|
||||
setHideEmpty(value: boolean) {
|
||||
this.hideEmpty$.value = value;
|
||||
const gb = this.groupBy$.value;
|
||||
if (gb) {
|
||||
this.ops.groupBySet({ ...gb, hideEmpty: value });
|
||||
}
|
||||
}
|
||||
|
||||
isGroupHidden(key: string): boolean {
|
||||
return this.groupPropertiesMap$.value[key]?.hide ?? false;
|
||||
}
|
||||
|
||||
setGroupHide(key: string, hide: boolean) {
|
||||
this.ops.changeGroupHide?.(key, hide);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sort order for date groupings and update GroupBy sort descriptor.
|
||||
*/
|
||||
setDateSortOrder(asc: boolean) {
|
||||
this.sortAsc$.value = asc;
|
||||
|
||||
const gb = this.groupBy$.value;
|
||||
if (gb) {
|
||||
this.ops.groupBySet({
|
||||
...gb,
|
||||
sort: { desc: !asc },
|
||||
hideEmpty: gb.hideEmpty,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addToGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
const groupInfo = this.groupInfo$.value;
|
||||
if (!groupMap || !groupInfo) {
|
||||
@@ -205,18 +322,34 @@ export class GroupTrait {
|
||||
.cellGetOrCreate(rowId, groupInfo.property.id)
|
||||
.valueSet(newValue);
|
||||
}
|
||||
}
|
||||
const map = this.groupDataMap$.value;
|
||||
const info = this.groupInfo$.value;
|
||||
if (!map || !info) return;
|
||||
|
||||
changeCardSort(groupKey: string, cardIds: string[]) {
|
||||
const groups = this.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return;
|
||||
}
|
||||
this.ops.changeRowSort(
|
||||
groups.map(v => v.key),
|
||||
groupKey,
|
||||
cardIds
|
||||
const addFn = info.config.addToGroup;
|
||||
if (addFn === false) return;
|
||||
|
||||
const group = map[key];
|
||||
if (!group) return;
|
||||
|
||||
const current = group.value;
|
||||
// Handle both null and non-null values to ensure proper group assignment
|
||||
const newVal = addFn(
|
||||
current,
|
||||
this.view.cellGetOrCreate(rowId, info.property.id).jsonValue$.value
|
||||
);
|
||||
this.view.cellGetOrCreate(rowId, info.property.id).valueSet(newVal);
|
||||
}
|
||||
changeGroupMode(modeName: string) {
|
||||
const propId = this.property$.value?.id;
|
||||
if (!propId) return;
|
||||
this.ops.groupBySet({
|
||||
type: 'groupBy',
|
||||
columnId: propId,
|
||||
name: modeName,
|
||||
sort: { desc: !this.sortAsc$.value },
|
||||
hideEmpty: this.hideEmpty$.value,
|
||||
});
|
||||
}
|
||||
|
||||
changeGroup(columnId: string | undefined) {
|
||||
@@ -225,31 +358,38 @@ export class GroupTrait {
|
||||
return;
|
||||
}
|
||||
const column = this.view.propertyGetOrCreate(columnId);
|
||||
const propertyMeta = this.view.manager.dataSource.propertyMetaGet(
|
||||
const meta = this.view.manager.dataSource.propertyMetaGet(
|
||||
column.type$.value
|
||||
);
|
||||
if (propertyMeta) {
|
||||
this.ops.groupBySet(
|
||||
defaultGroupBy(
|
||||
this.view.manager.dataSource,
|
||||
propertyMeta,
|
||||
column.id,
|
||||
column.data$.value
|
||||
)
|
||||
if (meta) {
|
||||
const gb = defaultGroupBy(
|
||||
this.view.manager.dataSource,
|
||||
meta,
|
||||
column.id,
|
||||
column.data$.value
|
||||
);
|
||||
if (gb) {
|
||||
gb.sort = { desc: !this.sortAsc$.value };
|
||||
gb.hideEmpty = this.hideEmpty$.value;
|
||||
}
|
||||
this.ops.groupBySet(gb);
|
||||
}
|
||||
}
|
||||
|
||||
changeGroupSort(keys: string[]) {
|
||||
this.ops.changeGroupSort(keys);
|
||||
property$ = computed(() => this.groupInfo$.value?.property);
|
||||
|
||||
get addGroup() {
|
||||
return this.property$.value?.meta$.value?.config.addGroup;
|
||||
}
|
||||
|
||||
defaultGroupProperty(key: string): GroupProperty {
|
||||
return {
|
||||
key,
|
||||
hide: false,
|
||||
manuallyCardSort: [],
|
||||
};
|
||||
updateData = (data: NonNullable<unknown>) => {
|
||||
const prop = this.property$.value;
|
||||
if (!prop) return;
|
||||
this.view.propertyGetOrCreate(prop.id).dataUpdate(() => data);
|
||||
};
|
||||
|
||||
changeGroupSort(keys: string[]) {
|
||||
this.ops.changeGroupSort(keys);
|
||||
}
|
||||
|
||||
moveCardTo(
|
||||
@@ -258,7 +398,6 @@ export class GroupTrait {
|
||||
toGroupKey: string,
|
||||
position: InsertToPosition
|
||||
) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -291,16 +430,16 @@ export class GroupTrait {
|
||||
.map(row => row.rowId) ?? [];
|
||||
const index = insertPositionToIndex(position, rows, row => row);
|
||||
rows.splice(index, 0, rowId);
|
||||
this.changeCardSort(toGroupKey, rows);
|
||||
const groupKeys = Object.keys(groupMap);
|
||||
this.ops.changeRowSort(groupKeys, toGroupKey, rows);
|
||||
}
|
||||
|
||||
moveGroupTo(groupKey: string, position: InsertToPosition) {
|
||||
this.view.lockRows(false);
|
||||
const groups = this.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return;
|
||||
}
|
||||
const keys = groups.map(v => v.key);
|
||||
const keys = groups.map(v => v!.key);
|
||||
keys.splice(
|
||||
keys.findIndex(key => key === groupKey),
|
||||
1
|
||||
@@ -311,7 +450,6 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
removeFromGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -330,7 +468,6 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
updateValue(rows: string[], value: unknown) {
|
||||
this.view.lockRows(false);
|
||||
const propertyId = this.property$.value?.id;
|
||||
if (!propertyId) {
|
||||
return;
|
||||
|
||||
@@ -21,8 +21,7 @@ type FnValueType<
|
||||
export class FnTypeInstance<
|
||||
Args extends readonly TypeInstance[] = readonly TypeInstance[],
|
||||
Return extends TypeInstance = TypeInstance,
|
||||
> implements TypeInstance
|
||||
{
|
||||
> implements TypeInstance {
|
||||
_validate = fnSchema;
|
||||
|
||||
readonly _valueType = undefined as never as FnValueType<Args, Return>;
|
||||
@@ -55,7 +54,6 @@ export class FnTypeInstance<
|
||||
unify(ctx: TypeVarContext, template: FnTypeInstance, unify: Unify): boolean {
|
||||
const newCtx = { ...ctx };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
for (let i = 0; i < template.args.length; i++) {
|
||||
const arg = template.args[i];
|
||||
const realArg = this.args[i];
|
||||
@@ -79,9 +77,9 @@ export class FnTypeInstance<
|
||||
|
||||
const fnSchema = Zod.function();
|
||||
|
||||
export class ArrayTypeInstance<Element extends TypeInstance = TypeInstance>
|
||||
implements TypeInstance
|
||||
{
|
||||
export class ArrayTypeInstance<
|
||||
Element extends TypeInstance = TypeInstance,
|
||||
> implements TypeInstance {
|
||||
readonly _validate;
|
||||
|
||||
readonly _valueType = undefined as never as ValueTypeOf<Element>[];
|
||||
|
||||
@@ -14,8 +14,7 @@ export class DTInstance<
|
||||
Name extends string = string,
|
||||
Data = unknown,
|
||||
ValueSchema extends Zod.ZodType = Zod.ZodType,
|
||||
> implements TypeInstance
|
||||
{
|
||||
> implements TypeInstance {
|
||||
readonly _valueType = undefined as never as Zod.TypeOf<ValueSchema>;
|
||||
|
||||
constructor(
|
||||
@@ -47,8 +46,7 @@ export class DataType<
|
||||
Name extends string = string,
|
||||
DataSchema extends Zod.ZodType = Zod.ZodType,
|
||||
ValueSchema extends Zod.ZodType = Zod.ZodType,
|
||||
> implements TypeDefinition
|
||||
{
|
||||
> implements TypeDefinition {
|
||||
constructor(
|
||||
private readonly name: Name,
|
||||
_dataSchema: DataSchema,
|
||||
|
||||
@@ -17,9 +17,9 @@ export class TypeVarDefinitionInstance<
|
||||
) {}
|
||||
}
|
||||
|
||||
export class TypeVarReferenceInstance<Name extends string = string>
|
||||
implements TypeInstance
|
||||
{
|
||||
export class TypeVarReferenceInstance<
|
||||
Name extends string = string,
|
||||
> implements TypeInstance {
|
||||
readonly _validate = unknownSchema;
|
||||
|
||||
readonly _valueType = undefined as unknown;
|
||||
|
||||
@@ -8,10 +8,10 @@ import type { Cell } from '../view-manager/cell.js';
|
||||
import type { CellRenderProps, DataViewCellLifeCycle } from './manager.js';
|
||||
|
||||
export abstract class BaseCellRenderer<
|
||||
RawValue = unknown,
|
||||
JsonValue = unknown,
|
||||
Data extends Record<string, unknown> = Record<string, unknown>,
|
||||
>
|
||||
RawValue = unknown,
|
||||
JsonValue = unknown,
|
||||
Data extends Record<string, unknown> = Record<string, unknown>,
|
||||
>
|
||||
extends SignalWatcher(WithDisposable(ShadowlessElement))
|
||||
implements DataViewCellLifeCycle, CellRenderProps<Data, RawValue, JsonValue>
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
popMenu,
|
||||
type PopupTarget,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import type { Middleware } from '@floating-ui/dom';
|
||||
|
||||
import { renderUniLit } from '../utils/index.js';
|
||||
import type { SortUtils } from './utils.js';
|
||||
@@ -13,9 +14,13 @@ export const popCreateSort = (
|
||||
sortUtils: SortUtils;
|
||||
onClose?: () => void;
|
||||
onBack?: () => void;
|
||||
},
|
||||
ops?: {
|
||||
middleware?: Middleware[];
|
||||
}
|
||||
) => {
|
||||
popMenu(target, {
|
||||
const subHandler = popMenu(target, {
|
||||
middleware: ops?.middleware,
|
||||
options: {
|
||||
onClose: props.onClose,
|
||||
title: {
|
||||
@@ -50,4 +55,5 @@ export const popCreateSort = (
|
||||
],
|
||||
},
|
||||
});
|
||||
subHandler.menu.menuElement.style.minHeight = '550px';
|
||||
};
|
||||
|
||||
@@ -29,8 +29,7 @@ export class CellBase<
|
||||
RawValue = unknown,
|
||||
JsonValue = unknown,
|
||||
Data extends Record<string, unknown> = Record<string, unknown>,
|
||||
> implements Cell<RawValue, JsonValue, Data>
|
||||
{
|
||||
> implements Cell<RawValue, JsonValue, Data> {
|
||||
get dataSource() {
|
||||
return this.view.manager.dataSource;
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ export abstract class PropertyBase<
|
||||
RawValue = unknown,
|
||||
JsonValue = unknown,
|
||||
Data extends Record<string, unknown> = Record<string, unknown>,
|
||||
> implements Property<RawValue, JsonValue, Data>
|
||||
{
|
||||
> implements Property<RawValue, JsonValue, Data> {
|
||||
meta$ = computed(() => {
|
||||
return this.dataSource.propertyMetaGet(this.type$.value);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export type MainProperties = {
|
||||
};
|
||||
|
||||
export interface SingleView {
|
||||
data$: any;
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
readonly manager: ViewManager;
|
||||
@@ -78,8 +79,7 @@ export interface SingleView {
|
||||
|
||||
export abstract class SingleViewBase<
|
||||
ViewData extends DataViewDataType = DataViewDataType,
|
||||
> implements SingleView
|
||||
{
|
||||
> implements SingleView {
|
||||
private readonly searchString = signal('');
|
||||
|
||||
private readonly traitMap = new Map<symbol, unknown>();
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { DataViewUILogicBase } from '../view/data-view-base.js';
|
||||
import type { DataViewWidgetProps } from './types.js';
|
||||
|
||||
export class WidgetBase<
|
||||
ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
|
||||
>
|
||||
ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
|
||||
>
|
||||
extends SignalWatcher(WithDisposable(ShadowlessElement))
|
||||
implements DataViewWidgetProps<ViewLogic>
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user