├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md └── workflows │ ├── build.yml │ ├── codeql.yml │ └── release.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh_CN.md ├── asset ├── DatabasePropertyPanel.png ├── DatabaseTable.png ├── action.png └── preview.xcf ├── eslint.config.js ├── icon.png ├── package.json ├── plugin.json ├── pnpm-lock.yaml ├── preview.png ├── preview.xcf ├── public └── i18n │ ├── en_US.json │ └── zh_CN.json ├── scripts ├── .gitignore ├── check_i18n.js ├── elevate.ps1 ├── make_dev_link.js ├── make_install.js ├── update_icons.js ├── update_version.js └── utils.js ├── src ├── PluginPanel.svelte ├── PluginPanel.test.js ├── api.ts ├── components │ ├── AttributeViewPanel.svelte │ ├── AttributeViewPanelNative.svelte │ ├── AttributeViewValue.svelte │ ├── ColumnIcon.spec.ts │ ├── ColumnIcon.svelte │ ├── CustomIcon.spec.ts │ ├── CustomIcon.svelte │ ├── ProtyleBreadcrumb.svelte │ ├── ShowEmptyAttributesToggle.svelte │ ├── ValueTypes │ │ ├── AttributeViewRollup.svelte │ │ ├── DateValue.svelte │ │ ├── MAssetValue.svelte │ │ ├── MultiSelectValue.svelte │ │ ├── RelationValue.svelte │ │ ├── TemplateValue.svelte │ │ └── TextValue.svelte │ └── ui │ │ ├── Button.svelte │ │ ├── Button.test.ts │ │ ├── CollapseButton.svelte │ │ ├── Icon.svelte │ │ ├── Icon.test.ts │ │ └── LayoutTabBar.svelte ├── constants.ts ├── index.scss ├── index.ts ├── libs │ ├── const.ts │ ├── getAVKeyAndValues.ts │ ├── is-empty.ts │ ├── setting-utils.ts │ ├── setting.types.ts │ └── siyuan │ │ └── protyle │ │ ├── render │ │ └── av │ │ │ ├── blockAttr.ts │ │ │ ├── col.ts │ │ │ └── openMenuPanel.ts │ │ ├── ui │ │ └── initUI.ts │ │ └── util │ │ ├── escape.ts │ │ └── functions.ts ├── services │ ├── AttributeViewService.ts │ ├── LoggerService.test.ts │ ├── LoggerService.ts │ └── StorageService.ts ├── stores │ ├── configStore.ts │ ├── documentSettingsStore.ts │ ├── i18nStore.ts │ ├── localSettingStore.test.ts │ └── localSettingStore.ts └── types │ ├── AttributeView.ts │ ├── LogEntry.ts │ ├── SiyuanIcon.ts │ ├── api.d.ts │ ├── context.ts │ ├── dto │ └── SettingsDTO.ts │ ├── events.ts │ ├── i18n.ts │ ├── index.d.ts │ ├── siyuan.types.ts │ └── svelte.d.ts ├── svelte.config.js ├── test ├── mocks │ ├── AttributeViewPanel.svelte │ ├── AttributeViewPanelNative.svelte │ ├── ProtyleBreadcrumb.svelte │ └── siyuan.ts └── setupTests.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── vitest.config.ts └── yaml-plugin.js /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | --- 7 | 8 | **Describe the bug** 9 | A clear and concise description of what the bug is. Preferrably in English. 10 | 11 | **To Reproduce** 12 | Steps to reproduce the behavior: 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Environment:** 23 | 24 | - OS: [Mac, Windows, Browser, iOS, Android] 25 | - Siyuan Version: 26 | - Plugin Version: 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Share an idea what we could add 4 | title: "" 5 | labels: enhancement 6 | --- 7 | 8 | **Describe the idea** 9 | A clear and concise description of what the bug is. Preferrably in English. 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main,develop] 6 | pull_request: 7 | branches: [main,develop] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Checkout 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | # Install Node.js 18 | - name: Install Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | registry-url: "https://registry.npmjs.org" 23 | 24 | # Install pnpm 25 | - name: Install pnpm 26 | uses: pnpm/action-setup@v4 27 | id: pnpm-install 28 | with: 29 | version: 8 30 | run_install: false 31 | 32 | # Get pnpm store directory 33 | - name: Get pnpm store directory 34 | id: pnpm-cache 35 | shell: bash 36 | run: | 37 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 38 | 39 | # Setup pnpm cache 40 | - name: Setup pnpm cache 41 | uses: actions/cache@v3 42 | with: 43 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 44 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 45 | restore-keys: | 46 | ${{ runner.os }}-pnpm-store- 47 | 48 | # Install dependencies 49 | - name: Install dependencies 50 | run: pnpm install 51 | 52 | - name: Lint 53 | run: pnpm lint 54 | 55 | - name: Check Translations 56 | run: pnpm check-i18n 57 | 58 | - name: Test 59 | run: pnpm vitest run 60 | 61 | - name: Build for production 62 | run: pnpm build 63 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '27 20 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release on Tag Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Checkout 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | # Environment Variables 18 | - name: Set Environment Variable 19 | run: | 20 | echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> $GITHUB_ENV 21 | echo "NODE_ENV=production" >> $GITHUB_ENV 22 | 23 | # Install Node.js 24 | - name: Install Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 20 28 | registry-url: "https://registry.npmjs.org" 29 | 30 | # Install pnpm 31 | - name: Install pnpm 32 | uses: pnpm/action-setup@v4 33 | id: pnpm-install 34 | with: 35 | version: 8 36 | run_install: false 37 | 38 | # Get pnpm store directory 39 | - name: Get pnpm store directory 40 | id: pnpm-cache 41 | shell: bash 42 | run: | 43 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 44 | 45 | # Setup pnpm cache 46 | - name: Setup pnpm cache 47 | uses: actions/cache@v3 48 | with: 49 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 50 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 51 | restore-keys: | 52 | ${{ runner.os }}-pnpm-store- 53 | 54 | # Install dependencies 55 | - name: Install dependencies 56 | run: pnpm install 57 | 58 | - name: Extract version from plugin.json 59 | id: extract_version 60 | run: | 61 | VERSION=$(jq -r '.version' plugin.json) 62 | echo "VERSION=$VERSION" >> $GITHUB_ENV 63 | 64 | # Build for production, 这一步会生成一个 package.zip 65 | - name: Build for production 66 | run: pnpm build 67 | 68 | - name: Create release on Sentry 69 | run: | 70 | npx @sentry/cli releases new ${{ env.VERSION }} --project ${{ env.SENTRY_PROJECT }} --org ${{ env.SENTRY_ORG }} 71 | npx @sentry/cli releases files ${{ env.VERSION }} upload-sourcemaps ./dist --rewrite --project ${{ env.SENTRY_PROJECT }} --org ${{ env.SENTRY_ORG }} 72 | npx @sentry/cli releases finalize ${{ env.VERSION }} 73 | env: 74 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 75 | SENTRY_ORG: paneon 76 | SENTRY_PROJECT: siyuan-database-properties-plugin 77 | 78 | - name: Release 79 | uses: ncipollo/release-action@v1 80 | with: 81 | allowUpdates: true 82 | artifactErrorsFailBuild: true 83 | artifacts: "package.zip" 84 | omitBodyDuringUpdate: true 85 | omitNameDuringUpdate: true 86 | token: ${{ secrets.GITHUB_TOKEN }} 87 | prerelease: false 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | package-lock.json 5 | package.zip 6 | node_modules 7 | dev 8 | dist 9 | build 10 | tmp 11 | .env 12 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | pnpm 9.12.0 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Database Properties Panel Plugin - Changelog 2 | 3 | [Changelog](./CHANGELOG.md) 4 | 5 | ### [1.3.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v1.3.0) 6 | 7 | - 🇺🇸 Hide database name when there is only one database 8 | - 🇺🇸 Overall layout adjustments to reduce used space 9 | - 🇺🇸 Fix bug which might have shown the panel although no databases are visible 10 | - 🇨🇳 在只有一个数据库时隐藏数据库名称 11 | - 🇨🇳 调整整体布局以减少使用空间 12 | - 🇨🇳 修复了在未显示数据库的情况下可能显示面板的错误 13 | 14 | ### [1.2.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v1.2.0) 15 | 16 | - 🇺🇸 Add a toggle to show/hide empty attributes on a per-document basis. 17 | - 🇺🇸 Add a button to add new attributes to the database 18 | - 🇨🇳 添加切换功能,可按文档显示/隐藏空属性。 19 | - 🇨🇳 添加按钮,用于向数据库添加新属性 20 | 21 | ## v1.1.2 - Patch to fix an issue with template fields freezing SiYuan 22 | 23 | ## v1.1.1 - Hotfix to patch a document bug 24 | 25 | ## [1.1.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v1.1.0) 26 | 27 | ### Features 28 | 29 | - Adds toggle which allows to hide the database properties panel for a document. This setting is stored per document. 30 | - Adds a tabbed layout to reduce used space for the panel (requires SiYuan >=3.0.17) 31 | - Removed setting to disable editing as it works well. 32 | 33 | ## [1.0.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v1.0.0) 34 | 35 | ### Features 36 | 37 | - Add ability to edit properties 38 | 39 | ### Fixes 40 | 41 | - "hide empty properties" now correctly hides empty number fields 42 | 43 | ## [0.5.2](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.5.2) 44 | 45 | - Fix #20: Remove text indentation 46 | 47 | ## [0.5.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.5.0) 48 | 49 | - Add support for column icons 50 | 51 | ## [0.4.4](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.4) 52 | 53 | - Fix issues with empty asset fields 54 | 55 | ## [0.4.3](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.3) 56 | 57 | - Fix issues with empty relation fields 58 | 59 | ## [0.4.2](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.2) 60 | 61 | - Fix an issue with guest users 62 | 63 | ## [0.4.1](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.1) 64 | 65 | - Improve hiding primary keys 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alexander Pape 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Database Properties Panel Plugin 2 | 3 | [中文版](./README.zh_CN.md) 4 | [Changelog](./CHANGELOG.md) 5 | 6 | ## Overview 7 | 8 | The SiYuan Database Properties Panel plugin enhances your SiYuan experience by enabling a feature commonly found in similar tools. 9 | It's heavily inspired by the now archived and no longer maintained [SiYuan-Attributes-Panel](https://github.com/TransMux/SiYuan-Attributes-Panel/). 10 | This plugin allows users to view database row attributes directly on the dedicated pages created from those rows. 11 | 12 | ## Changes in last release 13 | 14 | ### [1.3.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v1.3.0) 15 | 16 | - 🇺🇸 Hide database name when there is only one database 17 | - 🇺🇸 Overall layout adjustments to reduce used space 18 | - 🇺🇸 Fix bug which might have shown the panel although no databases are visible 19 | 20 | ## Features 21 | 22 | - Attribute Display: Automatically display and edit database row attributes on their corresponding pages 23 | - Familiar Interface: Provides a user experience similar to popular note-taking applications like Notion and Anytype while keeping the design of SiYuan 24 | - Allow optionally to hide the primary key 25 | - Allow optionally to hide empty fields 26 | 27 | ## Data Security Statement 28 | 29 | Out of absolute importance to data security, this plugin hereby declares that all APIs used by the plugin, and the code is completely open source (uncompiled and not confused), everyone is welcome to report security issues 30 | 31 | This plugin depends on the following APIs: 32 | 33 | - `/api/av/getAttributeViewKeys`: This parameter is used to obtain existing attributes through the new attribute-view 34 | 35 | ## Plugin permissions 36 | 37 | About data: The modification of your data by this plug-in is limited to the specified modification of the properties of the specified block according to the user's instructions under the user's operation, and will not modify anything else 38 | About UI: The user interface changes are limited to adding a properties panel under the document title, and have no effect on the rest of the section 39 | About networking: This plug-in is completely local and does not include any extranet communication 40 | 41 | ## Support & Feedback 42 | 43 | Please use Github issues to submit bugs or request features. 44 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # Database Properties Panel Plugin 2 | 3 | [英文版](./README.md) 4 | 5 | ## 概述 6 | 7 | 思源数据库属性面板插件通过启用类似工具中常见的功能来增强您的思源体验。 8 | 它在很大程度上受到现已归档且不再维护的 [SiYuan-Attributes-Panel](https://github.com/TransMux/SiYuan-Attributes-Panel/) 的启发。 9 | 该插件允许用户直接在根据这些行创建的专用页面上查看数据库行的属性。 10 | 11 | ## 最近更改 12 | 13 | ## 上一版本中的更改 14 | 15 | ### [1.3.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v1.3.0) 16 | 17 | - 🇨🇳 在只有一个数据库时隐藏数据库名称 18 | - 🇨🇳 调整整体布局以减少使用空间 19 | - 🇨🇳 修复了在未显示数据库的情况下可能显示面板的错误 20 | 21 | ## 旧的变化 22 | - [1.0.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v1.0.0) - 添加编辑功能 23 | - [0.5.2](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.5.2) - 修复 #20: 移除文本缩进 24 | - [0.5.0](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.5.0) - 添加对列图标的支持 25 | - [0.4.4](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.4) - 修复资产字段为空的问题 26 | - [0.4.3](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.3) - 修复关系值为空的问题 27 | - [0.4.2](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.2) - 修复访客用户问题 28 | - [0.4.1](https://github.com/Macavity/siyuan-database-properties-panel/releases/tag/v0.4.1) - 改进隐藏主键的功能 29 | 30 | ## 功能 31 | 32 | - 属性显示: 在相应页面上自动显示数据库行属性 33 | - 熟悉的界面: 提供类似于 Notion 和 Anytype 等流行笔记应用程序的用户体验,同时保持思源的设计风格 34 | 35 | ### 数据安全声明 36 | 37 | 出于对数据安全的绝对重视,本插件特此声明,插件使用的所有 API 和代码均完全开源(未编译且不会混淆),欢迎大家报告安全问题 38 | 39 | 本插件依赖于以下 API: 40 | 41 | - `/api/av/getAttributeViewKeys`: 该参数用于通过新的属性视图获取现有属性 42 | 43 | ## 插件权限 44 | 45 | 关于数据: 本插件对您的数据的修改仅限于在用户操作下根据用户指令对指定块的属性进行指定修改,而不会修改其他任何内容 46 | 关于用户界面: 用户界面的修改仅限于在文档标题下添加属性面板,对其他部分没有影响。 47 | 关于联网: 该插件完全本地化,不包括任何外部网络通信 48 | 49 | ## 支持与反馈 50 | 51 | 请使用 Github issues 提交错误或功能请求。 52 | -------------------------------------------------------------------------------- /asset/DatabasePropertyPanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macavity/siyuan-database-properties-panel/4522bcb9247e0e8c0bd372c9b1a2b1012e6d27d7/asset/DatabasePropertyPanel.png -------------------------------------------------------------------------------- /asset/DatabaseTable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macavity/siyuan-database-properties-panel/4522bcb9247e0e8c0bd372c9b1a2b1012e6d27d7/asset/DatabaseTable.png -------------------------------------------------------------------------------- /asset/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macavity/siyuan-database-properties-panel/4522bcb9247e0e8c0bd372c9b1a2b1012e6d27d7/asset/action.png -------------------------------------------------------------------------------- /asset/preview.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macavity/siyuan-database-properties-panel/4522bcb9247e0e8c0bd372c9b1a2b1012e6d27d7/asset/preview.xcf -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import tseslint from "typescript-eslint"; 3 | import eslintPluginSvelte from "eslint-plugin-svelte"; 4 | import svelteParser from "svelte-eslint-parser"; 5 | import js from "@eslint/js"; 6 | 7 | export default [ 8 | js.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | { 11 | files: ["src/**/*.svelte"], 12 | plugins: { 13 | svelte: eslintPluginSvelte, 14 | }, 15 | languageOptions: { 16 | parser: svelteParser, 17 | parserOptions: { 18 | parser: tseslint.parser, 19 | }, 20 | }, 21 | rules: { 22 | ...eslintPluginSvelte.configs.recommended.rules, 23 | // Disable specific a11y rules 24 | "svelte/a11y-click-events-have-key-events": "off", 25 | "svelte/a11y-missing-attribute": "off", 26 | }, 27 | }, 28 | { 29 | files: ["src/**/*.{js,ts,svelte}"], 30 | languageOptions: { 31 | globals: { 32 | ...globals.browser, 33 | }, 34 | }, 35 | rules: { 36 | "@typescript-eslint/no-unused-vars": "off", 37 | }, 38 | }, 39 | { 40 | rules: { 41 | // Disable specific a11y rules 42 | "svelte/a11y-click-events-have-key-events": "off", 43 | "svelte/no-static-element-interactions": "off", 44 | "svelte/a11y-missing-attribute": "off", 45 | }, 46 | }, 47 | { 48 | ignores: [ 49 | "src/**/*.test.ts", 50 | "src/libs/siyuan/**/*", 51 | "test/**", 52 | "vue/**", 53 | "scripts/**", 54 | "dev/**", 55 | "yaml-plugin.js", 56 | "dist/**", 57 | ], 58 | }, 59 | ]; 60 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macavity/siyuan-database-properties-panel/4522bcb9247e0e8c0bd372c9b1a2b1012e6d27d7/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siyuan-database-properties-panel", 3 | "version": "1.3.0", 4 | "type": "module", 5 | "description": "Plugin for Siyuan (https://b3log.org/siyuan). Provides a panel to view database properties of a page created from a database.", 6 | "repository": "https://github.com/Macavity/siyuan-database-properties-panel", 7 | "homepage": "https://github.com/Macavity/siyuan-database-properties-panel", 8 | "author": "Macavity", 9 | "license": "MIT", 10 | "scripts": { 11 | "make-link": "node --no-warnings ./scripts/make_dev_link.js", 12 | "make-link-win": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./scripts/elevate.ps1 -scriptPath ./scripts/make_dev_link.js", 13 | "dev": "NODE_ENV=development vite build", 14 | "dev-watch": "NODE_ENV=development vite build --watch", 15 | "update-version": "node --no-warnings ./scripts/update_version.js", 16 | "build": "vite build", 17 | "check-i18n": "node ./scripts/check_i18n.js", 18 | "lint": "eslint", 19 | "test": "vitest", 20 | "make-install": "vite build && node --no-warnings ./scripts/make_install.js" 21 | }, 22 | "dependencies": { 23 | "@eslint/js": "^9.12.0", 24 | "@sentry/browser": "^8.34.0", 25 | "@tsconfig/svelte": "^4.0.1", 26 | "@types/jest": "^29.5.14", 27 | "@types/node": "^20.16.11", 28 | "@typescript-eslint/eslint-plugin": "^8.8.1", 29 | "@typescript-eslint/parser": "^8.8.1", 30 | "dayjs": "^1.11.13", 31 | "eslint": "^9.12.0", 32 | "eslint-config-prettier": "^9.1.0", 33 | "eslint-plugin-prettier": "^5.2.1", 34 | "eslint-plugin-svelte": "^2.45.1", 35 | "fast-glob": "^3.2.12", 36 | "glob": "^11.0.0", 37 | "globals": "^15.11.0", 38 | "js-yaml": "^4.1.0", 39 | "lodash": "^4.17.21", 40 | "minimist": "^1.2.8", 41 | "prettier": "^3.3.3", 42 | "rollup-plugin-livereload": "^2.0.5", 43 | "sass": "^1.63.3", 44 | "semver": "^7.6.3", 45 | "siyuan": "^1.0.8", 46 | "siyuan-app": "git+https://github.com/siyuan-note/siyuan.git#v3.1.20", 47 | "svelte": "^5.0.0", 48 | "svelte-eslint-parser": "^0.42.0", 49 | "ts-node": "^10.9.1", 50 | "typescript": "^5.5.0", 51 | "typescript-eslint": "^8.8.1", 52 | "vite": "^5.4.17", 53 | "vite-plugin-static-copy": "^1.0.2", 54 | "vite-plugin-zip-pack": "^1.0.5", 55 | "@sveltejs/vite-plugin-svelte": "^4.0.4", 56 | "@types/lodash": "^4.17.14" 57 | }, 58 | "devDependencies": { 59 | "@testing-library/jest-dom": "^6.6.3", 60 | "@testing-library/svelte": "^5.2.6", 61 | "@types/semver": "^7.5.8", 62 | "@vitest/ui": "^3.0.3", 63 | "happy-dom": "^16.7.2", 64 | "jsdom": "^25.0.1", 65 | "vitest": "^3.0.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siyuan-database-properties-panel", 3 | "author": "Macavity", 4 | "url": "https://github.com/Macavity/siyuan-database-properties-panel", 5 | "version": "1.3.0", 6 | "minAppVersion": "3.0.17", 7 | "backends": [ 8 | "windows", 9 | "linux", 10 | "darwin", 11 | "docker", 12 | "ios", 13 | "android" 14 | ], 15 | "frontends": [ 16 | "desktop", 17 | "mobile", 18 | "browser-desktop", 19 | "browser-mobile", 20 | "desktop-window" 21 | ], 22 | "displayName": { 23 | "en_US": "Database Properties Panel", 24 | "zh_CN": "数据库属性面板" 25 | }, 26 | "description": { 27 | "en_US": "Provides a panel to view database properties of a page created from a database.", 28 | "zh_CN": "提供一个面板,用于查看从数据库创建的页面的数据库属性。" 29 | }, 30 | "readme": { 31 | "en_US": "README.md", 32 | "zh_CN": "README.zh_CN.md" 33 | }, 34 | "funding": { 35 | "openCollective": "", 36 | "patreon": "", 37 | "github": "" 38 | }, 39 | "keywords": [ 40 | "plugin", 41 | "database", 42 | "properties", 43 | "attributes" 44 | ] 45 | } -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macavity/siyuan-database-properties-panel/4522bcb9247e0e8c0bd372c9b1a2b1012e6d27d7/preview.png -------------------------------------------------------------------------------- /preview.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macavity/siyuan-database-properties-panel/4522bcb9247e0e8c0bd372c9b1a2b1012e6d27d7/preview.xcf -------------------------------------------------------------------------------- /public/i18n/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "databasePropertyPanel": "Database properties", 3 | "configAllowErrorReportingTitle": "Allow error reporting", 4 | "configAllowErrorReportingDesc": "If you notice broken behaviour in the plugin, please activate this before creating an issue on Github. It will make finding a solution easier.", 5 | "configAllowEditingTitle": "Allow editing", 6 | "configAllowEditingDesc": "(Experimental) Allow editing of the page properties in the attribute panel. This requires SiYuan version 3.0.17", 7 | "configShowBuiltInPropertiesTitle": "Show built-in properties", 8 | "configShowBuiltInPropertiesDesc": "Show built-in properties always in the attribute panel", 9 | "configShowEmptyPropertiesTitle": "Show empty properties", 10 | "configShowEmptyPropertiesDesc": "Use to hide properties that have no value in the panel.", 11 | "configShowPrimaryKeyTitle": "Show primary key", 12 | "configShowPrimaryKeyDesc": "Show primary key in the attribute panel, which is usually the same as the page title.", 13 | "configShowDatabasePropertiesTitle": "Show database properties", 14 | "configShowDatabasePropertiesDesc": "Show database properties always in the attribute panel", 15 | "configUseMultipleTableLayout": "Layout for multiple databases", 16 | "configUseMultipleTableLayoutDesc": "Decide how multiple databases are displayed in the panel.", 17 | "showEmptyAttributesToggle": "Show Empty Attributes", 18 | "hideEmptyAttributesToggle": "Hide Empty Attributes", 19 | "overrideSettings": "Override default settings", 20 | "collapse": "Collapse", 21 | "expand": "Expand" 22 | } -------------------------------------------------------------------------------- /public/i18n/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "databasePropertyPanel": "数据库属性", 3 | "configAllowErrorReportingTitle": "允许错误报告", 4 | "configAllowErrorReportingDesc": "如果您发现插件中的行为出现故障,请在 Github 上创建问题前激活此功能。这样会更容易找到解决方案。", 5 | "configAllowEditingTitle": "允许编辑", 6 | "configAllowEditingDesc": "(试验性) 允许在属性面板中编辑页面属性。需要思源 3.0.17 版", 7 | "configShowBuiltInPropertiesTitle": "显示内置属性", 8 | "configShowBuiltInPropertiesDesc": "在属性面板中始终显示内置属性", 9 | "configShowEmptyPropertiesTitle": "显示空值", 10 | "configShowEmptyPropertiesDesc": "用于隐藏面板中没有值的属性。", 11 | "configShowPrimaryKeyTitle": "显示主键", 12 | "configShowPrimaryKeyDesc": "在属性面板中显示主键,通常与页面标题相同。", 13 | "configShowDatabasePropertiesTitle": "显示数据库属性", 14 | "configShowDatabasePropertiesDesc": "在属性面板中始终显示数据库属性", 15 | "configUseMultipleTableLayout": "多个数据库的布局", 16 | "configUseMultipleTableLayoutDesc": "决定如何在面板中显示多个数据库。", 17 | "showEmptyAttributesToggle": "显示空属性", 18 | "hideEmptyAttributesToggle": "隐藏空属性", 19 | "overrideSettings": "覆盖默认设置", 20 | "collapse": "折叠", 21 | "expand": "展开" 22 | } -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | build 3 | dist 4 | *.exe 5 | *.spec 6 | -------------------------------------------------------------------------------- /scripts/check_i18n.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | const baseLang = 'en_US'; 9 | const langDir = path.join(__dirname, '../public/i18n'); 10 | const baseLangFile = path.join(langDir, `${baseLang}.json`); 11 | 12 | function getJsonContent(filePath) { 13 | return JSON.parse(fs.readFileSync(filePath, 'utf8')); 14 | } 15 | 16 | function checkTranslations(baseContent, targetContent, targetLang) { 17 | const missingKeys = []; 18 | for (const key in baseContent) { 19 | if (!targetContent.hasOwnProperty(key)) { 20 | missingKeys.push(key); 21 | } 22 | } 23 | if (missingKeys.length > 0) { 24 | console.error(`Missing translations in ${targetLang}:`, missingKeys); 25 | process.exit(1); 26 | } 27 | } 28 | 29 | function main() { 30 | const baseContent = getJsonContent(baseLangFile); 31 | const files = fs.readdirSync(langDir).filter(file => file.endsWith('.json') && file !== `${baseLang}.json`); 32 | 33 | files.forEach(file => { 34 | const targetLang = path.basename(file, '.json'); 35 | const targetContent = getJsonContent(path.join(langDir, file)); 36 | checkTranslations(baseContent, targetContent, targetLang); 37 | }); 38 | 39 | console.log('All translations are complete.'); 40 | } 41 | 42 | main(); -------------------------------------------------------------------------------- /scripts/elevate.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 by frostime. All Rights Reserved. 2 | # @Author : frostime 3 | # @Date : 2024-09-06 19:15:53 4 | # @FilePath : /scripts/elevate.ps1 5 | # @LastEditTime : 2024-09-06 19:39:13 6 | # @Description : Force to elevate the script to admin privilege. 7 | 8 | param ( 9 | [string]$scriptPath 10 | ) 11 | 12 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition 13 | $projectDir = Split-Path -Parent $scriptDir 14 | 15 | if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { 16 | $args = "-NoProfile -ExecutionPolicy Bypass -File `"" + $MyInvocation.MyCommand.Path + "`" -scriptPath `"" + $scriptPath + "`"" 17 | Start-Process powershell.exe -Verb RunAs -ArgumentList $args -WorkingDirectory $projectDir 18 | exit 19 | } 20 | 21 | Set-Location -Path $projectDir 22 | & node $scriptPath 23 | 24 | pause 25 | -------------------------------------------------------------------------------- /scripts/make_dev_link.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2023-07-15 15:31:31 5 | * @FilePath : /scripts/make_dev_link.js 6 | * @LastEditTime : 2024-09-06 18:13:53 7 | * @Description : 8 | */ 9 | // make_dev_link.js 10 | import fs from 'fs'; 11 | import { log, error, getSiYuanDir, chooseTarget, getThisPluginName, makeSymbolicLink } from './utils.js'; 12 | 13 | let targetDir = ''; 14 | 15 | /** 16 | * 1. Get the parent directory to install the plugin 17 | */ 18 | log('>>> Try to visit constant "targetDir" in make_dev_link.js...'); 19 | if (targetDir === '') { 20 | log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); 21 | let res = await getSiYuanDir(); 22 | 23 | if (!res || res.length === 0) { 24 | log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); 25 | let env = process.env?.SIYUAN_PLUGIN_DIR; 26 | if (env) { 27 | targetDir = env; 28 | log(`\tGot target directory from environment variable "SIYUAN_PLUGIN_DIR": ${targetDir}`); 29 | } else { 30 | error('\tCan not get SiYuan directory from environment variable "SIYUAN_PLUGIN_DIR", failed!'); 31 | process.exit(1); 32 | } 33 | } else { 34 | targetDir = await chooseTarget(res); 35 | } 36 | 37 | log(`>>> Successfully got target directory: ${targetDir}`); 38 | } 39 | if (!fs.existsSync(targetDir)) { 40 | error(`Failed! Plugin directory not exists: "${targetDir}"`); 41 | error('Please set the plugin directory in scripts/make_dev_link.js'); 42 | process.exit(1); 43 | } 44 | 45 | /** 46 | * 2. The dev directory, which contains the compiled plugin code 47 | */ 48 | const devDir = `${process.cwd()}/dev`; 49 | if (!fs.existsSync(devDir)) { 50 | fs.mkdirSync(devDir); 51 | } 52 | 53 | 54 | /** 55 | * 3. The target directory to make symbolic link to dev directory 56 | */ 57 | const name = getThisPluginName(); 58 | if (name === null) { 59 | process.exit(1); 60 | } 61 | const targetPath = `${targetDir}/${name}`; 62 | 63 | /** 64 | * 4. Make symbolic link 65 | */ 66 | makeSymbolicLink(devDir, targetPath); 67 | -------------------------------------------------------------------------------- /scripts/make_install.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-03-28 20:03:59 5 | * @FilePath : /scripts/make_install.js 6 | * @LastEditTime : 2024-09-06 18:08:19 7 | * @Description : 8 | */ 9 | // make_install.js 10 | import fs from 'fs'; 11 | import { log, error, getSiYuanDir, chooseTarget, copyDirectory, getThisPluginName } from './utils.js'; 12 | 13 | let targetDir = ''; 14 | 15 | /** 16 | * 1. Get the parent directory to install the plugin 17 | */ 18 | log('>>> Try to visit constant "targetDir" in make_install.js...'); 19 | if (targetDir === '') { 20 | log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); 21 | let res = await getSiYuanDir(); 22 | 23 | if (res === null || res === undefined || res.length === 0) { 24 | error('>>> Can not get SiYuan directory automatically'); 25 | process.exit(1); 26 | } else { 27 | targetDir = await chooseTarget(res); 28 | } 29 | log(`>>> Successfully got target directory: ${targetDir}`); 30 | } 31 | if (!fs.existsSync(targetDir)) { 32 | error(`Failed! Plugin directory not exists: "${targetDir}"`); 33 | error('Please set the plugin directory in scripts/make_install.js'); 34 | process.exit(1); 35 | } 36 | 37 | /** 38 | * 2. The dist directory, which contains the compiled plugin code 39 | */ 40 | const distDir = `${process.cwd()}/dist`; 41 | if (!fs.existsSync(distDir)) { 42 | fs.mkdirSync(distDir); 43 | } 44 | 45 | /** 46 | * 3. The target directory to install the plugin 47 | */ 48 | const name = getThisPluginName(); 49 | if (name === null) { 50 | process.exit(1); 51 | } 52 | const targetPath = `${targetDir}/${name}`; 53 | 54 | /** 55 | * 4. Copy the compiled plugin code to the target directory 56 | */ 57 | copyDirectory(distDir, targetPath); 58 | -------------------------------------------------------------------------------- /scripts/update_icons.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | const iconPaths = [ 5 | "./node_modules/siyuan-app/app/appearance/icons/ant/icon.js", 6 | "./node_modules/siyuan-app/app/appearance/icons/material/icon.js", 7 | ]; 8 | 9 | // Regular expression to match all id attributes within tags 10 | const idRegex = //g; 11 | const ids = []; 12 | 13 | iconPaths.forEach((filePath) => { 14 | const content = fs.readFileSync(filePath, "utf-8"); 15 | let match; 16 | while ((match = idRegex.exec(content)) !== null) { 17 | if (!ids.includes(match[1])) { 18 | ids.push(match[1]); 19 | } 20 | } 21 | }); 22 | 23 | // Create a string union type 24 | const unionType = `export type SiYuanIcon = ${ids.map((id) => `"${id}"`).join(" | ")};`; 25 | 26 | // Path to the output TypeScript file 27 | const outputFilePath = path.resolve("./src/types/SiyuanIcon.ts"); 28 | 29 | // Write the union type to the TypeScript file 30 | fs.writeFileSync(outputFilePath, unionType, "utf-8"); 31 | 32 | console.log("IconId type generated successfully."); 33 | -------------------------------------------------------------------------------- /scripts/update_version.js: -------------------------------------------------------------------------------- 1 | // const fs = require('fs'); 2 | // const path = require('path'); 3 | // const readline = require('readline'); 4 | import fs from 'node:fs'; 5 | import path from 'node:path'; 6 | import readline from 'node:readline'; 7 | 8 | // Utility to read JSON file 9 | function readJsonFile(filePath) { 10 | return new Promise((resolve, reject) => { 11 | fs.readFile(filePath, 'utf8', (err, data) => { 12 | if (err) return reject(err); 13 | try { 14 | const jsonData = JSON.parse(data); 15 | resolve(jsonData); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | // Utility to write JSON file 24 | function writeJsonFile(filePath, jsonData) { 25 | return new Promise((resolve, reject) => { 26 | fs.writeFile(filePath, JSON.stringify(jsonData, null, 2), 'utf8', (err) => { 27 | if (err) return reject(err); 28 | resolve(); 29 | }); 30 | }); 31 | } 32 | 33 | // Utility to prompt the user for input 34 | function promptUser(query) { 35 | const rl = readline.createInterface({ 36 | input: process.stdin, 37 | output: process.stdout 38 | }); 39 | return new Promise((resolve) => rl.question(query, (answer) => { 40 | rl.close(); 41 | resolve(answer); 42 | })); 43 | } 44 | 45 | // Function to parse the version string 46 | function parseVersion(version) { 47 | const [major, minor, patch] = version.split('.').map(Number); 48 | return { major, minor, patch }; 49 | } 50 | 51 | // Function to auto-increment version parts 52 | function incrementVersion(version, type) { 53 | let { major, minor, patch } = parseVersion(version); 54 | 55 | switch (type) { 56 | case 'major': 57 | major++; 58 | minor = 0; 59 | patch = 0; 60 | break; 61 | case 'minor': 62 | minor++; 63 | patch = 0; 64 | break; 65 | case 'patch': 66 | patch++; 67 | break; 68 | default: 69 | break; 70 | } 71 | 72 | return `${major}.${minor}.${patch}`; 73 | } 74 | 75 | // Main script 76 | (async function () { 77 | try { 78 | const pluginJsonPath = path.join(process.cwd(), 'plugin.json'); 79 | const packageJsonPath = path.join(process.cwd(), 'package.json'); 80 | 81 | // Read both JSON files 82 | const pluginData = await readJsonFile(pluginJsonPath); 83 | const packageData = await readJsonFile(packageJsonPath); 84 | 85 | // Get the current version from both files (assuming both have the same version) 86 | const currentVersion = pluginData.version || packageData.version; 87 | console.log(`\n🌟 Current version: \x1b[36m${currentVersion}\x1b[0m\n`); 88 | 89 | // Calculate potential new versions for auto-update 90 | const newPatchVersion = incrementVersion(currentVersion, 'patch'); 91 | const newMinorVersion = incrementVersion(currentVersion, 'minor'); 92 | const newMajorVersion = incrementVersion(currentVersion, 'major'); 93 | 94 | // Prompt the user with formatted options 95 | console.log('🔄 How would you like to update the version?\n'); 96 | console.log(` 1️⃣ Auto update \x1b[33mpatch\x1b[0m version (new version: \x1b[32m${newPatchVersion}\x1b[0m)`); 97 | console.log(` 2️⃣ Auto update \x1b[33mminor\x1b[0m version (new version: \x1b[32m${newMinorVersion}\x1b[0m)`); 98 | console.log(` 3️⃣ Auto update \x1b[33mmajor\x1b[0m version (new version: \x1b[32m${newMajorVersion}\x1b[0m)`); 99 | console.log(` 4️⃣ Input version \x1b[33mmanually\x1b[0m`); 100 | // Press 0 to skip version update 101 | console.log(' 0️⃣ Quit without updating\n'); 102 | 103 | const updateChoice = await promptUser('👉 Please choose (1/2/3/4): '); 104 | 105 | let newVersion; 106 | 107 | switch (updateChoice.trim()) { 108 | case '1': 109 | newVersion = newPatchVersion; 110 | break; 111 | case '2': 112 | newVersion = newMinorVersion; 113 | break; 114 | case '3': 115 | newVersion = newMajorVersion; 116 | break; 117 | case '4': 118 | newVersion = await promptUser('✍️ Please enter the new version (in a.b.c format): '); 119 | break; 120 | case '0': 121 | console.log('\n🛑 Skipping version update.'); 122 | return; 123 | default: 124 | console.log('\n❌ Invalid option, no version update.'); 125 | return; 126 | } 127 | 128 | // Update the version in both plugin.json and package.json 129 | pluginData.version = newVersion; 130 | packageData.version = newVersion; 131 | 132 | // Write the updated JSON back to files 133 | await writeJsonFile(pluginJsonPath, pluginData); 134 | await writeJsonFile(packageJsonPath, packageData); 135 | 136 | console.log(`\n✅ Version successfully updated to: \x1b[32m${newVersion}\x1b[0m\n`); 137 | 138 | } catch (error) { 139 | console.error('❌ Error:', error); 140 | } 141 | })(); 142 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-09-06 17:42:57 5 | * @FilePath : /scripts/utils.js 6 | * @LastEditTime : 2024-09-06 19:23:12 7 | * @Description : 8 | */ 9 | // common.js 10 | import fs from 'fs'; 11 | import path from 'node:path'; 12 | import http from 'node:http'; 13 | import readline from 'node:readline'; 14 | 15 | // Logging functions 16 | export const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); 17 | export const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); 18 | 19 | // HTTP POST headers 20 | export const POST_HEADER = { 21 | "Content-Type": "application/json", 22 | }; 23 | 24 | // Fetch function compatible with older Node.js versions 25 | export async function myfetch(url, options) { 26 | return new Promise((resolve, reject) => { 27 | let req = http.request(url, options, (res) => { 28 | let data = ''; 29 | res.on('data', (chunk) => { 30 | data += chunk; 31 | }); 32 | res.on('end', () => { 33 | resolve({ 34 | ok: true, 35 | status: res.statusCode, 36 | json: () => JSON.parse(data) 37 | }); 38 | }); 39 | }); 40 | req.on('error', (e) => { 41 | reject(e); 42 | }); 43 | req.end(); 44 | }); 45 | } 46 | 47 | /** 48 | * Fetch SiYuan workspaces from port 6806 49 | * @returns {Promise} 50 | */ 51 | export async function getSiYuanDir() { 52 | let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; 53 | let conf = {}; 54 | try { 55 | let response = await myfetch(url, { 56 | method: 'POST', 57 | headers: POST_HEADER 58 | }); 59 | if (response.ok) { 60 | conf = await response.json(); 61 | } else { 62 | error(`\tHTTP-Error: ${response.status}`); 63 | return null; 64 | } 65 | } catch (e) { 66 | error(`\tError: ${e}`); 67 | error("\tPlease make sure SiYuan is running!!!"); 68 | return null; 69 | } 70 | return conf?.data; // 保持原始返回值 71 | } 72 | 73 | /** 74 | * Choose target workspace 75 | * @param {{path: string}[]} workspaces 76 | * @returns {string} The path of the selected workspace 77 | */ 78 | export async function chooseTarget(workspaces) { 79 | let count = workspaces.length; 80 | log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`); 81 | workspaces.forEach((workspace, i) => { 82 | log(`\t[${i}] ${workspace.path}`); 83 | }); 84 | 85 | if (count === 1) { 86 | return `${workspaces[0].path}/data/plugins`; 87 | } else { 88 | const rl = readline.createInterface({ 89 | input: process.stdin, 90 | output: process.stdout 91 | }); 92 | let index = await new Promise((resolve) => { 93 | rl.question(`\tPlease select a workspace[0-${count - 1}]: `, (answer) => { 94 | resolve(answer); 95 | }); 96 | }); 97 | rl.close(); 98 | return `${workspaces[index].path}/data/plugins`; 99 | } 100 | } 101 | 102 | /** 103 | * Check if two paths are the same 104 | * @param {string} path1 105 | * @param {string} path2 106 | * @returns {boolean} 107 | */ 108 | export function cmpPath(path1, path2) { 109 | path1 = path1.replace(/\\/g, '/'); 110 | path2 = path2.replace(/\\/g, '/'); 111 | if (path1[path1.length - 1] !== '/') { 112 | path1 += '/'; 113 | } 114 | if (path2[path2.length - 1] !== '/') { 115 | path2 += '/'; 116 | } 117 | return path1 === path2; 118 | } 119 | 120 | export function getThisPluginName() { 121 | if (!fs.existsSync('./plugin.json')) { 122 | process.chdir('../'); 123 | if (!fs.existsSync('./plugin.json')) { 124 | error('Failed! plugin.json not found'); 125 | return null; 126 | } 127 | } 128 | 129 | const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); 130 | const name = plugin?.name; 131 | if (!name) { 132 | error('Failed! Please set plugin name in plugin.json'); 133 | return null; 134 | } 135 | 136 | return name; 137 | } 138 | 139 | export function copyDirectory(srcDir, dstDir) { 140 | if (!fs.existsSync(dstDir)) { 141 | fs.mkdirSync(dstDir); 142 | log(`Created directory ${dstDir}`); 143 | } 144 | 145 | fs.readdirSync(srcDir, { withFileTypes: true }).forEach((file) => { 146 | const src = path.join(srcDir, file.name); 147 | const dst = path.join(dstDir, file.name); 148 | 149 | if (file.isDirectory()) { 150 | copyDirectory(src, dst); 151 | } else { 152 | fs.copyFileSync(src, dst); 153 | log(`Copied file: ${src} --> ${dst}`); 154 | } 155 | }); 156 | log(`All files copied!`); 157 | } 158 | 159 | 160 | export function makeSymbolicLink(srcPath, targetPath) { 161 | if (!fs.existsSync(targetPath)) { 162 | // fs.symlinkSync(srcPath, targetPath, 'junction'); 163 | //Go 1.23 no longer supports junctions as symlinks 164 | //Please refer to https://github.com/siyuan-note/siyuan/issues/12399 165 | fs.symlinkSync(srcPath, targetPath, 'dir'); 166 | log(`Done! Created symlink ${targetPath}`); 167 | return; 168 | } 169 | 170 | //Check the existed target path 171 | let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); 172 | if (!isSymbol) { 173 | error(`Failed! ${targetPath} already exists and is not a symbolic link`); 174 | return; 175 | } 176 | let existedPath = fs.readlinkSync(targetPath); 177 | if (cmpPath(existedPath, srcPath)) { 178 | log(`Good! ${targetPath} is already linked to ${srcPath}`); 179 | } else { 180 | error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${existedPath}`); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/PluginPanel.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 |
50 | 51 | {#each avData as av, i} 52 | { 57 | openAvPanel(av.avID); 58 | }} 59 | onkeydown={() => { 60 | openAvPanel(av.avID); 61 | }} 62 | > 63 | 64 | {av.avName} 65 | 66 | {/each} 67 | 68 | {#if !isCollapsed} 69 | {#if allowEditing} 70 | 71 | {:else} 72 | 73 | {/if} 74 | {/if} 75 |
76 | -------------------------------------------------------------------------------- /src/PluginPanel.test.js: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | // All mocks must be defined before imports 4 | vi.mock("@/stores/localSettingStore", () => { 5 | const state = new Map(); 6 | state.set("test-block-id", { isCollapsed: false }); 7 | 8 | return { 9 | settingsStore: { 10 | subscribe: vi.fn((callback) => { 11 | callback(state); 12 | return () => {}; 13 | }), 14 | get: vi.fn((blockId) => ({ 15 | isCollapsed: false, 16 | lastSelectedAttributeView: null, 17 | })), 18 | activateTab: vi.fn(), 19 | toggleCollapsed: vi.fn(), 20 | }, 21 | }; 22 | }); 23 | 24 | vi.mock("@/components/ui/Icon.svelte", () => ({ 25 | default: vi.fn().mockImplementation(() => ({ 26 | $$render: () => '
', 27 | })), 28 | })); 29 | 30 | vi.mock("./components/AttributeViewPanel.svelte", async () => ({ 31 | default: (await import("../test/mocks/AttributeViewPanel.svelte")).default, 32 | })); 33 | 34 | vi.mock("./components/AttributeViewPanelNative.svelte", async () => ({ 35 | default: (await import("../test/mocks/AttributeViewPanelNative.svelte")) 36 | .default, 37 | })); 38 | 39 | vi.mock("@/components/ProtyleBreadcrumb.svelte", async () => ({ 40 | default: (await import("../test/mocks/ProtyleBreadcrumb.svelte")).default, 41 | })); 42 | 43 | // Regular imports after all mocks 44 | import { describe, it, expect, beforeEach } from "vitest"; 45 | import { render, fireEvent } from "@testing-library/svelte"; 46 | import PluginPanel from "./PluginPanel.svelte"; 47 | 48 | describe("PluginPanel", () => { 49 | const mockProtyle = { 50 | // Add minimum required protyle properties 51 | options: {}, 52 | breadcrumb: { render: vi.fn() }, 53 | }; 54 | 55 | const mockProps = { 56 | i18n: {}, 57 | protyle: mockProtyle, 58 | blockId: "test-block-id", 59 | avData: [ 60 | { avID: "av1", avName: "Database 1", keyValues: [] }, 61 | { avID: "av2", avName: "Database 2", keyValues: [] }, 62 | ], 63 | }; 64 | 65 | beforeEach(() => { 66 | vi.clearAllMocks(); 67 | }); 68 | 69 | it("renders without crashing", () => { 70 | const { container } = render(PluginPanel, mockProps); 71 | expect(container.querySelector(".plugin-panel")).toBeTruthy(); 72 | }); 73 | 74 | it("renders all database items in breadcrumb", async () => { 75 | const { settingsStore } = vi.mocked( 76 | await import("@/stores/localSettingStore.ts"), 77 | ); 78 | settingsStore.get.mockReturnValue({ 79 | isCollapsed: false, 80 | lastSelectedAttributeView: null, 81 | }); 82 | 83 | const { container } = render(PluginPanel, mockProps); 84 | const items = container.querySelectorAll(".protyle-breadcrumb__item"); 85 | expect(items.length).toBe(2); 86 | expect(items[0].textContent).toContain("Database 1"); 87 | expect(items[1].textContent).toContain("Database 2"); 88 | }); 89 | 90 | it("calls activateTab when clicking a database item", async () => { 91 | const { settingsStore } = vi.mocked( 92 | await import("@/stores/localSettingStore.ts"), 93 | ); 94 | settingsStore.get.mockReturnValue({ 95 | isCollapsed: false, 96 | lastSelectedAttributeView: null, 97 | }); 98 | 99 | const { container } = render(PluginPanel, mockProps); 100 | const firstItem = container.querySelector(".protyle-breadcrumb__item"); 101 | await fireEvent.click(firstItem); 102 | 103 | expect(settingsStore.activateTab).toHaveBeenCalledWith( 104 | "test-block-id", 105 | "av1", 106 | ); 107 | }); 108 | 109 | it("does not render AttributeViewPanel when collapsed", async () => { 110 | const { settingsStore } = vi.mocked( 111 | await import("@/stores/localSettingStore.ts"), 112 | ); 113 | settingsStore.get.mockReturnValue({ 114 | isCollapsed: true, 115 | lastSelectedAttributeView: null, 116 | }); 117 | settingsStore.subscribe.mockImplementation((callback) => { 118 | callback(new Map([["test-block-id", { isCollapsed: true }]])); 119 | return () => {}; 120 | }); 121 | 122 | const { container } = render(PluginPanel, mockProps); 123 | expect(container.querySelector(".attribute-view-panel")).toBeNull(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /** 3 | * Copyright (c) 2023 frostime. All rights reserved. 4 | * https://github.com/frostime/sy-plugin-template-vite 5 | * 6 | * See API Document in [API.md](https://github.com/siyuan-note/siyuan/blob/master/API.md) 7 | * API 文档见 [API_zh_CN.md](https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md) 8 | */ 9 | 10 | import { fetchPost, fetchSyncPost, IWebSocketData } from "siyuan"; 11 | import { AttributeView } from "@/types/AttributeView"; 12 | 13 | export async function request(url: string, data: any) { 14 | const response: IWebSocketData = await fetchSyncPost(url, data); 15 | const res = response.code === 0 ? response.data : null; 16 | return res; 17 | } 18 | 19 | export interface ApiError { 20 | code: number; 21 | msg: string; 22 | } 23 | 24 | // **************************************** Noteboook **************************************** 25 | 26 | export async function lsNotebooks(): Promise { 27 | const url = "/api/notebook/lsNotebooks"; 28 | return request(url, ""); 29 | } 30 | 31 | export async function openNotebook(notebook: NotebookId) { 32 | const url = "/api/notebook/openNotebook"; 33 | return request(url, { notebook: notebook }); 34 | } 35 | 36 | export async function closeNotebook(notebook: NotebookId) { 37 | const url = "/api/notebook/closeNotebook"; 38 | return request(url, { notebook: notebook }); 39 | } 40 | 41 | export async function renameNotebook(notebook: NotebookId, name: string) { 42 | const url = "/api/notebook/renameNotebook"; 43 | return request(url, { notebook: notebook, name: name }); 44 | } 45 | 46 | export async function createNotebook(name: string): Promise { 47 | const url = "/api/notebook/createNotebook"; 48 | return request(url, { name: name }); 49 | } 50 | 51 | export async function removeNotebook(notebook: NotebookId) { 52 | const url = "/api/notebook/removeNotebook"; 53 | return request(url, { notebook: notebook }); 54 | } 55 | 56 | export async function getNotebookConf( 57 | notebook: NotebookId, 58 | ): Promise { 59 | const data = { notebook: notebook }; 60 | const url = "/api/notebook/getNotebookConf"; 61 | return request(url, data); 62 | } 63 | 64 | export async function setNotebookConf( 65 | notebook: NotebookId, 66 | conf: NotebookConf, 67 | ): Promise { 68 | const data = { notebook: notebook, conf: conf }; 69 | const url = "/api/notebook/setNotebookConf"; 70 | return request(url, data); 71 | } 72 | 73 | // **************************************** File Tree **************************************** 74 | export async function createDocWithMd( 75 | notebook: NotebookId, 76 | path: string, 77 | markdown: string, 78 | ): Promise { 79 | const data = { 80 | notebook: notebook, 81 | path: path, 82 | markdown: markdown, 83 | }; 84 | const url = "/api/filetree/createDocWithMd"; 85 | return request(url, data); 86 | } 87 | 88 | export async function renameDoc( 89 | notebook: NotebookId, 90 | path: string, 91 | title: string, 92 | ): Promise { 93 | const data = { 94 | doc: notebook, 95 | path: path, 96 | title: title, 97 | }; 98 | const url = "/api/filetree/renameDoc"; 99 | return request(url, data); 100 | } 101 | 102 | export async function removeDoc(notebook: NotebookId, path: string) { 103 | const data = { 104 | notebook: notebook, 105 | path: path, 106 | }; 107 | const url = "/api/filetree/removeDoc"; 108 | return request(url, data); 109 | } 110 | 111 | export async function moveDocs( 112 | fromPaths: string[], 113 | toNotebook: NotebookId, 114 | toPath: string, 115 | ) { 116 | const data = { 117 | fromPaths: fromPaths, 118 | toNotebook: toNotebook, 119 | toPath: toPath, 120 | }; 121 | const url = "/api/filetree/moveDocs"; 122 | return request(url, data); 123 | } 124 | 125 | export async function getHPathByPath( 126 | notebook: NotebookId, 127 | path: string, 128 | ): Promise { 129 | const data = { 130 | notebook: notebook, 131 | path: path, 132 | }; 133 | const url = "/api/filetree/getHPathByPath"; 134 | return request(url, data); 135 | } 136 | 137 | export async function getHPathByID(id: BlockId): Promise { 138 | const data = { 139 | id: id, 140 | }; 141 | const url = "/api/filetree/getHPathByID"; 142 | return request(url, data); 143 | } 144 | 145 | export async function getIDsByHPath( 146 | notebook: NotebookId, 147 | path: string, 148 | ): Promise { 149 | const data = { 150 | notebook: notebook, 151 | path: path, 152 | }; 153 | const url = "/api/filetree/getIDsByHPath"; 154 | return request(url, data); 155 | } 156 | 157 | // **************************************** Asset Files **************************************** 158 | 159 | export async function upload( 160 | assetsDirPath: string, 161 | files: any[], 162 | ): Promise { 163 | const form = new FormData(); 164 | form.append("assetsDirPath", assetsDirPath); 165 | for (const file of files) { 166 | form.append("file[]", file); 167 | } 168 | const url = "/api/asset/upload"; 169 | return request(url, form); 170 | } 171 | 172 | // **************************************** Block **************************************** 173 | type DataType = "markdown" | "dom"; 174 | export async function insertBlock( 175 | dataType: DataType, 176 | data: string, 177 | nextID?: BlockId, 178 | previousID?: BlockId, 179 | parentID?: BlockId, 180 | ): Promise { 181 | const payload = { 182 | dataType: dataType, 183 | data: data, 184 | nextID: nextID, 185 | previousID: previousID, 186 | parentID: parentID, 187 | }; 188 | const url = "/api/block/insertBlock"; 189 | return request(url, payload); 190 | } 191 | 192 | export async function prependBlock( 193 | dataType: DataType, 194 | data: string, 195 | parentID: BlockId | DocumentId, 196 | ): Promise { 197 | const payload = { 198 | dataType: dataType, 199 | data: data, 200 | parentID: parentID, 201 | }; 202 | const url = "/api/block/prependBlock"; 203 | return request(url, payload); 204 | } 205 | 206 | export async function appendBlock( 207 | dataType: DataType, 208 | data: string, 209 | parentID: BlockId | DocumentId, 210 | ): Promise { 211 | const payload = { 212 | dataType: dataType, 213 | data: data, 214 | parentID: parentID, 215 | }; 216 | const url = "/api/block/appendBlock"; 217 | return request(url, payload); 218 | } 219 | 220 | export async function updateBlock( 221 | dataType: DataType, 222 | data: string, 223 | id: BlockId, 224 | ): Promise { 225 | const payload = { 226 | dataType: dataType, 227 | data: data, 228 | id: id, 229 | }; 230 | const url = "/api/block/updateBlock"; 231 | return request(url, payload); 232 | } 233 | 234 | export async function deleteBlock(id: BlockId): Promise { 235 | const data = { 236 | id: id, 237 | }; 238 | const url = "/api/block/deleteBlock"; 239 | return request(url, data); 240 | } 241 | 242 | export async function moveBlock( 243 | id: BlockId, 244 | previousID?: PreviousID, 245 | parentID?: ParentID, 246 | ): Promise { 247 | const data = { 248 | id: id, 249 | previousID: previousID, 250 | parentID: parentID, 251 | }; 252 | const url = "/api/block/moveBlock"; 253 | return request(url, data); 254 | } 255 | 256 | export async function foldBlock(id: BlockId) { 257 | const data = { 258 | id: id, 259 | }; 260 | const url = "/api/block/foldBlock"; 261 | return request(url, data); 262 | } 263 | 264 | export async function unfoldBlock(id: BlockId) { 265 | const data = { 266 | id: id, 267 | }; 268 | const url = "/api/block/unfoldBlock"; 269 | return request(url, data); 270 | } 271 | 272 | export async function getBlockKramdown( 273 | id: BlockId, 274 | ): Promise { 275 | const data = { 276 | id: id, 277 | }; 278 | const url = "/api/block/getBlockKramdown"; 279 | return request(url, data); 280 | } 281 | 282 | export async function getChildBlocks( 283 | id: BlockId, 284 | ): Promise { 285 | const data = { 286 | id: id, 287 | }; 288 | const url = "/api/block/getChildBlocks"; 289 | return request(url, data); 290 | } 291 | 292 | export async function transferBlockRef( 293 | fromID: BlockId, 294 | toID: BlockId, 295 | refIDs: BlockId[], 296 | ) { 297 | const data = { 298 | fromID: fromID, 299 | toID: toID, 300 | refIDs: refIDs, 301 | }; 302 | const url = "/api/block/transferBlockRef"; 303 | return request(url, data); 304 | } 305 | 306 | // **************************************** Attributes **************************************** 307 | export async function setBlockAttrs( 308 | id: BlockId, 309 | attrs: { [key: string]: string }, 310 | ) { 311 | const data = { 312 | id: id, 313 | attrs: attrs, 314 | }; 315 | const url = "/api/attr/setBlockAttrs"; 316 | return request(url, data); 317 | } 318 | 319 | export async function getAttributeViewKeys( 320 | id: BlockId, 321 | ): Promise { 322 | const data = { 323 | id: id, 324 | }; 325 | const url = "/api/av/getAttributeViewKeys"; 326 | return request(url, data); 327 | } 328 | 329 | export async function getBlockAttrs( 330 | id: BlockId, 331 | ): Promise<{ [key: string]: string }> { 332 | const data = { 333 | id: id, 334 | }; 335 | const url = "/api/attr/getBlockAttrs"; 336 | return request(url, data); 337 | } 338 | 339 | // **************************************** SQL **************************************** 340 | 341 | export async function sql(sql: string): Promise { 342 | const sqldata = { 343 | stmt: sql, 344 | }; 345 | const url = "/api/query/sql"; 346 | return request(url, sqldata); 347 | } 348 | 349 | export async function getBlockByID(blockId: string): Promise { 350 | const sqlScript = `select * from blocks where id ='${blockId}'`; 351 | const data = await sql(sqlScript); 352 | return data[0]; 353 | } 354 | 355 | // **************************************** Template **************************************** 356 | 357 | export async function render( 358 | id: DocumentId, 359 | path: string, 360 | ): Promise { 361 | const data = { 362 | id: id, 363 | path: path, 364 | }; 365 | const url = "/api/template/render"; 366 | return request(url, data); 367 | } 368 | 369 | export async function renderSprig(template: string): Promise { 370 | const url = "/api/template/renderSprig"; 371 | return request(url, { template: template }); 372 | } 373 | 374 | // **************************************** File **************************************** 375 | 376 | export async function getFile(path: string): Promise { 377 | const data = { 378 | path: path, 379 | }; 380 | const url = "/api/file/getFile"; 381 | return new Promise((resolve, _) => { 382 | fetchPost(url, data, (content: any) => { 383 | resolve(content); 384 | }); 385 | }); 386 | } 387 | 388 | /** 389 | * fetchPost will secretly convert data into json, this func merely return Blob 390 | * @param endpoint 391 | * @returns 392 | */ 393 | export const getFileBlob = async (path: string): Promise => { 394 | const endpoint = "/api/file/getFile"; 395 | const response = await fetch(endpoint, { 396 | method: "POST", 397 | body: JSON.stringify({ 398 | path: path, 399 | }), 400 | }); 401 | if (!response.ok) { 402 | return null; 403 | } 404 | const data = await response.blob(); 405 | return data; 406 | }; 407 | 408 | export async function putFile(path: string, isDir: boolean, file: any) { 409 | const form = new FormData(); 410 | form.append("path", path); 411 | form.append("isDir", isDir.toString()); 412 | // Copyright (c) 2023, terwer. 413 | // https://github.com/terwer/siyuan-plugin-importer/blob/v1.4.1/src/api/kernel-api.ts 414 | form.append("modTime", Math.floor(Date.now() / 1000).toString()); 415 | form.append("file", file); 416 | const url = "/api/file/putFile"; 417 | return request(url, form); 418 | } 419 | 420 | export async function removeFile(path: string) { 421 | const data = { 422 | path: path, 423 | }; 424 | const url = "/api/file/removeFile"; 425 | return request(url, data); 426 | } 427 | 428 | export async function readDir(path: string): Promise { 429 | const data = { 430 | path: path, 431 | }; 432 | const url = "/api/file/readDir"; 433 | return request(url, data); 434 | } 435 | 436 | // **************************************** Export **************************************** 437 | 438 | export async function exportMdContent( 439 | id: DocumentId, 440 | ): Promise { 441 | const data = { 442 | id: id, 443 | }; 444 | const url = "/api/export/exportMdContent"; 445 | return request(url, data); 446 | } 447 | 448 | export async function exportResources( 449 | paths: string[], 450 | name: string, 451 | ): Promise { 452 | const data = { 453 | paths: paths, 454 | name: name, 455 | }; 456 | const url = "/api/export/exportResources"; 457 | return request(url, data); 458 | } 459 | 460 | // **************************************** Convert **************************************** 461 | 462 | export type PandocArgs = string; 463 | export async function pandoc(args: PandocArgs[]) { 464 | const data = { 465 | args: args, 466 | }; 467 | const url = "/api/convert/pandoc"; 468 | return request(url, data); 469 | } 470 | 471 | // **************************************** Notification **************************************** 472 | 473 | // /api/notification/pushMsg 474 | // { 475 | // "msg": "test", 476 | // "timeout": 7000 477 | // } 478 | export async function pushMsg(msg: string, timeout: number = 7000) { 479 | const payload = { 480 | msg: msg, 481 | timeout: timeout, 482 | }; 483 | const url = "/api/notification/pushMsg"; 484 | return request(url, payload); 485 | } 486 | 487 | export async function pushErrMsg(msg: string, timeout: number = 7000) { 488 | const payload = { 489 | msg: msg, 490 | timeout: timeout, 491 | }; 492 | const url = "/api/notification/pushErrMsg"; 493 | return request(url, payload); 494 | } 495 | 496 | // **************************************** Network **************************************** 497 | export async function forwardProxy( 498 | url: string, 499 | method: string = "GET", 500 | payload: any = {}, 501 | headers: any[] = [], 502 | timeout: number = 7000, 503 | contentType: string = "text/html", 504 | ): Promise { 505 | const data = { 506 | url: url, 507 | method: method, 508 | timeout: timeout, 509 | contentType: contentType, 510 | headers: headers, 511 | payload: payload, 512 | }; 513 | const url1 = "/api/network/forwardProxy"; 514 | return request(url1, data); 515 | } 516 | 517 | // **************************************** System **************************************** 518 | 519 | export async function bootProgress(): Promise { 520 | return request("/api/system/bootProgress", {}); 521 | } 522 | 523 | export async function version(): Promise { 524 | return request("/api/system/version", {}); 525 | } 526 | 527 | export async function currentTime(): Promise { 528 | return request("/api/system/currentTime", {}); 529 | } 530 | -------------------------------------------------------------------------------- /src/components/AttributeViewPanel.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 | {#each avData as table} 41 |
42 | {#each filteredKeyValues(table.keyValues) as item} 43 |
49 | {#if enableDragAndDrop} 50 |
51 | 52 |
53 | {:else} 54 | 55 | {/if} 56 |
75 | 76 |
77 |
78 | {/each} 79 |
80 |
81 | {/each} 82 |
83 | 84 | 89 | -------------------------------------------------------------------------------- /src/components/AttributeViewPanelNative.svelte: -------------------------------------------------------------------------------- 1 | 123 | 124 |
125 | {#if tabs.length > 1} 126 | 131 | {/if} 132 |
133 |
134 | -------------------------------------------------------------------------------- /src/components/AttributeViewValue.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | {#if value.type === "block"} 25 |
{value.block.content}
26 | {:else if value.type === "text" && value?.text?.content} 27 | 33 | {:else if value.type === "number"} 34 | 43 | {value.number.formattedContent} 44 | {:else if value.type === "select" || value.type === "mSelect"} 45 | 46 | {:else if value.type === "mAsset"} 47 | 48 | {:else if value.type === "date"} 49 | 50 | {:else if value.type === "created" || value.type === "updated"} 51 | {dayjs(value[value.type].content).format("YYYY-MM-DD HH:mm")} 54 | {:else if value.type === "url"} 55 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | {:else if value.type === "phone"} 71 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | {:else if value.type === "checkbox"} 87 | 88 | {#if value.checkbox.checked} 89 | 90 | {:else} 91 | 92 | {/if} 93 | 94 | {:else if value.type === "template"} 95 | 96 | {:else if value.type === "email"} 97 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | {:else if value.type === "relation"} 113 | 114 | {:else if value.type === "rollup"} 115 | {#each value.rollup.contents as item} 116 | {#if ["select", "mSelect", "mAsset", "checkbox", "relation"].includes(item.type)} 117 | 118 | {:else} 119 | 120 | {/if} 121 | {#if item.id !== value.rollup.contents[value.rollup.contents.length - 1].id} 122 | ,  123 | {/if} 124 | {/each} 125 | {/if} 126 | -------------------------------------------------------------------------------- /src/components/ColumnIcon.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/svelte"; 2 | import ColumnIcon from "./ColumnIcon.svelte"; 3 | import { describe, it, expect } from "vitest"; 4 | import "@testing-library/jest-dom"; 5 | import { TAVCol } from "siyuan"; 6 | 7 | describe("ColumnIcon", () => { 8 | it("renders CustomIcon when key.icon is provided", () => { 9 | const key = { 10 | id: "test", 11 | name: "Test Column", 12 | icon: "1f600", 13 | type: "text" as TAVCol, 14 | }; 15 | 16 | const { container } = render(ColumnIcon, { 17 | props: { key }, 18 | }); 19 | 20 | const customIcon = container.querySelector(".block__logoicon"); 21 | expect(customIcon).toBeInTheDocument(); 22 | expect(customIcon.tagName).toBe("SPAN"); // CustomIcon renders a span when needSpan is true 23 | }); 24 | 25 | it("renders default icon when key.icon is not provided", () => { 26 | const key = { 27 | id: "test", 28 | name: "Test Column", 29 | icon: "", 30 | type: "text" as TAVCol, 31 | }; 32 | 33 | const { container } = render(ColumnIcon, { 34 | props: { key }, 35 | }); 36 | 37 | const svgIcon = container.querySelector("svg.block__logoicon"); 38 | expect(svgIcon).toBeInTheDocument(); 39 | }); 40 | 41 | it("renders the column name", () => { 42 | const key = { 43 | id: "test", 44 | name: "Test Column", 45 | icon: "", 46 | type: "text" as TAVCol, 47 | }; 48 | 49 | const { getByText } = render(ColumnIcon, { 50 | props: { key }, 51 | }); 52 | 53 | const columnName = getByText("Test Column"); 54 | expect(columnName).toBeInTheDocument(); 55 | }); 56 | 57 | it("sets the correct aria-label", () => { 58 | const key = { 59 | id: "test", 60 | name: "Test Column", 61 | icon: "", 62 | type: "text" as TAVCol, 63 | }; 64 | 65 | const { container } = render(ColumnIcon, { 66 | props: { key }, 67 | }); 68 | 69 | const div = container.querySelector(".block__logo"); 70 | expect(div).toHaveAttribute("aria-label", "Test Column"); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/components/ColumnIcon.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /src/components/CustomIcon.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/svelte"; 2 | import CustomIcon from "./CustomIcon.svelte"; 3 | import { describe, it, expect } from "vitest"; 4 | 5 | describe("CustomIcon", () => { 6 | it("renders an image when unicode contains a dot", () => { 7 | const { container } = render(CustomIcon, { 8 | props: { 9 | unicode: "emoji.svg", 10 | className: "test-class", 11 | needSpan: false, 12 | lazy: false, 13 | }, 14 | }); 15 | 16 | const img = container.querySelector("img"); 17 | expect(img).toBeTruthy(); 18 | expect(img?.className).toBe("test-class"); 19 | expect(img?.getAttribute("src")).toBe("/emojis/emoji.svg"); 20 | expect(img?.getAttribute("alt")).toBe("Emoji"); 21 | }); 22 | 23 | it("renders a span with emoji when needSpan is true", () => { 24 | const { container } = render(CustomIcon, { 25 | props: { 26 | unicode: "1f600", 27 | className: "test-class", 28 | needSpan: true, 29 | lazy: false, 30 | }, 31 | }); 32 | 33 | // Wait for next tick to allow derived state to update 34 | return new Promise((resolve) => setTimeout(resolve, 0)).then(() => { 35 | const span = container.querySelector("span"); 36 | expect(span).toBeTruthy(); 37 | expect(span?.className).toBe("test-class"); 38 | expect(span?.textContent).toBe("😀"); 39 | }); 40 | }); 41 | 42 | it("renders emoji without span when needSpan is false", async () => { 43 | const { container } = render(CustomIcon, { 44 | props: { 45 | unicode: "1f600", 46 | className: "test-class", 47 | needSpan: false, 48 | lazy: false, 49 | }, 50 | }); 51 | 52 | // Wait for next tick to allow derived state to update 53 | await new Promise((resolve) => setTimeout(resolve, 0)); 54 | const textContent = container.textContent?.trim(); 55 | expect(textContent).toBe("😀"); 56 | }); 57 | 58 | it("renders lazy-loaded image when lazy is true", () => { 59 | const { container } = render(CustomIcon, { 60 | props: { 61 | unicode: "emoji.svg", 62 | className: "test-class", 63 | needSpan: false, 64 | lazy: true, 65 | }, 66 | }); 67 | 68 | const img = container.querySelector("img"); 69 | expect(img).toBeTruthy(); 70 | expect(img?.className).toBe("test-class"); 71 | expect(img?.getAttribute("data-src")).toBe("/emojis/emoji.svg"); 72 | expect(img?.getAttribute("src")).toBeNull(); 73 | }); 74 | 75 | it("handles compound unicode emojis", async () => { 76 | const { container } = render(CustomIcon, { 77 | props: { 78 | unicode: "1f468-1f3fb-200d-1f4bb", 79 | className: "test-class", 80 | needSpan: true, 81 | }, 82 | }); 83 | 84 | // Wait for next tick to allow derived state to update 85 | await new Promise((resolve) => setTimeout(resolve, 0)); 86 | const span = container.querySelector("span"); 87 | expect(span?.textContent).toBe("👨🏻‍💻"); 88 | }); 89 | 90 | it("handles empty unicode gracefully", async () => { 91 | const { container } = render(CustomIcon, { 92 | props: { 93 | unicode: "", 94 | className: "test-class", 95 | needSpan: true, 96 | }, 97 | }); 98 | 99 | // Wait for next tick to allow derived state to update 100 | await new Promise((resolve) => setTimeout(resolve, 0)); 101 | const span = container.querySelector("span"); 102 | expect(span?.textContent).toBe(""); 103 | }); 104 | 105 | it("handles invalid unicode gracefully", async () => { 106 | const { container } = render(CustomIcon, { 107 | props: { 108 | unicode: "invalid", 109 | className: "test-class", 110 | needSpan: true, 111 | }, 112 | }); 113 | 114 | // Wait for next tick to allow derived state to update 115 | await new Promise((resolve) => setTimeout(resolve, 0)); 116 | const span = container.querySelector("span"); 117 | expect(span?.textContent).toBe(""); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/components/CustomIcon.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | {#if unicode.indexOf(".") > -1} 37 | Emoji 43 | {:else if needSpan} 44 | {emoji()} 45 | {:else} 46 | {emoji()} 47 | {/if} 48 | -------------------------------------------------------------------------------- /src/components/ProtyleBreadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | {#if isCollapsed || singleTab} 21 |
24 | 25 | {#if isCollapsed} 26 |
27 | {@render children?.()} 28 |
29 | {/if} 30 |
31 | {/if} 32 | 33 | -------------------------------------------------------------------------------- /src/components/ShowEmptyAttributesToggle.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 46 | -------------------------------------------------------------------------------- /src/components/ValueTypes/AttributeViewRollup.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#if value.type === 'block'} 13 | {#if value.isDetached} 14 | 15 | {value.block?.content || window.siyuan.languages.untitled} 16 | 17 | {:else} 18 | 22 | {value.block?.content || window.siyuan.languages.untitled} 23 | 24 | {/if} 25 | {:else if value.type === 'text'} 26 | {value.text.content} 27 | {:else if value.type === 'number'} 28 | {value.number.formattedContent || value.number.content.toString()} 29 | {:else if value.type === 'date'} 30 | {#if value.date && value.date.isNotEmpty} 31 | 32 | {dayjs(value.date.content).format(value.date.isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm")} 33 | {#if value.date.hasEndDate && value.date.isNotEmpty && value.date.isNotEmpty2} 34 | 35 | {dayjs(value.date.content2).format(value.date.isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm")} 36 | {/if} 37 | 38 | {/if} 39 | {:else if value.type === 'url'} 40 | {#if value.url.content} 41 | {value.url.content} 42 | {/if} 43 | {:else if value.type === 'phone'} 44 | {#if value.phone.content} 45 | {value.phone.content} 46 | {/if} 47 | {:else if value.type === 'email'} 48 | {#if value.email.content} 49 | {value.email.content} 50 | {/if} 51 | {/if} -------------------------------------------------------------------------------- /src/components/ValueTypes/DateValue.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | {#if value[value.type]} 16 | {#if value[value.type].isNotEmpty} 17 | {content} 18 | {#if value[value.type].hasEndDate && value[value.type].isNotEmpty2} 19 | 20 | {dayjs(value[value.type].content2).format(value[value.type].isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm")} 21 | {/if} 22 | {/if} 23 | 24 | {/if} 25 | -------------------------------------------------------------------------------- /src/components/ValueTypes/MAssetValue.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if value.mAsset} 12 | {#each value.mAsset as item} 13 | {#if item.type === "image"} 14 | 20 | {:else} 21 | 27 | {item.name || item.content} 28 | 29 | {/if} 30 | {/each} 31 | {/if} 32 | -------------------------------------------------------------------------------- /src/components/ValueTypes/MultiSelectValue.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if value.mSelect && value.mSelect.length > 0} 12 | {#each value.mSelect as item} 13 | 17 | {item.content} 18 | 19 | {/each} 20 | {/if} 21 | -------------------------------------------------------------------------------- /src/components/ValueTypes/RelationValue.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if value?.relation?.contents} 14 | {#each value.relation.contents as item} 15 | {#if item} 16 | 17 | {#if item.id !== lastId} 18 | ,  19 | {/if} 20 | {/if} 21 | {/each} 22 | {/if} 23 | -------------------------------------------------------------------------------- /src/components/ValueTypes/TemplateValue.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
{@html value.template.content}
12 | -------------------------------------------------------------------------------- /src/components/ValueTypes/TextValue.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/components/ui/Button.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/ui/Button.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { render, screen, fireEvent } from "@testing-library/svelte"; 3 | import Button from "./Button.svelte"; 4 | 5 | describe("Button", () => { 6 | it("renders with label", () => { 7 | render(Button, { props: { label: "Test Button" } }); 8 | expect(screen.getByText("Test Button")).toBeTruthy(); 9 | }); 10 | 11 | it("renders with icon", () => { 12 | render(Button, { props: { icon: "iconName" } }); 13 | // Check if Icon component is rendered 14 | expect(document.querySelector(".block__icon")).toBeTruthy(); 15 | }); 16 | 17 | it("renders with tooltip", () => { 18 | render(Button, { props: { tooltip: "Test Tooltip" } }); 19 | const button = screen.getByRole("button"); 20 | expect(button).toHaveClass("b3-tooltips"); 21 | expect(button).toHaveClass("b3-tooltips__w"); 22 | expect(button).toHaveAttribute("aria-label", "Test Tooltip"); 23 | }); 24 | 25 | it("applies focus class when isFocused is true", () => { 26 | render(Button, { props: { isFocused: true } }); 27 | const button = screen.getByRole("button"); 28 | expect(button).toHaveClass("item--focus"); 29 | }); 30 | 31 | it("calls onclick handler when clicked", async () => { 32 | const handleClick = vi.fn(); 33 | render(Button, { props: { onclick: handleClick } }); 34 | 35 | const button = screen.getByRole("button"); 36 | await fireEvent.click(button); 37 | 38 | expect(handleClick).toHaveBeenCalled(); 39 | }); 40 | 41 | it("calls onclick handler on keydown", async () => { 42 | const handleClick = vi.fn(); 43 | render(Button, { props: { onclick: handleClick } }); 44 | 45 | const button = screen.getByRole("button"); 46 | await fireEvent.keyDown(button); 47 | 48 | expect(handleClick).toHaveBeenCalled(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/ui/CollapseButton.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |