mirror of
https://github.com/logseq/logseq.git
synced 2026-05-15 16:32:21 +00:00
Merge pull request #12511 from logseq/enhance/i18n
Improve i18n key naming, tooling, and translation coverage
This commit is contained in:
167
.agents/skills/logseq-i18n/SKILL.md
Normal file
167
.agents/skills/logseq-i18n/SKILL.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: logseq-i18n
|
||||
description: "Logseq i18n workflow for adding, renaming, reviewing, or editing translation keys and user-facing strings. Use when: writing UI code with hardcoded text, adding new user-facing strings, editing translation dict files, reviewing i18n compliance, working with notification/show!, adding translatable UI attributes, or any task involving src/resources/dicts/. Also use when the user mentions i18n, translation, localization, or hardcoded strings."
|
||||
---
|
||||
|
||||
# Logseq i18n Skill
|
||||
|
||||
## When This Skill Applies
|
||||
|
||||
- Adding or editing user-facing strings in shipped UI
|
||||
- Replacing hardcoded UI text with translations
|
||||
- Adding, renaming, deduplicating, or removing keys in `src/resources/dicts/`
|
||||
- Reviewing code for i18n compliance
|
||||
- Editing `notification/show!` calls or translatable UI attributes
|
||||
- Updating i18n tooling, docs, or lint configuration
|
||||
|
||||
## Read These First
|
||||
|
||||
1. `docs/i18n-key-naming.md` for key ownership, reuse, and naming
|
||||
2. `.i18n-lint.toml` for lint scope, covered helpers/attributes, exclusions,
|
||||
and allowlists
|
||||
3. `src/main/frontend/context/i18n.cljs` for the translation helper APIs
|
||||
|
||||
Use `docs/contributing-to-translations.md` only when the task is specifically
|
||||
about locale contribution workflow.
|
||||
|
||||
## Scope Rules
|
||||
|
||||
- `.i18n-lint.toml` is the source of truth for which files and APIs are checked
|
||||
for hardcoded UI text.
|
||||
- Inside that scope, all shipped user-facing UI text must be internationalized.
|
||||
- Console output does not need i18n. Keep out-of-scope developer-only `(Dev)`
|
||||
labels inline in code/config, not in translation dictionaries.
|
||||
- If you introduce a new UI helper, alert API, translatable attribute, UI
|
||||
namespace, or shipped surface, update `.i18n-lint.toml` so lint coverage
|
||||
stays accurate.
|
||||
|
||||
## Use These Helpers
|
||||
|
||||
All translation helpers live in `frontend.context.i18n`.
|
||||
|
||||
| Helper | Use for |
|
||||
|---|---|
|
||||
| `t` | Standard translation with preferred locale |
|
||||
| `tt` | Try multiple keys and return the first existing translation |
|
||||
| `t-en` | Force English text when UI output also needs English console/debug output |
|
||||
| `interpolate-rich-text` / `interpolate-rich-text-node` | Replace placeholders with rich-text or hiccup fragments |
|
||||
| `interpolate-sentence` | Keep a full sentence in one key while inserting placeholders and inline links |
|
||||
| `replace-newlines-with-br` | Render translated newline characters as `[:br]` nodes |
|
||||
| `locale-join-rich-text` / `locale-join-rich-text-node` | Join rich fragments with locale-aware separators |
|
||||
| `locale-format-number` / `locale-format-date` / `locale-format-time` | Locale-aware formatting for dynamic values before translation |
|
||||
|
||||
Do not introduce parallel i18n helpers elsewhere unless the change also updates
|
||||
the shared i18n API deliberately.
|
||||
|
||||
## Core Rules
|
||||
|
||||
### Rule 1: No hardcoded shipped UI text
|
||||
|
||||
If the text is user-facing and in `.i18n-lint.toml` scope, hardcoded literals in
|
||||
buttons, labels, placeholders, tooltips, dialogs, notifications, empty states,
|
||||
and similar UI are a bug.
|
||||
|
||||
### Rule 2: Reuse keys by meaning, not by English text
|
||||
|
||||
Search `src/resources/dicts/en.edn` first. Reuse a key only when both match:
|
||||
|
||||
- semantic owner
|
||||
- textual role
|
||||
|
||||
If the English text matches but the meaning differs, create a new key and follow
|
||||
`docs/i18n-key-naming.md`.
|
||||
|
||||
### Rule 3: English source lives in `en.edn`
|
||||
|
||||
- Add new English source text to `src/resources/dicts/en.edn`.
|
||||
- Add non-English entries only when you are also providing actual translations.
|
||||
- When renaming or removing keys, update affected locale files so stale keys do
|
||||
not remain behind.
|
||||
- Do not copy English into non-English locale files just to fill gaps. Tongue
|
||||
falls back to `:en`.
|
||||
|
||||
### Rule 4: Keep complete sentences together
|
||||
|
||||
- Prefer one translation entry per complete sentence or message.
|
||||
- Do not split rich text or linked text across multiple keys.
|
||||
- Use `interpolate-sentence` or `interpolate-rich-text*` when markup and word
|
||||
order must stay together.
|
||||
|
||||
### Rule 5: Prefer placeholders for plain dynamic text
|
||||
|
||||
Use placeholder strings like `{1}` and `{2}` for plain dynamic text. Format
|
||||
arguments in the caller before passing them to `t`.
|
||||
|
||||
### Rule 6: Function-valued translations are restricted
|
||||
|
||||
Use function values only when:
|
||||
|
||||
- the locale needs real logic such as conditional/plural behavior, or
|
||||
- the translation must return hiccup rich text
|
||||
|
||||
When function values are necessary, only these are allowed inside the function
|
||||
body:
|
||||
|
||||
- `str`
|
||||
- `when`
|
||||
- `if`
|
||||
- `=`
|
||||
|
||||
### Rule 7: Locale details matter
|
||||
|
||||
- Preserve emoji and icon glyphs from `en.edn` exactly.
|
||||
- Use punctuation natural to the locale.
|
||||
- Pluralization is locale-specific. Do not force English singular/plural logic
|
||||
onto every language.
|
||||
|
||||
## Workflow
|
||||
|
||||
When adding or changing user-facing text:
|
||||
|
||||
1. Use `.i18n-lint.toml` to confirm the text is in i18n scope.
|
||||
2. Search `src/resources/dicts/en.edn` for an exact semantic match.
|
||||
3. If no exact match exists, name the key with `docs/i18n-key-naming.md`.
|
||||
4. If the naming guide still does not yield one clear key, stop and ask for
|
||||
human guidance instead of guessing.
|
||||
5. Add or update the English source text in `en.edn`.
|
||||
6. Replace the literal with the appropriate helper from
|
||||
`frontend.context.i18n`.
|
||||
7. Add/update locale translations only where actual translations are being
|
||||
supplied.
|
||||
8. If you introduced a new linted helper/attribute/surface, update
|
||||
`.i18n-lint.toml`.
|
||||
|
||||
## Validation
|
||||
|
||||
After changing keys:
|
||||
|
||||
```bash
|
||||
bb lang:validate-translations
|
||||
```
|
||||
|
||||
After changing shipped UI text:
|
||||
|
||||
```bash
|
||||
bb lang:lint-hardcoded --git-changed
|
||||
```
|
||||
|
||||
After editing dictionary files:
|
||||
|
||||
```bash
|
||||
bb lang:format-dicts
|
||||
```
|
||||
|
||||
`bb lang:format-dicts` is the canonical repo formatter for dictionary key
|
||||
ordering and namespace spacing.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---|---|
|
||||
| Hardcoded UI string in a linted UI surface | Move it into `en.edn` and use a helper from `frontend.context.i18n` |
|
||||
| Reusing a key only because the English text matches | Reuse only on exact semantic owner + role match |
|
||||
| Copying English into non-English locale files | Leave the key missing unless you are adding a real translation |
|
||||
| Using `(fn ...)` for plain placeholder text | Use `"..."` with `{1}`, `{2}`, ... |
|
||||
| Splitting one sentence across multiple keys | Keep a single translation entry and interpolate into it |
|
||||
| Adding a new linted helper but not updating `.i18n-lint.toml` | Extend the TOML config in the same change |
|
||||
| Editing dict files without running `bb lang:format-dicts` | Run the formatter before finishing |
|
||||
2
.github/workflows/build-desktop-release.yml
vendored
2
.github/workflows/build-desktop-release.yml
vendored
@@ -666,7 +666,7 @@ jobs:
|
||||
./*.apk
|
||||
|
||||
release:
|
||||
# NOTE: For now, we only have beta channel to be released on Github
|
||||
# NOTE: For now, we only have beta channel to be released on GitHub
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build-target == 'beta' }}
|
||||
needs: [ build-macos-x64, build-macos-arm64, build-linux-x64, build-linux-arm64, build-windows-x64, build-windows-arm64 ]
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -118,6 +118,9 @@ jobs:
|
||||
- name: Lint invalid translation entries
|
||||
run: bb lang:validate-translations
|
||||
|
||||
- name: Lint hardcoded user-facing strings
|
||||
run: bb lang:lint-hardcoded
|
||||
|
||||
- name: Lint to keep worker independent of frontend
|
||||
run: bb lint:worker-and-frontend-separate
|
||||
|
||||
@@ -171,4 +174,4 @@ jobs:
|
||||
run: cd deps/db && yarn nbb-logseq -cp src:../cli/src -m logseq.cli validate -g ../../scripts/properties-graph ../../scripts/schema-graph
|
||||
|
||||
- name: Export a created DB graph and confirm the export is idempotent
|
||||
run: cd deps/db && yarn nbb-logseq -cp src:../cli/src -m logseq.cli export-edn -g ../../scripts/properties-graph -f properties.edn --roundtrip
|
||||
run: cd deps/db && yarn nbb-logseq -cp src:../cli/src -m logseq.cli export-edn -g ../../scripts/properties-graph -f properties.edn --roundtrip
|
||||
|
||||
231
.github/workflows/update-i18n-lint.yml
vendored
Normal file
231
.github/workflows/update-i18n-lint.yml
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
name: Update logseq-i18n-lint binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
LINT_REPO: ${{ github.repository_owner }}/logseq-i18n-lint
|
||||
LINT_REF: master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout logseq-i18n-lint source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.LINT_REPO }}
|
||||
ref: ${{ env.LINT_REF }}
|
||||
path: lint-src
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache cargo registry and build artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
lint-src/target/
|
||||
key: test-cargo-${{ hashFiles('lint-src/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
test-cargo-
|
||||
|
||||
- name: Run tests
|
||||
working-directory: lint-src
|
||||
run: cargo test --locked
|
||||
|
||||
build:
|
||||
needs: test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
artifact: logseq-i18n-lint-x86_64-windows.exe
|
||||
- target: aarch64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
artifact: logseq-i18n-lint-aarch64-windows.exe
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
artifact: logseq-i18n-lint-x86_64-macos
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
artifact: logseq-i18n-lint-aarch64-macos
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
artifact: logseq-i18n-lint-x86_64-linux
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
artifact: logseq-i18n-lint-aarch64-linux
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout logseq-i18n-lint source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.LINT_REPO }}
|
||||
ref: ${{ env.LINT_REF }}
|
||||
path: lint-src
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Cache cargo registry and build artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
lint-src/target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('lint-src/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.target }}-cargo-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Install cross (Linux ARM64)
|
||||
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Install musl tools (Linux x64)
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
||||
|
||||
- name: Build with cross
|
||||
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||
working-directory: lint-src
|
||||
run: cross build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Build with cargo
|
||||
if: matrix.target != 'aarch64-unknown-linux-musl'
|
||||
working-directory: lint-src
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Stage artifact (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: cp lint-src/target/${{ matrix.target }}/release/logseq-i18n-lint ${{ matrix.artifact }}
|
||||
|
||||
- name: Stage artifact (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: cp lint-src/target/${{ matrix.target }}/release/logseq-i18n-lint.exe ${{ matrix.artifact }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: ${{ matrix.artifact }}
|
||||
|
||||
open-pr:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout this repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout logseq-i18n-lint source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.LINT_REPO }}
|
||||
ref: ${{ env.LINT_REF }}
|
||||
fetch-depth: 0
|
||||
path: lint-src
|
||||
|
||||
- name: Download built artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Resolve source commit and stage launcher
|
||||
id: source
|
||||
run: |
|
||||
cd lint-src
|
||||
COMMIT_SHA="$(git rev-parse HEAD)"
|
||||
SHORT_SHA="$(git rev-parse --short=12 HEAD)"
|
||||
echo "commit_sha=${COMMIT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
cd ..
|
||||
cp lint-src/scripts/logseq-i18n-lint artifacts/logseq-i18n-lint
|
||||
|
||||
- name: Replace bin artifacts
|
||||
run: |
|
||||
cp artifacts/logseq-i18n-lint bin/logseq-i18n-lint
|
||||
cp artifacts/logseq-i18n-lint-aarch64-linux bin/logseq-i18n-lint-aarch64-linux
|
||||
cp artifacts/logseq-i18n-lint-aarch64-macos bin/logseq-i18n-lint-aarch64-macos
|
||||
cp artifacts/logseq-i18n-lint-aarch64-windows.exe bin/logseq-i18n-lint-aarch64-windows.exe
|
||||
cp artifacts/logseq-i18n-lint-x86_64-linux bin/logseq-i18n-lint-x86_64-linux
|
||||
cp artifacts/logseq-i18n-lint-x86_64-macos bin/logseq-i18n-lint-x86_64-macos
|
||||
cp artifacts/logseq-i18n-lint-x86_64-windows.exe bin/logseq-i18n-lint-x86_64-windows.exe
|
||||
chmod +x bin/logseq-i18n-lint bin/logseq-i18n-lint-*-linux bin/logseq-i18n-lint-*-macos 2>/dev/null || true
|
||||
|
||||
- name: Commit updated binaries
|
||||
id: commit
|
||||
env:
|
||||
SHORT_SHA: ${{ steps.source.outputs.short_sha }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
BRANCH="chore/update-i18n-lint-${SHORT_SHA}"
|
||||
git switch -C "${BRANCH}"
|
||||
git add -A -- \
|
||||
bin/logseq-i18n-lint \
|
||||
bin/logseq-i18n-lint-aarch64-linux \
|
||||
bin/logseq-i18n-lint-aarch64-macos \
|
||||
bin/logseq-i18n-lint-aarch64-windows.exe \
|
||||
bin/logseq-i18n-lint-x86_64-linux \
|
||||
bin/logseq-i18n-lint-x86_64-macos \
|
||||
bin/logseq-i18n-lint-x86_64-windows.exe
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore: update logseq-i18n-lint binaries to ${SHORT_SHA}"
|
||||
git push origin "${BRANCH}"
|
||||
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Open pull request
|
||||
if: steps.commit.outputs.branch != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
COMMIT_SHA: ${{ steps.source.outputs.commit_sha }}
|
||||
SHORT_SHA: ${{ steps.source.outputs.short_sha }}
|
||||
BRANCH: ${{ steps.commit.outputs.branch }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base develop \
|
||||
--head "${BRANCH}" \
|
||||
--title "chore: update logseq-i18n-lint binaries to ${SHORT_SHA}" \
|
||||
--body "$(cat <<EOF
|
||||
## Update logseq-i18n-lint binaries
|
||||
|
||||
This PR was created automatically by the [update-i18n-lint](.github/workflows/update-i18n-lint.yml) workflow.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Source repo | [${LINT_REPO}](https://github.com/${LINT_REPO}) |
|
||||
| Source ref | \`${LINT_REF}\` |
|
||||
| Source commit | \`${COMMIT_SHA}\` |
|
||||
| Trigger | Manual (\`workflow_dispatch\`) |
|
||||
|
||||
### Changed files
|
||||
This PR rebuilds and replaces exactly 7 files in \`bin/\` from the latest \`${LINT_REF}\` source of \`${LINT_REPO}\`.
|
||||
EOF
|
||||
)"
|
||||
371
.i18n-lint.toml
Normal file
371
.i18n-lint.toml
Normal file
@@ -0,0 +1,371 @@
|
||||
##########################################################################################
|
||||
# CAUTION: Do not modify this file without a clear understanding of its logic. #
|
||||
# Check [https://github.com/logseq/logseq-i18n-lint] before proceeding with any changes. #
|
||||
##########################################################################################
|
||||
|
||||
# Logseq-specific configuration for logseq-i18n-lint.
|
||||
|
||||
# ── Shared settings ────────────────────────────────────────────────────────────
|
||||
|
||||
# Path from the executable's directory to the Logseq repo root.
|
||||
# Behaviour is independent of the working directory.
|
||||
project_root = ".."
|
||||
|
||||
# Directories to scan (relative to project_root).
|
||||
include_dirs = [
|
||||
"src/main/frontend",
|
||||
"src/main/electron",
|
||||
"src/main/mobile",
|
||||
"src/electron",
|
||||
"deps",
|
||||
]
|
||||
|
||||
# File extensions to scan.
|
||||
file_extensions = ["clj", "cljs", "cljc"]
|
||||
|
||||
# Translation functions — calls to these provide translation keys for both subcommands.
|
||||
i18n_functions = [
|
||||
"t",
|
||||
"tt",
|
||||
"i18n/t",
|
||||
"i18n/tt",
|
||||
]
|
||||
|
||||
# Alert/notification functions.
|
||||
# lint: the FIRST argument is user-visible text; analyzed in UI context so
|
||||
# str-concat, conditional-text, and format-string rules apply inside it.
|
||||
# check-keys: the FIRST keyword argument is a translation key reference.
|
||||
alert_functions = [
|
||||
"notification/show!",
|
||||
]
|
||||
|
||||
# UI component functions.
|
||||
# lint: string arguments are user-visible text.
|
||||
# check-keys: keyword arguments are translation key references.
|
||||
ui_functions = [
|
||||
"ui/button",
|
||||
"ui/tooltip",
|
||||
"ui/tooltip-content",
|
||||
"ui/badge",
|
||||
"ui/dropdown-menu-item",
|
||||
"ui/dropdown-menu-sub-trigger",
|
||||
"ui/loading",
|
||||
"ui/select-item",
|
||||
"ui/tabs-trigger",
|
||||
"ui/form-label",
|
||||
"ui/form-description",
|
||||
"ui/card-title",
|
||||
"ui/alert-title",
|
||||
"ui/alert-description",
|
||||
"ui/table-cell",
|
||||
"ui/table-header",
|
||||
"ui/link",
|
||||
]
|
||||
|
||||
# Namespace prefixes where every function is treated as a UI component.
|
||||
ui_namespaces = [
|
||||
"shui",
|
||||
]
|
||||
|
||||
# HTML/hiccup attributes.
|
||||
# lint: string values are flagged as user-visible text.
|
||||
# check-keys: keyword values are treated as translation key references.
|
||||
ui_attributes = [
|
||||
"placeholder",
|
||||
"title",
|
||||
"aria-label",
|
||||
"alt",
|
||||
"label",
|
||||
]
|
||||
|
||||
# ── [lint] settings ────────────────────────────────────────────────────────────
|
||||
|
||||
[lint]
|
||||
|
||||
# Glob patterns for files to skip during lint.
|
||||
# These patterns are also applied when using --git-changed.
|
||||
exclude_patterns = [
|
||||
"**/test/**",
|
||||
"**/node_modules/**",
|
||||
"**/static/**",
|
||||
"**/target/**",
|
||||
"**/tmp/**",
|
||||
"**/cljs-test-runner-out/**",
|
||||
"**/.nbb/**",
|
||||
"deps/cli/**",
|
||||
"deps/publish/**",
|
||||
"deps/publishing/**",
|
||||
"deps/db-sync/src/logseq/db_sync/malli_schema.cljs", # Malli protocol schema — wire-protocol message type names
|
||||
"deps/graph-parser/src/logseq/graph_parser/schema/mldoc.cljc", # mldoc schema — AST node type names (Label, Paragraph, etc.)
|
||||
"deps/shui/src/logseq/shui/demo*.cljs", # storybook demo UI — intentionally hardcoded
|
||||
"src/main/frontend/components/profiler.cljs", # Developer profiling tool
|
||||
"src/main/frontend/db/rtc/debug_ui.cljs", # Developer RTC tool
|
||||
"src/main/frontend/handler/export/html.cljs", # Raw HTML export — intentional
|
||||
"src/main/frontend/handler/shell.cljs", # Run shell command
|
||||
"src/main/frontend/undo_redo/debug_ui.cljs", # Developer undo/redo tool
|
||||
"src/main/frontend/worker/commands.cljs", # Internal command identifier strings
|
||||
]
|
||||
|
||||
# Maximum character length of the text preview in output.
|
||||
text_preview_length = 60
|
||||
|
||||
# Pure (non-UI) functions — string arguments inside are not reported even in UI context.
|
||||
pure_functions = [
|
||||
# String utilities whose arguments are data, not UI text.
|
||||
"text-util/cut-by",
|
||||
"text-util/split-by",
|
||||
# mldoc/markdown dispatch functions — args are AST node type names, not UI text.
|
||||
"markup-element-cp",
|
||||
"markup-elements-cp",
|
||||
# mldoc inline renderer — first arg is config, second is an AST node vector.
|
||||
"inline",
|
||||
# Rum component key wrapper — second arg is a key string, not UI text.
|
||||
"rum/with-key",
|
||||
# Shortcut wrapper — positional args are shortcut IDs and positions, not UI text.
|
||||
"ui/with-shortcut",
|
||||
# Macro type identifier dispatch.
|
||||
"macro->text",
|
||||
]
|
||||
|
||||
# Format/printf functions — ONLY the FIRST argument (the template string) is flagged,
|
||||
# and ONLY when the call site is inside a UI context (hiccup or UI function call).
|
||||
format_functions = [
|
||||
"format",
|
||||
"goog.string/format",
|
||||
"gstring/format",
|
||||
"util/format",
|
||||
]
|
||||
|
||||
# Strings to allow (exact match — also matches after trimming whitespace).
|
||||
# Keep this list SHORT. Add only strings that:
|
||||
# 1. Are NOT covered by any allow_pattern
|
||||
# 2. Have a clear, Logseq-specific reason for appearing in ui context
|
||||
# 3. Are truly non-translatable (brand names, internal IDs, technical constants)
|
||||
allow_strings = [
|
||||
# Brand name — displayed literally in UI, intentionally not translated.
|
||||
"Logseq",
|
||||
"Logseq Sync",
|
||||
"GitHub",
|
||||
# Typography test string — rendered as a glyph sample, not translatable.
|
||||
"Ag",
|
||||
# Column header abbreviation for row index — shown as-is in table view.
|
||||
"ID:",
|
||||
# org-mode structural keywords — shown literally in drawer/block syntax.
|
||||
":END:",
|
||||
# Common non-translatable UI labels.
|
||||
"URL",
|
||||
"OPML",
|
||||
"EDN",
|
||||
"HTML",
|
||||
"PNG",
|
||||
"SQLite",
|
||||
"HTTP",
|
||||
"SOCKS5",
|
||||
# Config directory path shown literally.
|
||||
"~/.logseq",
|
||||
]
|
||||
|
||||
# Regex patterns to allow.
|
||||
allow_patterns = [
|
||||
# Developer-only English labels intentionally outside i18n scope.
|
||||
# e.g., "(Dev) RTC", "(Dev) Profiler"
|
||||
"^\\(Dev\\)\\s",
|
||||
|
||||
# Logseq macro syntax.
|
||||
# e.g., {{query ...}}, {{video ...}}
|
||||
"^\\{\\{",
|
||||
|
||||
# Email addresses.
|
||||
# e.g., user@example.com, tech.support@domain.org
|
||||
"^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$",
|
||||
|
||||
# URLs and URI schemes.
|
||||
# e.g., https://google.com, sfsymbols://icon, file:///path/to/res
|
||||
"^[a-z]+://[^\\s]*$",
|
||||
|
||||
# Git commands.
|
||||
# e.g., git commit -m "feat", git push origin main
|
||||
"^git\\s+[a-z]+(\\s+.*)?$",
|
||||
|
||||
# Tailwind CSS color / shade utility classes.
|
||||
# e.g., bg-red-500, text-gray-300, border-blue-100
|
||||
"^(bg|text|border|ring|shadow|fill|from|via|to|outline|divide|accent|caret|decoration)-[a-z]+-[0-9]+(/[0-9]+)?$",
|
||||
|
||||
# CSS color functions.
|
||||
# e.g., rgb(255, 255, 255), rgba(0, 0, 0, 0.5)
|
||||
"^rgba?\\(",
|
||||
|
||||
# CSS custom property access.
|
||||
# e.g., var(--primary-color), var(--spacing-unit)
|
||||
"^var\\(--",
|
||||
|
||||
# CSS BEM modifier classes (double-hyphen notation).
|
||||
# e.g., shortcut-feedback--error, block__title--active
|
||||
"^[a-z][a-z0-9-]*--[a-z][a-z0-9-]*$",
|
||||
|
||||
# Numeric base prefixes.
|
||||
# e.g., 0b (binary), 0o (octal), 0x (hex)
|
||||
"^0[box]$",
|
||||
|
||||
# Regex anchor notation.
|
||||
# e.g., ^starting-with
|
||||
"^\\^",
|
||||
|
||||
# Web resource references with specific extensions.
|
||||
# e.g., script.js, styles.css, module.mjs
|
||||
"^[a-z][a-z0-9/._-]+\\.(mjs|js|css|wasm)$",
|
||||
|
||||
# MIME types.
|
||||
# e.g., image/png, application/json, text/html
|
||||
"^[a-z][a-z0-9+.-]+/[a-z0-9.+*-]+$",
|
||||
|
||||
# Hex colors (3, 4, 6, or 8 digits).
|
||||
# e.g., #fff, #1a2b3c, #ff00ffaa
|
||||
"^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
|
||||
|
||||
# DOM element IDs.
|
||||
# e.g., #main-container, #submit-btn
|
||||
"^#[a-z][a-z0-9-]+$",
|
||||
|
||||
# Dot-notation identifiers (icon library names, SF Symbols, CSS dot-joined classes).
|
||||
# e.g., person.fill, cloud.sun.rain.fill, bg-red-600.top-1.absolute
|
||||
"^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)*$",
|
||||
|
||||
# CSS unit values (supports decimals).
|
||||
# e.g., 10px, 1.5rem, 100%, 500ms
|
||||
"^[0-9]+(\\.[0-9]+)?(px|em|rem|vh|vw|%|pt|s|ms)$",
|
||||
|
||||
# DOM element ID / React key fragments (start or end with hyphen).
|
||||
# e.g., tag-, -refs, sidebar-block-, -custom-query-, -add-property
|
||||
"^-[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"^[a-z0-9]+(-[a-z0-9]+)*-$",
|
||||
|
||||
# Strings starting with a dot (file fragments or class selectors).
|
||||
# e.g., .hidden, .tmp-file
|
||||
"^\\.",
|
||||
|
||||
# printf-style format templates.
|
||||
# e.g., %s, [%d%%], #%x
|
||||
"^[^A-Za-z ]*%",
|
||||
]
|
||||
|
||||
# Exception/error constructor functions — arguments are developer-facing, not UI text.
|
||||
exception_functions = [
|
||||
"ex-info",
|
||||
"throw",
|
||||
]
|
||||
|
||||
# Functions whose arguments are NOT checked.
|
||||
ignore_context_functions = [
|
||||
"js/console.log",
|
||||
"js/console.error",
|
||||
"js/console.warn",
|
||||
"prn",
|
||||
"println",
|
||||
"log/debug",
|
||||
"log/info",
|
||||
"log/warn",
|
||||
"log/error",
|
||||
"re-pattern",
|
||||
"re-find",
|
||||
"re-matches",
|
||||
"require",
|
||||
"ns",
|
||||
# shui utilities that take CSS IDs / class utility strings, not user-visible text.
|
||||
"shui/cn",
|
||||
"shui/popup-show",
|
||||
"shui/popup-show!",
|
||||
"shui/popup-hide",
|
||||
"shui/popup-hide!",
|
||||
"shui/popup-hide-all",
|
||||
"shui/dialog-open",
|
||||
"shui/dialog-close",
|
||||
"shui/dialog-close-all",
|
||||
"shui/dialog-confirm",
|
||||
"shui/table-get-selection-rows",
|
||||
"shui/trigger-as",
|
||||
# CSS class-joining utilities — string arguments are class names, not UI text.
|
||||
"util/classnames",
|
||||
"classnames",
|
||||
# Icon functions — arguments are icon library identifiers (e.g. "trash",
|
||||
# "arrow-right"), never user-visible text that needs translation.
|
||||
"ui/icon",
|
||||
"shui/tabler-icon",
|
||||
"icon-v2/root",
|
||||
# Ghost-icon button — the only positional argument is an icon name.
|
||||
"button-ghost-icon",
|
||||
# Shortcut display/trigger functions — arguments are key identifiers
|
||||
# (e.g. "mod+enter", "backspace"), not translatable text.
|
||||
"shui/shortcut",
|
||||
"shui/shortcut-press!",
|
||||
]
|
||||
|
||||
# ── [check-keys] settings ──────────────────────────────────────────────────────
|
||||
|
||||
[check-keys]
|
||||
|
||||
# Glob patterns for files to skip during check-keys.
|
||||
# NOTE: **/profiler.cljs is intentionally NOT excluded here so that translation
|
||||
# key references inside profiler.cljs are detected and not reported as unused.
|
||||
exclude_patterns = [
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/dev/**",
|
||||
"**/node_modules/**",
|
||||
"**/target/**",
|
||||
"**/static/**",
|
||||
"**/cljs-test-runner-out/**",
|
||||
"**/.nbb/**",
|
||||
"deps/cli/**",
|
||||
"deps/publish/**",
|
||||
"deps/publishing/**",
|
||||
]
|
||||
|
||||
# Directory containing dictionary EDN files (relative to project_root).
|
||||
dicts_dir = "src/resources/dicts"
|
||||
|
||||
# Primary dictionary file (relative to project_root).
|
||||
primary_dict = "src/resources/dicts/en.edn"
|
||||
|
||||
# Key patterns always considered "used" — for dynamically generated keys
|
||||
# that cannot be detected via static analysis.
|
||||
always_used_key_patterns = [
|
||||
# Table view keys used dynamically via (for [[option-key _] options] (t option-key)).
|
||||
"^:view\\.table/group-journal-date",
|
||||
"^:view\\.table/group-page",
|
||||
]
|
||||
|
||||
# Key namespace prefixes excluded from unused-key checking.
|
||||
ignore_key_namespaces = [
|
||||
# Shortcut keys are dynamically assembled via (keyword "command.ns" name).
|
||||
"command",
|
||||
# Shortcut category labels.
|
||||
"shortcut.category",
|
||||
# Shortcut handler group keys.
|
||||
"shortcut.handler",
|
||||
# Color theme keys derived from built-in-colors vector.
|
||||
"color",
|
||||
# Date NLP labels derived from nlp-pages vector.
|
||||
"date.nlp",
|
||||
# Flashcard FSRS rating keys derived via (keyword "flashcard.rating" ...).
|
||||
"flashcard.rating",
|
||||
# Graph validation keys derived from deprecated config keys.
|
||||
"graph.validation",
|
||||
# Left sidebar nav keys derived from tag nav entries.
|
||||
"nav",
|
||||
]
|
||||
|
||||
# Map attribute keys whose keyword values are translation key references.
|
||||
# Combined with ui_attributes during check-keys analysis.
|
||||
translation_key_attributes = ["i18n-key", "prompt-key", "title-key"]
|
||||
|
||||
# Built-in db-ident definition sources.
|
||||
# Each entry scopes keyword extraction to a specific named def/defonce form,
|
||||
# preventing false positives from other keyword literals in the same file.
|
||||
[[check-keys.db_ident_defs]]
|
||||
file = "deps/db/src/logseq/db/frontend/property.cljs"
|
||||
def = "built-in-properties"
|
||||
|
||||
[[check-keys.db_ident_defs]]
|
||||
file = "deps/db/src/logseq/db/frontend/class.cljs"
|
||||
def = "built-in-classes"
|
||||
@@ -21,6 +21,8 @@
|
||||
- Follow existing namespace and file layout; keep related workers and RTC code in their dedicated directories.
|
||||
- Prefer concise, imperative commit subjects aligned with existing history (examples: `fix: download`, `enhance(rtc): ...`).
|
||||
- Clojure map keyword name should prefer `-` instead of `_`, e.g. `:user-id` instead of `:user_id`.
|
||||
- For i18n work, use `.i18n-lint.toml` as the source of truth for lint scope and exceptions. Inside that scope, shipped UI text must use helpers from `frontend.context.i18n`; console text is exempt. Keep out-of-scope developer-only `(Dev)` labels inline in code/config, not in translation dictionaries.
|
||||
- Reuse `src/resources/dicts/en.edn` keys only on exact semantic owner + textual role match. Follow `docs/i18n-key-naming.md` for new or renamed keys. Add English source text in `en.edn`; add non-English entries only when providing real translations; keep complete sentences whole; use placeholders for plain dynamic text; run `bb lang:validate-translations`, `bb lang:lint-hardcoded`, and `bb lang:format-dicts` as needed.
|
||||
|
||||
## Testing Guidelines
|
||||
- Unit tests live in `src/test/` and should be runnable via `bb dev:lint-and-test`.
|
||||
@@ -32,6 +34,7 @@
|
||||
- PRs should describe the behavior change, link relevant issues, and note any test coverage added or skipped.
|
||||
|
||||
## Agent-Specific Notes
|
||||
- Project-specific skills live under `.agents/skills/`; load `.agents/skills/logseq-i18n/SKILL.md` for i18n/localization/hardcoded UI text tasks.
|
||||
- Review notes live in `prompts/review.md`; check them when preparing changes.
|
||||
- DB-sync feature guide for AI agents: `docs/agent-guide/db-sync/db-sync-guide.md`.
|
||||
- DB-sync protocol reference: `docs/agent-guide/db-sync/protocol.md`.
|
||||
|
||||
@@ -74,7 +74,7 @@ The DB version is in beta status while the new mobile app and RTC is in alpha. T
|
||||
|
||||
To get started with the DB version:
|
||||
* To try the latest web version, go to https://test.logseq.com/.
|
||||
* To try the latest desktop version, login to Github and go to https://github.com/logseq/logseq/actions/workflows/build-desktop-release.yml and click on the latest release. Scroll to the bottom and under the `Artifacts` section download the artifact for your operating system.
|
||||
* To try the latest desktop version, login to GitHub and go to https://github.com/logseq/logseq/actions/workflows/build-desktop-release.yml and click on the latest release. Scroll to the bottom and under the `Artifacts` section download the artifact for your operating system.
|
||||
* To try the latest by building from the source code
|
||||
* Use `test/db` for stable releases. Fewer bugs and slower updates. Update frequency: days or weeks.
|
||||
* Use `master` for the latest updates as they are developed. Expect more bugs and faster changes. Update frequency: hours or days.
|
||||
|
||||
9
bb.edn
9
bb.edn
@@ -222,9 +222,18 @@
|
||||
lang:missing
|
||||
logseq.tasks.lang/list-missing
|
||||
|
||||
lang:pseudo
|
||||
logseq.tasks.lang/list-pseudo
|
||||
|
||||
lang:format-dicts
|
||||
logseq.tasks.lang/format-dicts
|
||||
|
||||
lang:validate-translations
|
||||
logseq.tasks.lang/validate-translations
|
||||
|
||||
lang:lint-hardcoded
|
||||
logseq.tasks.lang/lint-hardcoded
|
||||
|
||||
ai:check-common-errors
|
||||
logseq.tasks.common-errors/check-common-errors}
|
||||
|
||||
|
||||
64
bin/logseq-i18n-lint
Normal file
64
bin/logseq-i18n-lint
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# logseq-i18n-lint launcher
|
||||
# Detects the current OS/arch and runs the prebuilt binary in the same directory.
|
||||
# All arguments are forwarded to the binary.
|
||||
#
|
||||
# Supported platforms:
|
||||
# Linux x86_64 -> logseq-i18n-lint-x86_64-linux
|
||||
# Linux aarch64 -> logseq-i18n-lint-aarch64-linux
|
||||
# macOS x86_64 -> logseq-i18n-lint-x86_64-macos
|
||||
# macOS arm64 -> logseq-i18n-lint-aarch64-macos
|
||||
# Windows x86_64 -> logseq-i18n-lint-x86_64-windows.exe (via Git Bash / MSYS2)
|
||||
# Windows aarch64 -> logseq-i18n-lint-aarch64-windows.exe
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Detect OS ────────────────────────────────────────────────────────────────
|
||||
|
||||
OS="$(uname -s)"
|
||||
case "${OS}" in
|
||||
Linux*) platform="linux" ;;
|
||||
Darwin*) platform="macos" ;;
|
||||
MINGW*|MSYS*|CYGWIN*|Windows_NT)
|
||||
platform="windows" ;;
|
||||
*)
|
||||
echo "error: unsupported OS: ${OS}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Detect architecture ───────────────────────────────────────────────────────
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "${ARCH}" in
|
||||
x86_64|amd64) arch="x86_64" ;;
|
||||
aarch64|arm64) arch="aarch64" ;;
|
||||
*)
|
||||
echo "error: unsupported architecture: ${ARCH}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Resolve binary path ────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "${platform}" == "windows" ]]; then
|
||||
bin="${SCRIPT_DIR}/logseq-i18n-lint-${arch}-${platform}.exe"
|
||||
else
|
||||
bin="${SCRIPT_DIR}/logseq-i18n-lint-${arch}-${platform}"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${bin}" ]]; then
|
||||
echo "error: binary not found: ${bin}" >&2
|
||||
echo " Download it from: https://github.com/logseq/logseq-i18n-lint/releases/latest" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "${bin}" ]]; then
|
||||
chmod +x "${bin}"
|
||||
fi
|
||||
|
||||
# ── Run ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
exec "${bin}" "$@"
|
||||
BIN
bin/logseq-i18n-lint-aarch64-linux
Normal file
BIN
bin/logseq-i18n-lint-aarch64-linux
Normal file
Binary file not shown.
BIN
bin/logseq-i18n-lint-aarch64-macos
Normal file
BIN
bin/logseq-i18n-lint-aarch64-macos
Normal file
Binary file not shown.
BIN
bin/logseq-i18n-lint-aarch64-windows.exe
Normal file
BIN
bin/logseq-i18n-lint-aarch64-windows.exe
Normal file
Binary file not shown.
BIN
bin/logseq-i18n-lint-x86_64-linux
Normal file
BIN
bin/logseq-i18n-lint-x86_64-linux
Normal file
Binary file not shown.
BIN
bin/logseq-i18n-lint-x86_64-macos
Normal file
BIN
bin/logseq-i18n-lint-x86_64-macos
Normal file
Binary file not shown.
BIN
bin/logseq-i18n-lint-x86_64-windows.exe
Normal file
BIN
bin/logseq-i18n-lint-x86_64-windows.exe
Normal file
Binary file not shown.
@@ -1,7 +1,5 @@
|
||||
(ns logseq.e2e.graph
|
||||
(:require [clojure.edn :as edn]
|
||||
[clojure.string :as string]
|
||||
[logseq.e2e.assert :as assert]
|
||||
(:require [logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.util :as util]
|
||||
@@ -111,7 +109,7 @@
|
||||
(.first (w/-query (format "div[data-testid='logseq_db_%s'] .graph-action-btn" graph-name)))]
|
||||
(w/click action-btn)
|
||||
(w/click ".delete-local-graph-menu-item")
|
||||
(w/click "div[role='alertdialog'] button:text('ok')")))
|
||||
(w/click "div[role='alertdialog'] button:text('Confirm')")))
|
||||
|
||||
(defn remove-remote-graph
|
||||
[graph-name]
|
||||
@@ -120,7 +118,7 @@
|
||||
(.first (w/-query (format "div[data-testid='logseq_db_%s'] .graph-action-btn" graph-name)))]
|
||||
(w/click action-btn)
|
||||
(w/click ".delete-remote-graph-menu-item")
|
||||
(w/click "div[role='alertdialog'] button:text('ok')")))
|
||||
(w/click "div[role='alertdialog'] button:text('Confirm')")))
|
||||
|
||||
(defn switch-graph
|
||||
[to-graph-name wait-sync? need-input-password?]
|
||||
@@ -136,8 +134,9 @@
|
||||
(k/esc)
|
||||
(k/esc)
|
||||
(util/search-and-click "(Dev) Validate current graph")
|
||||
(assert/assert-is-visible (loc/and ".notifications div.notification-success div" (w/get-by-text "Your graph is valid")))
|
||||
(let [content (.textContent (loc/and ".notifications div.notification-success div" (w/get-by-text "Your graph is valid")))
|
||||
summary (edn/read-string (subs content (string/index-of content "{")))]
|
||||
(w/click ".notifications div.notification-success .ls-icon-x")
|
||||
summary))
|
||||
(assert/assert-is-visible
|
||||
(loc/and ".notifications div.notification-success div"
|
||||
(w/get-by-text "Your graph is valid")))
|
||||
(when (w/visible? ".notifications div.notification-success .ls-icon-x")
|
||||
(w/click ".notifications div.notification-success .ls-icon-x"))
|
||||
{:valid? true})
|
||||
|
||||
@@ -36,9 +36,9 @@
|
||||
(defn delete-page
|
||||
[page-name]
|
||||
(goto-page page-name)
|
||||
(w/click "button[title='More']")
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(w/click "[role='menuitem'] div:text('Delete page')")
|
||||
(w/click "div[role='alertdialog'] button:text('ok')"))
|
||||
(w/click "div[role='alertdialog'] button:text('Confirm')"))
|
||||
|
||||
(defn rename-page
|
||||
[old-page-name new-page-name]
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
(ns logseq.e2e.settings
|
||||
(:require [logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(def ^:private e2e-init-script
|
||||
"localStorage.setItem('preferred-language', '\"en\"'); localStorage.setItem('developer-mode', '\"true\"');")
|
||||
|
||||
(def ^:private refresh-ready-script
|
||||
"(() => document.documentElement.lang === 'en'
|
||||
&& localStorage.getItem('preferred-language') === '\"en\"'
|
||||
&& localStorage.getItem('developer-mode') === '\"true\"')()")
|
||||
|
||||
(defn install-init-script!
|
||||
[ctx]
|
||||
(.addInitScript ctx e2e-init-script))
|
||||
|
||||
(defn wait-test-env-ready!
|
||||
[]
|
||||
(loop [remaining 20]
|
||||
(if (w/eval-js refresh-ready-script)
|
||||
true
|
||||
(if (zero? remaining)
|
||||
(throw (ex-info "test env not ready after refresh" {}))
|
||||
(do
|
||||
(util/wait-timeout 250)
|
||||
(recur (dec remaining)))))))
|
||||
|
||||
(defn- test-env-ready?
|
||||
[]
|
||||
(try
|
||||
(wait-test-env-ready!)
|
||||
true
|
||||
(catch Throwable _e
|
||||
false)))
|
||||
|
||||
(defn refresh-test-env!
|
||||
[]
|
||||
(loop [attempt 0]
|
||||
(w/refresh)
|
||||
(assert/assert-graph-loaded?)
|
||||
(if (test-env-ready?)
|
||||
true
|
||||
(if (< attempt 2)
|
||||
(recur (inc attempt))
|
||||
(wait-test-env-ready!)))))
|
||||
|
||||
(defn developer-mode
|
||||
[]
|
||||
(w/eval-js "localStorage.setItem('preferred-language', '\"en\"')")
|
||||
(w/click "button[title='More'] .ls-icon-dots")
|
||||
(w/click ".ls-icon-settings")
|
||||
(w/click "[data-id='advanced']")
|
||||
(let [q (.last (w/-query ".ui__toggle [aria-checked='false']"))]
|
||||
(when (.isVisible q)
|
||||
(w/click q)))
|
||||
(k/esc)
|
||||
(w/eval-js e2e-init-script)
|
||||
(assert/assert-in-normal-mode?))
|
||||
|
||||
@@ -96,7 +96,11 @@
|
||||
(defn search-and-click
|
||||
[search-text]
|
||||
(search search-text)
|
||||
(w/click (.first (w/get-by-test-id search-text))))
|
||||
(let [result (.first (w/get-by-test-id search-text))]
|
||||
(repeat-until-visible 5 result #(do
|
||||
(search search-text)
|
||||
(wait-timeout 300)))
|
||||
(w/click result)))
|
||||
|
||||
(defn wait-editor-gone
|
||||
([]
|
||||
@@ -153,12 +157,12 @@
|
||||
:or {username "e2etest"
|
||||
password "Logseq-e2e"}}]
|
||||
(w/eval-js "localStorage.setItem(\"login-enabled\",true);")
|
||||
(w/click "button[title=\"More\"]")
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(w/click "div:text(\"Login\")")
|
||||
(input username)
|
||||
(k/tab)
|
||||
(input password)
|
||||
(w/click "button[type=\"submit\"]:text(\"Sign in\")")
|
||||
(w/click ".cp__user-login button[type=\"submit\"]")
|
||||
(w/wait-for-not-visible ".cp__user-login"))
|
||||
|
||||
(defn goto-journals
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
(w/grant-permissions :clipboard-write :clipboard-read)
|
||||
(binding [custom-report/*pw-contexts* #{(.context (w/get-page))}
|
||||
custom-report/*pw-page->console-logs* (atom {})]
|
||||
(settings/install-init-script! (.context (w/get-page)))
|
||||
(w/grant-permissions :clipboard-write :clipboard-read)
|
||||
(w/navigate (pw-page/get-test-url port))
|
||||
(settings/developer-mode)
|
||||
(w/refresh)
|
||||
(assert/assert-graph-loaded?)
|
||||
(settings/refresh-test-env!)
|
||||
(let [p (w/get-page)]
|
||||
(.onConsoleMessage p (fn [msg]
|
||||
(when custom-report/*pw-page->console-logs*
|
||||
@@ -43,6 +43,7 @@
|
||||
:slow-mo @config/*slow-mo}
|
||||
p1 (w/make-page page-opts)
|
||||
p2 (w/make-page page-opts)]
|
||||
(run! #(settings/install-init-script! (.context @%)) [p1 p2])
|
||||
(reset! *page1 p1)
|
||||
(reset! *page2 p2)
|
||||
(binding [custom-report/*pw-contexts* (set [(.context @p1) (.context @p2)])
|
||||
@@ -53,8 +54,7 @@
|
||||
(w/grant-permissions :clipboard-write :clipboard-read)
|
||||
(w/navigate (pw-page/get-test-url (or port @config/*port)))
|
||||
(settings/developer-mode)
|
||||
(w/refresh)
|
||||
(assert/assert-graph-loaded?)
|
||||
(settings/refresh-test-env!)
|
||||
(let [p (w/get-page)]
|
||||
(.onConsoleMessage
|
||||
p
|
||||
@@ -84,7 +84,7 @@
|
||||
(w/with-page-open p) ; use with-page-open to close playwright instance
|
||||
(binding [custom-report/*pw-contexts* #{ctx}
|
||||
*pw-ctx* ctx]
|
||||
(.addInitScript ctx "localStorage.setItem('preferred-language', '\"en\"')")
|
||||
(settings/install-init-script! ctx)
|
||||
(f)
|
||||
(.close (.browser *pw-ctx*)))))
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
2
|
||||
#(w/with-page %
|
||||
(settings/developer-mode)
|
||||
(w/refresh)
|
||||
(settings/refresh-test-env!)
|
||||
(util/login-test-account))
|
||||
[@*page1 @*page2])
|
||||
(w/with-page @*page1
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"Opens the plugins dialog via the More menu"
|
||||
[]
|
||||
(util/double-esc)
|
||||
(w/click "button[title='More'] .ls-icon-dots")
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(w/click ".ui__dropdown-menu-item:has-text('Plugins')")
|
||||
(w/wait-for ".cp__plugins-page"))
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(def ^:private property-types ["Text" "Number" "Date" "DateTime" "Checkbox" "Url" "Node"])
|
||||
(def ^:private property-types ["Text" "Number" "Date" "DateTime" "Checkbox" "URL" "Node"])
|
||||
|
||||
(defn add-new-properties
|
||||
[title-prefix]
|
||||
@@ -23,7 +23,7 @@
|
||||
(let [property-name (str "p-" title-prefix "-" property-type)]
|
||||
(w/click (util/get-by-text (str title-prefix "-" property-type) true))
|
||||
(k/press "Control+e")
|
||||
(util/input-command "Add new property")
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(util/input property-name)
|
||||
(w/click (util/get-by-text "New option:" false))
|
||||
@@ -41,7 +41,7 @@
|
||||
(k/enter)
|
||||
(k/esc))
|
||||
"Checkbox" nil
|
||||
"Url" nil
|
||||
"URL" nil
|
||||
"Node" (do
|
||||
(w/click (w/get-by-text "Skip choosing tag"))
|
||||
(util/input (str title-prefix "-Node-value"))
|
||||
|
||||
2
deps/cli/README.md
vendored
2
deps/cli/README.md
vendored
@@ -1,6 +1,6 @@
|
||||
## Description
|
||||
|
||||
This library provides a `logseq` CLI for DB graphs created using the [database-version](/README.md#-database-version). By default, the CLI works offline with local graphs. This allows for running commands automatically on CI/CD platforms like Github Actions. Most CLI commands also connect to the current DB graph in a desktop app (a.k.a. in-app graph) if the [HTTP API Server](https://docs.logseq.com/#/page/local%20http%20server) is turned on.
|
||||
This library provides a `logseq` CLI for DB graphs created using the [database-version](/README.md#-database-version). By default, the CLI works offline with local graphs. This allows for running commands automatically on CI/CD platforms like GitHub Actions. Most CLI commands also connect to the current DB graph in a desktop app (a.k.a. in-app graph) if the [HTTP API Server](https://docs.logseq.com/#/page/local%20http%20server) is turned on.
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
5
deps/common/src/logseq/common/config.cljs
vendored
5
deps/common/src/logseq/common/config.cljs
vendored
@@ -78,6 +78,9 @@
|
||||
|
||||
(defonce block-pattern "-")
|
||||
|
||||
(def unused-in-db-graphs-deprecation
|
||||
"is not used in DB graphs")
|
||||
|
||||
(def file-only-config
|
||||
"File only config keys that are deprecated in DB graphs along with
|
||||
descriptions for their deprecation."
|
||||
@@ -100,7 +103,7 @@
|
||||
:srs/initial-interval
|
||||
:whiteboards-directory
|
||||
:feature/enable-whiteboards?]
|
||||
(repeat "is not used in DB graphs"))
|
||||
(repeat unused-in-db-graphs-deprecation))
|
||||
{:preferred-format
|
||||
"is not used in DB graphs as there is only markdown mode."
|
||||
:property-pages/enabled?
|
||||
|
||||
1
deps/common/src/logseq/common/date.cljs
vendored
1
deps/common/src/logseq/common/date.cljs
vendored
@@ -31,6 +31,7 @@
|
||||
"MM_dd_yyyy"
|
||||
"yyyy/MM/dd"
|
||||
"yyyy-MM-dd"
|
||||
"yyyy-MM-dd EEE"
|
||||
"yyyy-MM-dd EEEE"
|
||||
"yyyy_MM_dd"
|
||||
"yyyyMMdd"
|
||||
|
||||
8
deps/db/src/logseq/db.cljs
vendored
8
deps/db/src/logseq/db.cljs
vendored
@@ -522,6 +522,14 @@
|
||||
(when db
|
||||
(d/entity db (get-first-page-by-name db page-name))))
|
||||
|
||||
(defn get-journal-page-by-day
|
||||
"Get a journal page given its :block/journal-day value."
|
||||
[db journal-day]
|
||||
(when (and db journal-day)
|
||||
(when-let [eid (some-> (first (d/datoms db :avet :block/journal-day journal-day))
|
||||
:e)]
|
||||
(d/entity db eid))))
|
||||
|
||||
(def get-built-in-page db-db/get-built-in-page)
|
||||
|
||||
(def library? db-db/library?)
|
||||
|
||||
394
deps/db/src/logseq/db/frontend/property.cljs
vendored
394
deps/db/src/logseq/db/frontend/property.cljs
vendored
@@ -55,17 +55,15 @@
|
||||
:logseq.property/ui-position {:title "Property position"
|
||||
:schema {:type :keyword
|
||||
:hide? true}}
|
||||
:logseq.property/classes
|
||||
{:title "Property classes"
|
||||
:schema {:type :entity
|
||||
:cardinality :many
|
||||
:public? false
|
||||
:hide? true}}
|
||||
:logseq.property/value
|
||||
{:title "Property value"
|
||||
:schema {:type :any
|
||||
:public? false
|
||||
:hide? true}}
|
||||
:logseq.property/classes {:title "Property classes"
|
||||
:schema {:type :entity
|
||||
:cardinality :many
|
||||
:public? false
|
||||
:hide? true}}
|
||||
:logseq.property/value {:title "Property value"
|
||||
:schema {:type :any
|
||||
:public? false
|
||||
:hide? true}}
|
||||
|
||||
:block/alias {:title "Alias"
|
||||
:attribute :block/alias
|
||||
@@ -230,19 +228,18 @@
|
||||
|
||||
:logseq.property.pdf/hl-type {:title "Annotation type"
|
||||
:schema {:type :keyword :hide? true}}
|
||||
:logseq.property.pdf/hl-color
|
||||
{:title "Annotation color"
|
||||
:schema {:type :default :hide? true}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
|
||||
[[:logseq.property/color.yellow "yellow"]
|
||||
[:logseq.property/color.red "red"]
|
||||
[:logseq.property/color.green "green"]
|
||||
[:logseq.property/color.blue "blue"]
|
||||
[:logseq.property/color.purple "purple"]])}
|
||||
:logseq.property.pdf/hl-color {:title "Annotation color"
|
||||
:schema {:type :default :hide? true}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
|
||||
[[:logseq.property/color.yellow "yellow"]
|
||||
[:logseq.property/color.red "red"]
|
||||
[:logseq.property/color.green "green"]
|
||||
[:logseq.property/color.blue "blue"]
|
||||
[:logseq.property/color.purple "purple"]])}
|
||||
:logseq.property.pdf/hl-page {:title "Annotation page"
|
||||
:schema {:type :raw-number :hide? true}}
|
||||
:logseq.property.pdf/hl-image {:title "Annotation image"
|
||||
@@ -262,14 +259,6 @@
|
||||
:schema {:type :node
|
||||
:cardinality :many
|
||||
:hide? true}}
|
||||
;; TODO: Remove deprecated
|
||||
:logseq.property.tldraw/page {:title "Tldraw Page"
|
||||
:schema {:type :map
|
||||
:hide? true}}
|
||||
;; TODO: Remove deprecated
|
||||
:logseq.property.tldraw/shape {:title "Tldraw Shape"
|
||||
:schema {:type :map
|
||||
:hide? true}}
|
||||
|
||||
;; Journal props
|
||||
:logseq.property.journal/title-format {:title "Title Format"
|
||||
@@ -277,132 +266,119 @@
|
||||
{:type :string
|
||||
:public? false}}
|
||||
|
||||
:logseq.property/choice-checkbox-state
|
||||
{:title "Choice checkbox state"
|
||||
:schema {:type :checkbox
|
||||
:hide? true}
|
||||
:queryable? false}
|
||||
:logseq.property/choice-checkbox-state {:title "Choice checkbox state"
|
||||
:schema {:type :checkbox
|
||||
:hide? true}
|
||||
:queryable? false}
|
||||
;; tag-scoped choice, a choice can be specified locally for specified tags
|
||||
:logseq.property/choice-classes
|
||||
{:title "Choice classes"
|
||||
:schema {:type :class
|
||||
:cardinality :many
|
||||
:public? false
|
||||
:hide? true
|
||||
:view-context :never}
|
||||
:queryable? false}
|
||||
:logseq.property/choice-classes {:title "Choice classes"
|
||||
:schema {:type :class
|
||||
:cardinality :many
|
||||
:public? false
|
||||
:hide? true
|
||||
:view-context :never}
|
||||
:queryable? false}
|
||||
;; tag can define which global choices are hidden for its objects
|
||||
:logseq.property/choice-exclusions
|
||||
{:title "Choice exclusions"
|
||||
:schema {:type :node
|
||||
:cardinality :many
|
||||
:public? false
|
||||
:hide? true
|
||||
:view-context :never}
|
||||
:queryable? false}
|
||||
:logseq.property/checkbox-display-properties
|
||||
{:title "Properties displayed as checkbox"
|
||||
:schema {:type :property
|
||||
:cardinality :many
|
||||
:hide? true}
|
||||
:queryable? false}
|
||||
:logseq.property/choice-exclusions {:title "Choice exclusions"
|
||||
:schema {:type :node
|
||||
:cardinality :many
|
||||
:public? false
|
||||
:hide? true
|
||||
:view-context :never}
|
||||
:queryable? false}
|
||||
:logseq.property/checkbox-display-properties {:title "Properties displayed as checkbox"
|
||||
:schema {:type :property
|
||||
:cardinality :many
|
||||
:hide? true}
|
||||
:queryable? false}
|
||||
;; Task props
|
||||
:logseq.property/status
|
||||
{:title "Status"
|
||||
:schema
|
||||
{:type :default
|
||||
:public? true
|
||||
:ui-position :block-left}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value icon checkbox-state]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
|
||||
:icon {:type :tabler-icon :id icon}
|
||||
:properties (when (some? checkbox-state)
|
||||
{:logseq.property/choice-checkbox-state checkbox-state})})
|
||||
[[:logseq.property/status.backlog "Backlog" "Backlog"]
|
||||
[:logseq.property/status.todo "Todo" "Todo" false]
|
||||
[:logseq.property/status.doing "Doing" "InProgress50"]
|
||||
[:logseq.property/status.in-review "In Review" "InReview"]
|
||||
[:logseq.property/status.done "Done" "Done" true]
|
||||
[:logseq.property/status.canceled "Canceled" "Cancelled"]])
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/default-value :logseq.property/status.todo
|
||||
:logseq.property/enable-history? true}
|
||||
:queryable? true}
|
||||
:logseq.property/priority
|
||||
{:title "Priority"
|
||||
:schema
|
||||
{:type :default
|
||||
:public? true
|
||||
:ui-position :block-left}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value icon]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
|
||||
:icon {:type :tabler-icon :id icon}})
|
||||
[[:logseq.property/priority.low "Low" "priorityLvlLow"]
|
||||
[:logseq.property/priority.medium "Medium" "priorityLvlMedium"]
|
||||
[:logseq.property/priority.high "High" "priorityLvlHigh"]
|
||||
[:logseq.property/priority.urgent "Urgent" "priorityLvlUrgent"]])
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/enable-history? true}}
|
||||
:logseq.property/deadline
|
||||
{:title "Deadline"
|
||||
:schema {:type :datetime
|
||||
:public? true
|
||||
:ui-position :block-below}
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/description "Use it to finish something at a specific date(time)."}
|
||||
:queryable? true}
|
||||
:logseq.property/scheduled
|
||||
{:title "Scheduled"
|
||||
:schema {:type :datetime
|
||||
:public? true
|
||||
:ui-position :block-below}
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/description "Use it to plan something to start at a specific date(time)."}
|
||||
:queryable? true}
|
||||
:logseq.property.repeat/recur-frequency
|
||||
(let [schema {:type :number
|
||||
:public? false}]
|
||||
{:title "Repeating recur frequency"
|
||||
:schema schema
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/default-value 1}
|
||||
:queryable? true})
|
||||
:logseq.property.repeat/recur-unit
|
||||
{:title "Repeating recur unit"
|
||||
:schema {:type :default
|
||||
:public? false}
|
||||
:closed-values (mapv (fn [[db-ident value]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
|
||||
[[:logseq.property.repeat/recur-unit.minute "Minute"]
|
||||
[:logseq.property.repeat/recur-unit.hour "Hour"]
|
||||
[:logseq.property.repeat/recur-unit.day "Day"]
|
||||
[:logseq.property.repeat/recur-unit.week "Week"]
|
||||
[:logseq.property.repeat/recur-unit.month "Month"]
|
||||
[:logseq.property.repeat/recur-unit.year "Year"]])
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/default-value :logseq.property.repeat/recur-unit.day}
|
||||
:queryable? true}
|
||||
:logseq.property.repeat/repeated?
|
||||
{:title "Node Repeats?"
|
||||
:schema {:type :checkbox
|
||||
:hide? true}
|
||||
:queryable? true}
|
||||
:logseq.property.repeat/temporal-property
|
||||
{:title "Repeating Temporal Property"
|
||||
:schema {:type :property
|
||||
:hide? true}}
|
||||
:logseq.property.repeat/checked-property
|
||||
{:title "Repeating Checked Property"
|
||||
:schema {:type :property
|
||||
:hide? true}}
|
||||
:logseq.property/status {:title "Status"
|
||||
:schema
|
||||
{:type :default
|
||||
:public? true
|
||||
:ui-position :block-left}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value icon checkbox-state]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
|
||||
:icon {:type :tabler-icon :id icon}
|
||||
:properties (when (some? checkbox-state)
|
||||
{:logseq.property/choice-checkbox-state checkbox-state})})
|
||||
[[:logseq.property/status.backlog "Backlog" "Backlog"]
|
||||
[:logseq.property/status.todo "Todo" "Todo" false]
|
||||
[:logseq.property/status.doing "Doing" "InProgress50"]
|
||||
[:logseq.property/status.in-review "In Review" "InReview"]
|
||||
[:logseq.property/status.done "Done" "Done" true]
|
||||
[:logseq.property/status.canceled "Canceled" "Cancelled"]])
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/default-value :logseq.property/status.todo
|
||||
:logseq.property/enable-history? true}
|
||||
:queryable? true}
|
||||
:logseq.property/priority {:title "Priority"
|
||||
:schema
|
||||
{:type :default
|
||||
:public? true
|
||||
:ui-position :block-left}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value icon]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
|
||||
:icon {:type :tabler-icon :id icon}})
|
||||
[[:logseq.property/priority.low "Low" "priorityLvlLow"]
|
||||
[:logseq.property/priority.medium "Medium" "priorityLvlMedium"]
|
||||
[:logseq.property/priority.high "High" "priorityLvlHigh"]
|
||||
[:logseq.property/priority.urgent "Urgent" "priorityLvlUrgent"]])
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/enable-history? true}}
|
||||
:logseq.property/deadline {:title "Deadline"
|
||||
:schema {:type :datetime
|
||||
:public? true
|
||||
:ui-position :block-below}
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/description "Use it to finish something at a specific date(time)."}
|
||||
:queryable? true}
|
||||
:logseq.property/scheduled {:title "Scheduled"
|
||||
:schema {:type :datetime
|
||||
:public? true
|
||||
:ui-position :block-below}
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/description "Use it to plan something to start at a specific date(time)."}
|
||||
:queryable? true}
|
||||
:logseq.property.repeat/recur-frequency (let [schema {:type :number
|
||||
:public? false}]
|
||||
{:title "Repeating recur frequency"
|
||||
:schema schema
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/default-value 1}
|
||||
:queryable? true})
|
||||
:logseq.property.repeat/recur-unit {:title "Repeating recur unit"
|
||||
:schema {:type :default
|
||||
:public? false}
|
||||
:closed-values (mapv (fn [[db-ident value]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
|
||||
[[:logseq.property.repeat/recur-unit.minute "Minute"]
|
||||
[:logseq.property.repeat/recur-unit.hour "Hour"]
|
||||
[:logseq.property.repeat/recur-unit.day "Day"]
|
||||
[:logseq.property.repeat/recur-unit.week "Week"]
|
||||
[:logseq.property.repeat/recur-unit.month "Month"]
|
||||
[:logseq.property.repeat/recur-unit.year "Year"]])
|
||||
:properties {:logseq.property/hide-empty-value true
|
||||
:logseq.property/default-value :logseq.property.repeat/recur-unit.day}
|
||||
:queryable? true}
|
||||
:logseq.property.repeat/repeated? {:title "Node Repeats?"
|
||||
:schema {:type :checkbox
|
||||
:hide? true}
|
||||
:queryable? true}
|
||||
:logseq.property.repeat/temporal-property {:title "Repeating Temporal Property"
|
||||
:schema {:type :property
|
||||
:hide? true}}
|
||||
:logseq.property.repeat/checked-property {:title "Repeating Checked Property"
|
||||
:schema {:type :property
|
||||
:hide? true}}
|
||||
|
||||
;; TODO: Add more props :Assignee, :Estimate, :Cycle, :Project
|
||||
|
||||
@@ -426,39 +402,36 @@
|
||||
:view-context :page
|
||||
:public? true}}
|
||||
|
||||
:logseq.property.view/type
|
||||
{:title "View Type"
|
||||
:schema
|
||||
{:type :default
|
||||
:public? false
|
||||
:hide? true}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value icon]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
|
||||
:icon {:type :tabler-icon :id icon}})
|
||||
[[:logseq.property.view/type.table "Table View" "table"]
|
||||
[:logseq.property.view/type.list "List View" "list"]
|
||||
[:logseq.property.view/type.gallery "Gallery View" "layout-grid"]])
|
||||
:properties {:logseq.property/default-value :logseq.property.view/type.table}
|
||||
:queryable? true}
|
||||
:logseq.property.view/type {:title "View Type"
|
||||
:schema
|
||||
{:type :default
|
||||
:public? false
|
||||
:hide? true}
|
||||
:closed-values
|
||||
(mapv (fn [[db-ident value icon]]
|
||||
{:db-ident db-ident
|
||||
:value value
|
||||
:uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
|
||||
:icon {:type :tabler-icon :id icon}})
|
||||
[[:logseq.property.view/type.table "Table View" "table"]
|
||||
[:logseq.property.view/type.list "List View" "list"]
|
||||
[:logseq.property.view/type.gallery "Gallery View" "layout-grid"]])
|
||||
:properties {:logseq.property/default-value :logseq.property.view/type.table}
|
||||
:queryable? true}
|
||||
|
||||
:logseq.property.view/feature-type
|
||||
{:title "View Feature Type"
|
||||
:schema
|
||||
{:type :keyword
|
||||
:public? false
|
||||
:hide? true}
|
||||
:queryable? false}
|
||||
:logseq.property.view/feature-type {:title "View Feature Type"
|
||||
:schema
|
||||
{:type :keyword
|
||||
:public? false
|
||||
:hide? true}
|
||||
:queryable? false}
|
||||
|
||||
:logseq.property.view/group-by-property
|
||||
{:title "View group by property"
|
||||
:schema
|
||||
{:type :property
|
||||
:public? false
|
||||
:hide? true}
|
||||
:queryable? true}
|
||||
:logseq.property.view/group-by-property {:title "View group by property"
|
||||
:schema
|
||||
{:type :property
|
||||
:public? false
|
||||
:hide? true}
|
||||
:queryable? true}
|
||||
|
||||
:logseq.property.view/sort-groups-by-property {:title "View sort groups by"
|
||||
:schema
|
||||
@@ -922,3 +895,52 @@
|
||||
(when db
|
||||
(let [block (or (d/entity db (:db/id block)) block)]
|
||||
(lookup block db-ident))))
|
||||
|
||||
(defn built-in-ident->i18n-key
|
||||
"Derives an i18n key from a built-in db-ident.
|
||||
Returns nil for non-built-in idents.
|
||||
Examples:
|
||||
:block/alias -> :property.built-in/alias
|
||||
:logseq.property/status -> :property.built-in/status
|
||||
:logseq.property.code/lang -> :property.built-in/code-lang
|
||||
:logseq.class/Task -> :class.built-in/task
|
||||
:logseq.property/status.backlog -> :property.status/backlog"
|
||||
[db-ident]
|
||||
(let [ns-str (namespace db-ident)
|
||||
n (name db-ident)]
|
||||
(cond
|
||||
(= ns-str "logseq.class")
|
||||
(keyword "class.built-in" (string/lower-case n))
|
||||
|
||||
(or (= ns-str "logseq.property")
|
||||
(string/starts-with? ns-str "logseq.property."))
|
||||
(let [sub-ns (when (not= ns-str "logseq.property")
|
||||
(subs ns-str (count "logseq.property.")))
|
||||
dot-idx (string/index-of n ".")
|
||||
clean-n (string/replace n #"\?$" "")]
|
||||
(if dot-idx
|
||||
;; Closed value: logseq.property/status.backlog -> :property.status/backlog
|
||||
(let [prop-part (subs clean-n 0 dot-idx)
|
||||
choice-part (subs clean-n (inc dot-idx))
|
||||
subdomain (if sub-ns (str sub-ns "-" prop-part) prop-part)]
|
||||
(keyword (str "property." subdomain) choice-part))
|
||||
;; Property definition
|
||||
(if sub-ns
|
||||
(keyword "property.built-in" (str sub-ns "-" clean-n))
|
||||
(keyword "property.built-in" clean-n))))
|
||||
|
||||
(= ns-str "block")
|
||||
(keyword "property.built-in" (string/replace n #"\?$" ""))
|
||||
|
||||
:else nil)))
|
||||
|
||||
(defn built-in-display-title
|
||||
"Returns the display title for a built-in entity (property or class).
|
||||
`translate-fn` takes an i18n keyword and returns the translated string.
|
||||
Falls back to (:block/title entity) when no translation is available."
|
||||
[entity translate-fn]
|
||||
(or (when-let [i18n-key (some-> (:db/ident entity) built-in-ident->i18n-key)]
|
||||
(let [s (translate-fn i18n-key)]
|
||||
(when-not (string/starts-with? (str s) "{Missing")
|
||||
s)))
|
||||
(:block/title entity)))
|
||||
|
||||
10
deps/db/test/logseq/db_test.cljs
vendored
10
deps/db/test/logseq/db_test.cljs
vendored
@@ -58,6 +58,16 @@
|
||||
(is (= "movie" (:block/title (ldb/get-case-page @conn "movie"))))
|
||||
(is (= "Movie" (:block/title (ldb/get-case-page @conn "Movie"))))))
|
||||
|
||||
(deftest get-journal-page-by-day
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:build/journal 20260410}}
|
||||
{:page {:build/journal 20260411}}]})]
|
||||
(is (= "Apr 10th, 2026"
|
||||
(:block/title (ldb/get-journal-page-by-day @conn 20260410))))
|
||||
(is (= "Apr 11th, 2026"
|
||||
(:block/title (ldb/get-journal-page-by-day @conn 20260411))))))
|
||||
|
||||
(deftest page-exists
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
Your notes will be stored in the local browser storage. We are using IndexedDB.
|
||||
** How do I use it?
|
||||
*** 1. Sync between multiple devices
|
||||
Currently, we only support syncing through Github, more options (e.g.
|
||||
Currently, we only support syncing through GitHub, more options (e.g.
|
||||
Gitlab, Dropbox, Google Drive, WebDAV, etc.) will be added soon.
|
||||
|
||||
We are using an excellent web git client called [[https://isomorphic-git.org/][isomorphic-git]].
|
||||
**** Step 1
|
||||
Click the button /Login with Github/.
|
||||
Click the button /Login with GitHub/.
|
||||
**** Step 2
|
||||
Set your Github personal access token, the token will be encrypted and
|
||||
Set your GitHub personal access token, the token will be encrypted and
|
||||
stored in the browser local storage, our server will never store it.
|
||||
|
||||
If you know nothing about either Git or the personal access token, no worries,
|
||||
@@ -50,7 +50,7 @@
|
||||
- Twitter: https://twitter.com/logseq
|
||||
- Discord: https://discord.gg/KpN4eHY where we ask questions and share tips
|
||||
- Website: https://logseq.com/
|
||||
- Github: https://github.com/logseq/logseq everyone is encouraged to report issues!
|
||||
- GitHub: https://github.com/logseq/logseq everyone is encouraged to report issues!
|
||||
- Our blog: https://logseq.com/blog
|
||||
** Credits to
|
||||
- [[https://roamresearch.com/][Roam Research]]
|
||||
|
||||
3
deps/outliner/src/logseq/outliner/core.cljs
vendored
3
deps/outliner/src/logseq/outliner/core.cljs
vendored
@@ -842,7 +842,8 @@
|
||||
(when (seq (filter :logseq.property/built-in? top-level-blocks*))
|
||||
(throw (ex-info "Built-in nodes can't be deleted"
|
||||
{:type :notification
|
||||
:payload {:message "Built-in nodes can't be deleted"
|
||||
:payload {:message "Built-in nodes can't be deleted."
|
||||
:i18n-key :node/built-in-cant-delete-error
|
||||
:type :error}})))
|
||||
(when (seq top-level-blocks)
|
||||
(let [from-property (:logseq.property/created-from-property start-block)
|
||||
|
||||
6
deps/outliner/src/logseq/outliner/page.cljs
vendored
6
deps/outliner/src/logseq/outliner/page.cljs
vendored
@@ -257,13 +257,15 @@
|
||||
(and (not class?) (not (every? ldb/internal-page? pages)))
|
||||
(throw (ex-info "Cannot create this page unless all parents are pages"
|
||||
{:type :notification
|
||||
:payload {:message "Cannot create this page unless all parents are pages"
|
||||
:payload {:message "Cannot create this page unless all parents are pages."
|
||||
:i18n-key :page.validation/parents-must-be-pages
|
||||
:type :warning}}))
|
||||
|
||||
(and class? (not (every? ldb/class? pages)))
|
||||
(throw (ex-info "Cannot create this tag unless all parents are tags"
|
||||
{:type :notification
|
||||
:payload {:message "Cannot create this tag unless all parents are tags"
|
||||
:payload {:message "Cannot create this tag unless all parents are tags."
|
||||
:i18n-key :class.validation/parents-must-be-tags
|
||||
:type :warning}}))
|
||||
|
||||
:else
|
||||
|
||||
31
deps/outliner/src/logseq/outliner/property.cljs
vendored
31
deps/outliner/src/logseq/outliner/property.cljs
vendored
@@ -61,7 +61,8 @@
|
||||
(throw (ex-info "Property is protected and can't be deleted"
|
||||
{:type :notification
|
||||
:payload {:type :error
|
||||
:message "Property is protected and can't be deleted"
|
||||
:message "Property is protected and can't be deleted."
|
||||
:i18n-key :property.validation/protected
|
||||
:entity-idents entity-idents
|
||||
:property property-ident}}))))
|
||||
|
||||
@@ -72,7 +73,9 @@
|
||||
ldb/private-tags))]
|
||||
(throw (ex-info "Can't remove private tags"
|
||||
{:type :notification
|
||||
:payload {:message (str "Can't remove private tags: " (string/join ", " private-tags))
|
||||
:payload {:message (str "Can't remove private tags: " (string/join ", " private-tags) ".")
|
||||
:i18n-key :class.validation/cant-remove-private-tags
|
||||
:i18n-args [(string/join ", " private-tags)]
|
||||
:type :error}
|
||||
:property-id :block/tags}))))
|
||||
|
||||
@@ -81,7 +84,8 @@
|
||||
(when (contains? db-malli-schema/required-properties property-ident)
|
||||
(throw (ex-info "Can't remove required property"
|
||||
{:type :notification
|
||||
:payload {:message "Can't remove required property"
|
||||
:payload {:message "Can't remove required property."
|
||||
:i18n-key :property.validation/cant-remove-required
|
||||
:type :error}
|
||||
:property-id property-ident}))))
|
||||
|
||||
@@ -144,7 +148,9 @@
|
||||
(or result
|
||||
(throw (ex-info (str "Can't convert \"" v-str "\" to a number")
|
||||
{:type :notification
|
||||
:payload {:message (str "Can't convert \"" v-str "\" to a number")
|
||||
:payload {:message (str "Can't convert \"" v-str "\" to a number.")
|
||||
:i18n-key :property.validation/cant-convert-to-number
|
||||
:i18n-args [v-str]
|
||||
:type :error}})))))
|
||||
|
||||
(defn ^:api convert-property-input-string
|
||||
@@ -218,6 +224,7 @@
|
||||
(throw (ex-info "Disallowed many to one conversion"
|
||||
{:type :notification
|
||||
:payload {:message "This property can't change from multiple values to one value because it has existing data."
|
||||
:i18n-key :property.validation/many-to-one
|
||||
:type :warning}})))
|
||||
(when (seq tx-data)
|
||||
(ldb/transact! conn tx-data {:outliner-op :update-property
|
||||
@@ -249,11 +256,13 @@
|
||||
(when-not (m/validate schema value)
|
||||
(let [errors (-> (m/explain schema value)
|
||||
(me/humanize))
|
||||
error-msg (str "\"" (:block/title property) "\"" " " (if (coll? errors) (first errors) errors))]
|
||||
error-msg (str "Property validation failed: \"" (:block/title property) "\" " (if (coll? errors) (first errors) errors))]
|
||||
(throw
|
||||
(ex-info "Schema validation failed"
|
||||
{:type :notification
|
||||
:payload {:message error-msg
|
||||
:i18n-key :property.validation/invalid-value
|
||||
:i18n-args [(:block/title property) (if (coll? errors) (first errors) errors)]
|
||||
:type :warning}
|
||||
:property (:db/ident property)
|
||||
:value value
|
||||
@@ -403,7 +412,8 @@
|
||||
(when (and ref? (= value (:db/id block)))
|
||||
(throw (ex-info "Can't set this block itself as own property value"
|
||||
{:type :notification
|
||||
:payload {:message "Can't set this block itself as own property value"
|
||||
:payload {:message "Can't set this block itself as own property value."
|
||||
:i18n-key :property.validation/cant-set-self-value
|
||||
:type :error}}))))
|
||||
|
||||
(defn batch-remove-property!
|
||||
@@ -638,6 +648,7 @@
|
||||
(throw (ex-info (str e)
|
||||
{:type :notification
|
||||
:payload {:message "Property failed to create. Please try a different property name."
|
||||
:i18n-key :property/create-error
|
||||
:type :error}})))))]
|
||||
(assert (qualified-keyword? db-ident))
|
||||
(when (and (contains? #{:checkbox} (:logseq.property/type schema))
|
||||
@@ -831,14 +842,17 @@
|
||||
(throw (ex-info "Closed value choice already exists"
|
||||
{:error :value-exists
|
||||
:type :notification
|
||||
:payload {:message "Choice already exists"
|
||||
:payload {:message "Choice already exists."
|
||||
:i18n-key :property.choice/already-exists
|
||||
:type :warning}}))
|
||||
|
||||
validate-message
|
||||
(throw (ex-info "Invalid property value"
|
||||
{:error :value-invalid
|
||||
:type :notification
|
||||
:payload {:message validate-message
|
||||
:payload {:message (str "Invalid choice \"" value' "\" for this property: " validate-message ".")
|
||||
:i18n-key :property.choice/invalid
|
||||
:i18n-args [value' validate-message]
|
||||
:type :warning}}))
|
||||
|
||||
(nil? resolved-value)
|
||||
@@ -894,6 +908,7 @@
|
||||
(throw (ex-info "The choice can't be deleted"
|
||||
{:type :notification
|
||||
:payload {:message "The choice can't be deleted because it's built-in."
|
||||
:i18n-key :property.choice/cant-delete-built-in
|
||||
:type :warning}}))
|
||||
(let [tx-data (conj (:tx-data (outliner-core/delete-blocks @conn [value-block] {}))
|
||||
(outliner-core/block-with-updated-at {:db/id (:db/id property)}))]
|
||||
|
||||
51
deps/outliner/src/logseq/outliner/validate.cljs
vendored
51
deps/outliner/src/logseq/outliner/validate.cljs
vendored
@@ -19,6 +19,7 @@
|
||||
(merge meta-m
|
||||
{:type :notification
|
||||
:payload {:message "Page name can't include \"#\"."
|
||||
:i18n-key :page.validation/name-no-hash
|
||||
:type :warning}}))))
|
||||
(when (and (string/includes? page-title ns-util/parent-char)
|
||||
(not (common-date/normalize-date page-title nil)))
|
||||
@@ -26,6 +27,7 @@
|
||||
(merge meta-m
|
||||
{:type :notification
|
||||
:payload {:message "Page name can't include \"/\"."
|
||||
:i18n-key :page.validation/name-no-slash
|
||||
:type :warning}})))))
|
||||
|
||||
(defn ^:api validate-page-title
|
||||
@@ -35,6 +37,7 @@
|
||||
(merge meta-m
|
||||
{:type :notification
|
||||
:payload {:message "Page name can't be blank."
|
||||
:i18n-key :page.validation/name-blank
|
||||
:type :warning}})))))
|
||||
|
||||
(defn- find-other-ids-with-title-and-tags
|
||||
@@ -91,11 +94,15 @@
|
||||
(throw (ex-info "Duplicate property"
|
||||
{:type :notification
|
||||
:payload {:message (str "Another property named " (pr-str new-title) " already exists.")
|
||||
:i18n-key :property.validation/duplicate
|
||||
:i18n-args [new-title]
|
||||
:type :warning}}))
|
||||
(ldb/class? entity)
|
||||
(throw (ex-info "Duplicate class"
|
||||
{:type :notification
|
||||
:payload {:message (str "Another tag named " (pr-str new-title) " already exists.")
|
||||
:i18n-key :class.validation/duplicate
|
||||
:i18n-args [new-title]
|
||||
:type :warning}}))
|
||||
:else
|
||||
(throw (ex-info "Duplicate page"
|
||||
@@ -103,6 +110,10 @@
|
||||
:payload {:message (str "Another page named " (pr-str new-title) " already exists for tags: "
|
||||
(string/join ", "
|
||||
(map (fn [id] (str "#" (:block/title (d/entity db id)))) common-tag-ids)))
|
||||
:i18n-key :page.validation/duplicate
|
||||
:i18n-args [new-title
|
||||
(string/join ", "
|
||||
(map (fn [id] (str "#" (:block/title (d/entity db id)))) common-tag-ids))]
|
||||
:type :warning}}))))))))
|
||||
|
||||
(defn ^:api validate-unique-by-name-and-tags
|
||||
@@ -122,6 +133,7 @@
|
||||
(throw (ex-info "Page can't be renamed to a journal"
|
||||
{:type :notification
|
||||
:payload {:message "This page can't be changed to a journal page"
|
||||
:i18n-key :journal/page-cant-convert-warning
|
||||
:type :warning}}))))
|
||||
|
||||
(defn validate-block-title
|
||||
@@ -139,6 +151,7 @@
|
||||
(merge meta-m
|
||||
{:type :notification
|
||||
:payload {:message "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['."
|
||||
:i18n-key :property.validation/invalid-name
|
||||
:type :error}}))))))
|
||||
|
||||
(defn- validate-extends-property-have-correct-type
|
||||
@@ -149,6 +162,7 @@
|
||||
(throw (ex-info "Can't extend this page since either it is not a tag or is extending from a page that is not a tag"
|
||||
{:type :notification
|
||||
:payload {:message "Can't extend this page since either it is not a tag or is extending from a page that is not a tag"
|
||||
:i18n-key :class.validation/invalid-extends-type
|
||||
:type :error}
|
||||
:blocks (map #(select-keys % [:db/id :block/title]) (remove ldb/class? child-ents))}))))
|
||||
|
||||
@@ -158,6 +172,7 @@
|
||||
(throw (ex-info "Can't change the extends of a built-in tag"
|
||||
{:type :notification
|
||||
:payload {:message "Can't change the extends of a built-in tag"
|
||||
:i18n-key :class.validation/built-in-extends-change
|
||||
:type :error}}))))
|
||||
|
||||
(defn- disallow-extends-cycle
|
||||
@@ -169,6 +184,7 @@
|
||||
(throw (ex-info "Extends cycle"
|
||||
{:type :notification
|
||||
:payload {:message "Tag extends cycle"
|
||||
:i18n-key :class.validation/extends-cycle
|
||||
:type :error
|
||||
:blocks (map #(select-keys % [:db/id :block/title]) [child])}}))))))
|
||||
|
||||
@@ -189,6 +205,8 @@
|
||||
(throw (ex-info (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
|
||||
{:type :notification
|
||||
:payload {:message (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
|
||||
:i18n-key :class.validation/tag-with-non-tag
|
||||
:i18n-args [(:block/title tag-ent)]
|
||||
:type :error}
|
||||
:property-value v})))))
|
||||
|
||||
@@ -201,14 +219,17 @@
|
||||
(and
|
||||
(every? (fn [id] (ldb/asset? (d/entity db id))) block-eids)
|
||||
(= :logseq.class/Asset (:db/ident (d/entity db v))))))
|
||||
(throw (ex-info (str (if delete? "Can't remove tag" "Can't set tag")
|
||||
" with built-in #" (:block/title (d/entity db v)))
|
||||
{:type :notification
|
||||
:payload {:message (str (if delete? "Can't remove tag" "Can't set tag")
|
||||
" with built-in #" (:block/title (d/entity db v)))
|
||||
:type :error}
|
||||
:property-id :block/tags
|
||||
:property-value v}))))
|
||||
(let [tag-title (:block/title (d/entity db v))]
|
||||
(throw (ex-info (str (if delete? "Can't remove tag" "Can't set tag")
|
||||
" with built-in #" tag-title)
|
||||
{:type :notification
|
||||
:payload {:message (str (if delete? "Can't remove tag" "Can't set tag")
|
||||
" with built-in #" tag-title)
|
||||
:i18n-key (if delete? :class.validation/cant-remove-tag-built-in :class.validation/cant-set-tag-built-in)
|
||||
:i18n-args [tag-title]
|
||||
:type :error}
|
||||
:property-id :block/tags
|
||||
:property-value v})))))
|
||||
|
||||
(defn- disallow-tagging-a-built-in-entity
|
||||
[db block-eids & {:keys [delete?]}]
|
||||
@@ -219,6 +240,8 @@
|
||||
{:type :notification
|
||||
:payload {:message (str (if delete? "Can't remove tag" "Can't add tag")
|
||||
" on built-in " (pr-str (:block/title built-in-ent)))
|
||||
:i18n-key (if delete? :class.validation/cant-remove-tag-on-built-in :class.validation/cant-add-tag-on-built-in)
|
||||
:i18n-args [(:block/title built-in-ent)]
|
||||
:type :error}}))))
|
||||
|
||||
(defn- disallow-removing-page-tag
|
||||
@@ -239,6 +262,8 @@
|
||||
:payload
|
||||
{:message (str "Page " (pr-str (:block/title entity)) " cannot be converted to a block")
|
||||
:type :error
|
||||
:i18n-key :page.convert/cant-be-block
|
||||
:i18n-args [(:block/title entity)]
|
||||
:entity (into {} entity)
|
||||
:property :block/tags}}))
|
||||
(= (:db/id library-page) (:db/id (:block/parent entity)))
|
||||
@@ -247,6 +272,8 @@
|
||||
:payload
|
||||
{:message (str "Page " (pr-str (:block/title entity)) " cannot be converted to a block, please move it to another page first")
|
||||
:type :error
|
||||
:i18n-key :page.convert/cant-be-block-move-first
|
||||
:i18n-args [(:block/title entity)]
|
||||
:entity (into {} entity)
|
||||
:property :block/tags}}))
|
||||
(some entity-util/page? (:block/_parent entity))
|
||||
@@ -255,6 +282,8 @@
|
||||
:payload
|
||||
{:message (str "Page " (pr-str (:block/title entity)) " cannot be converted to a block because it has page children")
|
||||
:type :error
|
||||
:i18n-key :page.convert/cant-be-block-has-children
|
||||
:i18n-args [(:block/title entity)]
|
||||
:entity (into {} entity)
|
||||
:property :block/tags}})))))))))
|
||||
|
||||
@@ -274,10 +303,14 @@
|
||||
(:logseq.property/created-from-property block))
|
||||
(let [message (if (:logseq.property/created-from-property block)
|
||||
"Can't convert property value to page."
|
||||
"Can't convert this block to page since its parent is not a page.")]
|
||||
"Can't convert this block to page since its parent is not a page.")
|
||||
i18n-key (if (:logseq.property/created-from-property block)
|
||||
:page.convert/property-value-to-page
|
||||
:page.convert/block-parent-not-page)]
|
||||
(throw (ex-info message
|
||||
{:type :notification
|
||||
:payload {:message message
|
||||
:i18n-key i18n-key
|
||||
:type :error
|
||||
:block (into {} block)}})))))))))
|
||||
|
||||
|
||||
23
deps/shui/src/logseq/shui/dialog/core.cljs
vendored
23
deps/shui/src/logseq/shui/dialog/core.cljs
vendored
@@ -173,8 +173,9 @@
|
||||
|
||||
(rum/defc alert-inner
|
||||
[config]
|
||||
(let [{:keys [id title description content footer deferred open?]} config
|
||||
props (dissoc config :id :title :description :content :footer :deferred :open? :alert?)]
|
||||
(let [{:keys [id title description content footer deferred open? ok-label]} config
|
||||
props (dissoc config :id :title :description :content :footer :deferred :open? :alert? :ok-label)
|
||||
ok-label (or ok-label "OK")]
|
||||
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
@@ -205,15 +206,18 @@
|
||||
(base/button
|
||||
{:key "ok"
|
||||
:on-click #(do (close!) (p/resolve! deferred true))
|
||||
:size :sm} "OK")]))))))
|
||||
:size :sm} ok-label)]))))))
|
||||
|
||||
(rum/defc confirm-inner
|
||||
[config]
|
||||
(let [{:keys [id deferred outside-cancel? data-reminder]} config
|
||||
(let [{:keys [id deferred outside-cancel? data-reminder data-reminder-label
|
||||
cancel-label ok-label]} config
|
||||
reminder? (boolean (and id data-reminder))
|
||||
[ready?, set-ready!] (rum/use-state (not reminder?))
|
||||
*ok-ref (rum/use-ref nil)
|
||||
*reminder-ref (rum/use-ref nil)]
|
||||
*reminder-ref (rum/use-ref nil)
|
||||
cancel-label (or cancel-label "Cancel")
|
||||
ok-label (or ok-label "OK")]
|
||||
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
@@ -245,17 +249,16 @@
|
||||
:footer
|
||||
[:<>
|
||||
[:span.flex.items-center.pt-1
|
||||
(when (and id data-reminder)
|
||||
(when (and id data-reminder data-reminder-label)
|
||||
[:label.flex.items-center.gap-1.text-sm
|
||||
(form/checkbox {:ref *reminder-ref})
|
||||
[:span.opacity-50 "Don't remind me again"]])]
|
||||
[:span.opacity-50 data-reminder-label]])]
|
||||
[:span.flex.gap-2
|
||||
(base/button
|
||||
{:key "cancel"
|
||||
:on-click #(do (close!) (p/reject! deferred false))
|
||||
:variant :outline
|
||||
:size :sm}
|
||||
"Cancel")
|
||||
:size :sm} cancel-label)
|
||||
(base/button
|
||||
{:key "ok"
|
||||
:ref *ok-ref
|
||||
@@ -265,7 +268,7 @@
|
||||
(js/localStorage.setItem (str id) (js/Date.now))))
|
||||
(close!)
|
||||
(p/resolve! deferred true))
|
||||
:size :sm} "OK")]])))))
|
||||
:size :sm} ok-label)]])))))
|
||||
|
||||
(rum/defc install-modals
|
||||
< rum/static
|
||||
|
||||
11
deps/shui/src/logseq/shui/select/multi.cljs
vendored
11
deps/shui/src/logseq/shui/select/multi.cljs
vendored
@@ -2,7 +2,7 @@
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.shui.form.core :as form]
|
||||
[logseq.shui.hooks :as hooks]
|
||||
[logseq.shui.popup.core :as popup]
|
||||
[logseq.shui.popup.core :as shui-popup]
|
||||
[rum.core :as rum]))
|
||||
|
||||
(defn- get-k [item]
|
||||
@@ -35,8 +35,7 @@
|
||||
[:div.search-input
|
||||
{:ref *el}
|
||||
(form/input
|
||||
(merge {:placeholder "search"
|
||||
:on-key-up #(case (.-key %)
|
||||
(merge {:on-key-up #(case (.-key %)
|
||||
"ArrowDown" (set-down! (inc down))
|
||||
"ArrowUp" nil
|
||||
"Enter" (when (fn? on-enter) (on-enter))
|
||||
@@ -57,10 +56,11 @@
|
||||
[items selected-items & {:keys [on-chosen item-render value-render
|
||||
head-render foot-render open? close!
|
||||
search-enabled? search-key on-search-key-change
|
||||
search-input-placeholder
|
||||
search-fn search-key-render
|
||||
item-props content-props]}]
|
||||
(let [x-content popup/dropdown-menu-content
|
||||
x-item popup/dropdown-menu-item
|
||||
(let [x-content shui-popup/dropdown-menu-content
|
||||
x-item shui-popup/dropdown-menu-item
|
||||
*head-ref (rum/use-ref nil)
|
||||
[search-key1 set-search-key!] (rum/use-state search-key)
|
||||
search-key1' (some-> search-key1 (string/trim) (string/lower-case))
|
||||
@@ -118,6 +118,7 @@
|
||||
(when search-enabled?
|
||||
(search-input
|
||||
{:value search-key1
|
||||
:placeholder (or search-input-placeholder "")
|
||||
:on-key-down (fn [^js e]
|
||||
(.stopPropagation e)
|
||||
(case (.-key e)
|
||||
|
||||
@@ -1,109 +1,138 @@
|
||||
## Intro
|
||||
|
||||
Thanks for your interest in improving our translations! This document provides
|
||||
details on how to contribute to a translation. This document assumes you can run
|
||||
commandline tools, know how to switch languages within Logseq and basic
|
||||
Clojurescript familiarity. We use [tongue](https://github.com/tonsky/tongue), a
|
||||
most excellent library, for our translations.
|
||||
Thanks for helping improve Logseq translations.
|
||||
|
||||
This guide is for contributors who translate existing UI text or add missing
|
||||
translations for a locale. It is not the guide for changing application code,
|
||||
inventing dictionary keys, or rewriting the English source text in
|
||||
`src/resources/dicts/en.edn`.
|
||||
|
||||
If the English wording or key name is wrong, ask a developer to update
|
||||
`en.edn` and follow [the key naming guide](i18n-key-naming.md).
|
||||
|
||||
## Setup
|
||||
|
||||
In order to run the commands in this doc, you will need to install
|
||||
To run the commands in this doc, install
|
||||
[Babashka](https://github.com/babashka/babashka#installation).
|
||||
|
||||
## Where to Contribute
|
||||
## Where Translations Live
|
||||
|
||||
Language translations are under,
|
||||
[src/resources/dicts/](https://github.com/logseq/logseq/blob/master/src/resources/dicts/) with each language having its own file. For example, the es locale is in `es.edn`.
|
||||
Translation dictionaries live under
|
||||
[src/resources/dicts/](https://github.com/logseq/logseq/blob/master/src/resources/dicts/).
|
||||
Each locale has its own EDN file, for example `es.edn`.
|
||||
|
||||
## Language Overview
|
||||
`en.edn` is the source of truth for keys and English text. Most translation
|
||||
contributors only need to edit their locale file.
|
||||
|
||||
First, let's get an overview of Logseq's languages and how many translations your
|
||||
language has compared to others:
|
||||
## Find Missing Translations
|
||||
|
||||
```shell
|
||||
$ bb lang:list
|
||||
|
||||
| :locale | :percent-translated | :translation-count | :language |
|
||||
|----------+---------------------+--------------------+------------------------|
|
||||
| :es | 100 | 492 | Español |
|
||||
| :tr | 100 | 492 | Türkçe |
|
||||
| :en | 100 | 492 | English |
|
||||
| :uk | 95 | 466 | Українська |
|
||||
| :ru | 95 | 466 | Русский |
|
||||
| :ko | 93 | 459 | 한국어 |
|
||||
| :de | 93 | 459 | Deutsch |
|
||||
| :fr | 92 | 453 | Français |
|
||||
| :pt-PT | 92 | 453 | Português (Europeu) |
|
||||
| :pt-BR | 92 | 451 | Português (Brasileiro) |
|
||||
| :sk | 90 | 445 | Slovenčina |
|
||||
| :zh-CN | 90 | 441 | 简体中文 |
|
||||
| :nb-NO | 75 | 370 | Norsk (bokmål) |
|
||||
| :ja | 75 | 368 | 日本語 |
|
||||
| :pl | 72 | 353 | Polski |
|
||||
| :nl | 72 | 353 | Dutch (Nederlands) |
|
||||
| :zh-Hant | 71 | 349 | 繁體中文 |
|
||||
| :it | 71 | 349 | Italiano |
|
||||
| :af | 22 | 106 | Afrikaans |
|
||||
Total: 19
|
||||
```
|
||||
|
||||
Let's try to get your language translated as close to 100% as you can!
|
||||
|
||||
## Edit a Language
|
||||
|
||||
To see what translations are missing for your language, let's run a command using `es` as the example language:
|
||||
|
||||
```shell
|
||||
$ bb lang:missing es
|
||||
| :translation-key | :string-to-translate | :file |
|
||||
|---------------------------------------+-------------------------------------------------------+---------------|
|
||||
| :command.editor/toggle-number-list | Toggle number list | dicts/es.edn |
|
||||
...
|
||||
```
|
||||
|
||||
Now, manually, add keys for your language to the translation files, save and rerun the above command.
|
||||
Over time you're aiming to have this list drop to zero. Since this process can be tedious, there is an option to print the untranslated strings to copy and paste them to the files:
|
||||
To see the overall translation status of every locale:
|
||||
|
||||
```sh
|
||||
# When pasting this content, be sure to update the indentation to match the file
|
||||
$ bb lang:missing es --copy
|
||||
|
||||
;; For dicts/es.edn
|
||||
:command.editor/toggle-number-list "Toggle number list"
|
||||
...
|
||||
bb lang:list
|
||||
```
|
||||
|
||||
Almost all translations are small. The only exceptions to this are keys that point to files e.g. their value is prefixed with `#resource`. TODO: Update when new tutorials are written
|
||||
That table includes `:untranslated-count`, which shows how many English keys
|
||||
are still missing for each locale, and `:same-as-en-count`, which helps you
|
||||
spot locales that still contain entries copied from English.
|
||||
|
||||
To see which entries are missing for one locale, use `es` as an example:
|
||||
|
||||
```sh
|
||||
bb lang:missing es
|
||||
```
|
||||
|
||||
To print copy/paste-ready entries:
|
||||
|
||||
```sh
|
||||
bb lang:missing es --copy
|
||||
```
|
||||
|
||||
That command prints the missing keys and the current English value so you can
|
||||
paste them into your locale file and translate them there.
|
||||
|
||||
## Find Entries Still Matching English
|
||||
|
||||
To list them for one locale, use `es` as an example:
|
||||
|
||||
```sh
|
||||
bb lang:pseudo es
|
||||
```
|
||||
|
||||
This is a review tool, not a hard error. Some entries may legitimately match
|
||||
English, but many are unfinished translations copied from `en.edn`.
|
||||
|
||||
## Edit a Locale
|
||||
|
||||
1. Run `bb lang:missing <locale>`.
|
||||
2. Add the missing keys to `src/resources/dicts/<locale>.edn`.
|
||||
3. Save the file.
|
||||
4. Run `bb lang:missing <locale>` again until the list is empty or contains
|
||||
only entries you want to leave for later.
|
||||
|
||||
Missing keys are allowed. Logseq falls back to English automatically, so do not
|
||||
copy English into your locale file just to make the list shorter.
|
||||
|
||||
### Editing Tips
|
||||
|
||||
* Some translations may include punctuation like `:` or `!`. When translating them, please use the punctuation that makes the most sense for your language as you don't have to follow the English ones.
|
||||
* Some translations may include arguments/interpolations e.g. `{1}`. If you see them in a translation, be sure to include them. These arguments are substituted in the string and are usually used for something the app needs to calculate e.g. a number. See [these docs](https://github.com/tonsky/tongue#interpolation) for more examples.
|
||||
* Rarely, a translation is a function that calls code and look like `(fn ... )`
|
||||
* The logic for these fns must be simple and can only use the following fns: `str`, `when`, `if` and `=`.
|
||||
* These fn translations are usually used to handle pluralization or handle formatted text by returning [hiccup-style HTML](https://github.com/weavejester/hiccup#syntax). For example, a hiccup style translation would look like `(fn [] [:div "FOO"])`. See `:on-boarding/main-title` for more examples.
|
||||
- Translate the complete sentence or label owned by the key. Do not rename keys
|
||||
or split one sentence across multiple keys.
|
||||
- If the English value is a plain string, keep your locale value a plain
|
||||
string.
|
||||
- Keep placeholders exactly aligned with English, for example `{1}` and `{2}`.
|
||||
- If the English value uses hiccup or `(fn ...)`, keep the same outer shape and
|
||||
translate only the user-visible strings inside it. If changing that structure
|
||||
seems necessary, ask a developer for help.
|
||||
- Preserve emoji and icon glyphs from `en.edn` exactly, but use punctuation
|
||||
that is natural for your language.
|
||||
- If a sentence is already correct in your language without plural logic, use a
|
||||
plain string. Do not add function logic just because English does.
|
||||
|
||||
## Fix Mistakes
|
||||
|
||||
There is a lint command to catch common translation mistakes - `bb
|
||||
lang:validate-translations`. This runs for all contribution pull requests so
|
||||
you'll need to ensure it doesn't fail. Mistakes that it catches:
|
||||
Run this before submitting translation changes:
|
||||
|
||||
* Adding translation entries for nonexistent entries in English.
|
||||
* Most common mistake is mistyping an entry name
|
||||
* Adding English entries for translations that don't exist in the UI.
|
||||
* Adding translation entries that are just duplicates of the English entry.
|
||||
* This catches contributors copying entries from English and then forgetting to translate. Sometimes you do want to have the translation be the same. For this case, add an entry to `allowed-duplicates` in
|
||||
[lang.clj](https://github.com/logseq/logseq/blob/master/scripts/src/logseq/tasks/lang.clj) for your language
|
||||
with a list of duplicated entries e.g. `:nb-NO #{:port ...}`.
|
||||
```sh
|
||||
bb lang:validate-translations
|
||||
```
|
||||
|
||||
Nonexistent and some invalid entries can be removed by running `bb lang:validate-translations --fix`.
|
||||
It checks for:
|
||||
|
||||
- locale keys that do not exist in `en.edn`
|
||||
- dictionary keys that are no longer used
|
||||
- placeholder mismatches such as `{1}` vs `{2}`
|
||||
- locale entries that no longer match an English rich-translation shape
|
||||
|
||||
`bb lang:validate-translations` does not flag entries that still match English.
|
||||
Use `bb lang:pseudo <locale>` when you want to review those separately.
|
||||
|
||||
To remove stale or invalid keys automatically:
|
||||
|
||||
```sh
|
||||
bb lang:validate-translations --fix
|
||||
```
|
||||
|
||||
`--fix` removes invalid or unused keys. It does not repair placeholder mistakes
|
||||
or rewrite rich translations for you.
|
||||
|
||||
After editing dictionary files, run:
|
||||
|
||||
```sh
|
||||
bb lang:format-dicts
|
||||
```
|
||||
|
||||
This restores the repo's canonical key ordering and namespace spacing.
|
||||
|
||||
You do not need `bb lang:lint-hardcoded` for translation-only work. That
|
||||
command is for developers who are editing UI code.
|
||||
|
||||
## Add a Language
|
||||
|
||||
To add a new language:
|
||||
* Add an entry to `frontend.dicts/languages`
|
||||
* Create a new file under `src/resources/dicts/` and name the file the same as the locale e.g. zz.edn for a hypothetical zz locale.
|
||||
* Add an entry in `frontend.dicts/dicts` referencing the file you created.
|
||||
* Then start translating for your language and adding entries in your language's EDN file using the `bb lang:missing` workflow.
|
||||
|
||||
1. Add an entry to `frontend.dicts/languages`.
|
||||
2. Create a new file under `src/resources/dicts/` and name it after the locale,
|
||||
for example `zz.edn`.
|
||||
3. Add that file to `frontend.dicts/dicts`.
|
||||
4. Use the `bb lang:missing <locale>` workflow to populate translations.
|
||||
5. Run `bb lang:validate-translations` and `bb lang:format-dicts`.
|
||||
|
||||
@@ -76,23 +76,90 @@ error if it detects an invalid query.
|
||||
|
||||
### Translations
|
||||
|
||||
We use [tongue](https://github.com/tonsky/tongue), a simple and effective
|
||||
library, for translations. We have a couple bb tasks for working with
|
||||
translations under `lang:` e.g. `bb lang:list`. See [the translator
|
||||
guide](./contributing-to-translations.md) for usage.
|
||||
We use [tongue](https://github.com/tonsky/tongue) for translations.
|
||||
|
||||
One useful task for reviewers (us) and contributors alike, is `bb
|
||||
lang:validate-translations` which catches [common
|
||||
mistakes](./contributing-to-translations.md#fix-mistakes)). When reviewing
|
||||
translations here are some things to keep in mind:
|
||||
Responsibilities are split across a few files:
|
||||
|
||||
* Punctuation and delimiting characters (e.g. `:`, `:`, `?`) should be part of
|
||||
the translatable string. Those characters and their position may vary depending on the language.
|
||||
* Translations usually return strings but they can return hiccup vectors with a
|
||||
fn translation. Hiccup vectors are needed when word order matters for a
|
||||
translation and formatting is involved. See [this 3 word Turkish
|
||||
example](https://github.com/logseq/logseq/commit/1d932f07c4a0aad44606da6df03a432fe8421480#r118971415).
|
||||
* Translations can be anonymous fns with arguments for interpolating strings. Fns should be simple and only include the following fns: `str`, `when`, `if` and `=`.
|
||||
* [docs/contributing-to-translations.md](./contributing-to-translations.md) is
|
||||
for locale contributors.
|
||||
* [docs/i18n-key-naming.md](./i18n-key-naming.md) is for naming and reusing
|
||||
keys in `src/resources/dicts/en.edn`.
|
||||
* [.i18n-lint.toml](../.i18n-lint.toml) is the source of truth for hardcoded UI
|
||||
text lint scope, translatable helpers/attributes, exclusions, and allowlists.
|
||||
|
||||
#### What must be internationalized
|
||||
|
||||
Inside the scope defined by `.i18n-lint.toml`, all user-visible UI text must be
|
||||
internationalized.
|
||||
|
||||
Exceptions:
|
||||
|
||||
* Console output does not need translation.
|
||||
* Keep out-of-scope developer-only `(Dev)` labels next to the developer
|
||||
UI/command definition; do not add them to translation dictionaries.
|
||||
|
||||
If you introduce a new UI helper, alert API, UI namespace, translatable
|
||||
attribute, or other shipped UI surface, update `.i18n-lint.toml` so the lint
|
||||
continues to cover it.
|
||||
|
||||
#### Translation helpers
|
||||
|
||||
All translation helpers live in
|
||||
`src/main/frontend/context/i18n.cljs`. Do not add parallel ad hoc i18n helpers
|
||||
elsewhere.
|
||||
|
||||
| Helper | Use for |
|
||||
|---|---|
|
||||
| `t` | Standard translation with preferred-locale lookup |
|
||||
| `tt` | Try multiple keys and return the first existing translation |
|
||||
| `t-en` | Force English output, for example when UI text also needs an English console copy |
|
||||
| `interpolate-rich-text` / `interpolate-rich-text-node` | Replace placeholders with rich-text or hiccup fragments |
|
||||
| `interpolate-sentence` | Keep a full sentence in one key while inserting placeholders and inline links |
|
||||
| `replace-newlines-with-br` | Render translated newline characters as `[:br]` nodes |
|
||||
| `locale-join-rich-text` / `locale-join-rich-text-node` | Join rich fragments with locale-aware separators |
|
||||
| `locale-format-number` / `locale-format-date` / `locale-format-time` | Format dynamic numbers and dates before passing them into translations |
|
||||
|
||||
#### Developer workflow
|
||||
|
||||
1. Use `.i18n-lint.toml` to decide whether the text is in i18n scope.
|
||||
2. Search `src/resources/dicts/en.edn` for an existing key with the same
|
||||
semantic owner and textual role.
|
||||
3. If no exact match exists, follow
|
||||
[the key naming guide](./i18n-key-naming.md) and add the English source text
|
||||
to `en.edn`.
|
||||
4. Add non-English locale entries only when you are also providing actual
|
||||
translations. When renaming or removing keys, clean up stale locale keys.
|
||||
5. Replace the literal with the appropriate helper from
|
||||
`frontend.context.i18n`.
|
||||
|
||||
Recommended checks:
|
||||
|
||||
```sh
|
||||
bb lang:validate-translations
|
||||
bb lang:lint-hardcoded --git-changed
|
||||
bb lang:format-dicts
|
||||
```
|
||||
|
||||
`bb lang:format-dicts` is the repo-owned formatter for dictionary key ordering
|
||||
and namespace spacing. Run it after editing dict files.
|
||||
|
||||
#### Content rules
|
||||
|
||||
* Keep each translation as complete as possible. Do not assemble sentences from
|
||||
fragments in the caller.
|
||||
* For plain dynamic text, use placeholders like `{1}` and pre-format arguments
|
||||
in the caller before passing them to `t`.
|
||||
* Function-valued translations are allowed only when a locale needs real logic
|
||||
or rich-text hiccup output. When functions are necessary, only `str`, `when`,
|
||||
`if`, and `=` are allowed inside the function body.
|
||||
* Keep rich text in a single translation entry. Do not split one sentence
|
||||
across multiple keys.
|
||||
* Non-English locale files should contain only actual translations. Do not copy
|
||||
English values just to fill gaps; Tongue falls back to `:en`.
|
||||
* Preserve emoji/icon glyphs from `en.edn` exactly, and use punctuation natural
|
||||
to each locale.
|
||||
* Pluralization is locale-specific. Do not force English singular/plural rules
|
||||
onto other languages.
|
||||
|
||||
### Spell Checker
|
||||
|
||||
@@ -444,8 +511,9 @@ These tasks are specific to database graphs. For these tasks there is a one time
|
||||
### Dev Commands
|
||||
|
||||
In the app, you can enable Dev commands under `Settings > Advanced > Developer
|
||||
mode`. Then search for commands starting with `(Dev)`. Commands include
|
||||
inspectors for block/page data and AST.
|
||||
mode`. Then search for commands labeled with `(Dev)`. Those labels are
|
||||
intentionally hardcoded English developer-only labels, not translation keys.
|
||||
Commands include inspectors for block/page data and AST.
|
||||
|
||||
### Desktop Developer Tools
|
||||
|
||||
|
||||
710
docs/i18n-key-naming.md
Normal file
710
docs/i18n-key-naming.md
Normal file
@@ -0,0 +1,710 @@
|
||||
# Logseq i18n Key Naming Standard
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines how to name new i18n keys in `src/resources/dicts/en.edn`.
|
||||
|
||||
Goal: given any new user-facing string, this document should let you determine
|
||||
its key name directly.
|
||||
|
||||
Secondary goal: keep prefixes converged. A name that is slightly less "pure" but
|
||||
stays inside an existing owner is usually better than creating a new low-density
|
||||
root or singleton dotted subdomain.
|
||||
|
||||
This standard is intended to be deterministic. After applying it, you should be
|
||||
able to choose one reasonable key name. If you still cannot determine the name,
|
||||
treat that as a gap in the standard rather than guessing:
|
||||
|
||||
- AI agents must stop and ask for human guidance.
|
||||
- Developers are encouraged to report the gap to the Logseq team so the standard
|
||||
can be clarified.
|
||||
|
||||
Audience:
|
||||
|
||||
- developers adding or renaming English keys
|
||||
- AI agents reviewing or generating i18n changes
|
||||
|
||||
Non-English locale contributors should not invent or rename keys. They should
|
||||
use [contributing-to-translations.md](contributing-to-translations.md) instead.
|
||||
|
||||
This document is only about key naming and reuse. Rules for placeholders,
|
||||
hiccup, punctuation, locale fallback, linting, and helper selection live in
|
||||
[dev-practices.md](dev-practices.md).
|
||||
|
||||
Developer-only `(Dev)` labels are out of scope for this document. Keep them as
|
||||
inline English labels next to the developer UI/command definition instead of
|
||||
adding translation keys.
|
||||
|
||||
## Key Shape
|
||||
|
||||
Use this shape:
|
||||
|
||||
```clojure
|
||||
:<root>[.<subdomain>]/<leaf>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `<root>` chooses the semantic owner
|
||||
- `<subdomain>` is optional and used only for a stable subfeature, workflow, or
|
||||
representation
|
||||
- `<leaf>` describes the text's role inside that owner
|
||||
- `<root>` names are singular semantic owners
|
||||
- All segments use kebab-case
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:ui/close
|
||||
:page.delete/confirm-title
|
||||
:nav.all-pages/title
|
||||
:settings.editor/show-brackets
|
||||
:plugin.install-from-file/success
|
||||
:view.table/sort-ascending
|
||||
:cmdk.action/open
|
||||
:mobile.toolbar/undo
|
||||
```
|
||||
|
||||
## Before Naming a New Key
|
||||
|
||||
1. Search `src/resources/dicts/en.edn`.
|
||||
2. Reuse a key only when both match:
|
||||
- semantic owner
|
||||
- textual role
|
||||
3. If the English text matches but the owner or role differs, create a new key.
|
||||
|
||||
Examples:
|
||||
|
||||
- toolbar `"Bold"` and command `"Bold"` are different keys
|
||||
- dialog `"Close"` and window `"Close"` are different keys
|
||||
- reusable `"Copied!"` feedback may share a `notification/*` key only when it is
|
||||
intentionally cross-domain
|
||||
|
||||
## Step 1: Choose the Owner
|
||||
|
||||
The namespace must be chosen by owner, not by file path, not by component name,
|
||||
and not by where the text happens to be rendered.
|
||||
|
||||
There are only 5 owner classes.
|
||||
|
||||
### 1. Interaction Systems
|
||||
|
||||
Use when the text belongs to an interaction registry or interaction subsystem.
|
||||
|
||||
| Namespace | Use for |
|
||||
|---|---|
|
||||
| `command.*` | Built-in command descriptions |
|
||||
| `shortcut.category` | Shortcut help categories |
|
||||
| `keymap` | Keybinding editor text |
|
||||
| `cmdk` | Command palette text |
|
||||
|
||||
Use this class when:
|
||||
|
||||
- the text is attached to a command id
|
||||
- the text names a shortcut group
|
||||
- the text belongs to rebinding/conflict/chord UI
|
||||
- the text belongs only to command palette behavior
|
||||
|
||||
For `command.*` keys:
|
||||
|
||||
- the subdomain is the command group name
|
||||
- for built-in command descriptions, mirror the command id namespace even when
|
||||
the command opens another surface or workflow; the owner is still the command
|
||||
registry entry
|
||||
- use a stable semantic group name, not an implementation placeholder
|
||||
- avoid new opaque or self-referential groups such as `command.command`
|
||||
- `command.command-palette/*` is valid for descriptions attached to
|
||||
`:command-palette/*` ids; reserve `cmdk.*` for command-palette UI copy itself
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:command.editor/bold
|
||||
:command.graph/open
|
||||
:command.page/toggle-favorite
|
||||
:command.shell/run
|
||||
:shortcut.category/block-editing
|
||||
:keymap/search-placeholder
|
||||
:cmdk.action/open
|
||||
```
|
||||
|
||||
### 2. Shared Primitives
|
||||
|
||||
Use only when the wording keeps the same meaning across unrelated domains.
|
||||
|
||||
| Namespace | Use for |
|
||||
|---|---|
|
||||
| `ui` | Generic actions and states |
|
||||
| `nav` | Global destinations and route-level constraints |
|
||||
| `notification` | Reusable cross-feature feedback and notification-center shell controls |
|
||||
| `search` | Generic search vocabulary |
|
||||
| `select` | Generic picker vocabulary |
|
||||
| `format` | Formatting vocabulary |
|
||||
| `color` | Color vocabulary |
|
||||
|
||||
Qualification rules:
|
||||
|
||||
- removing product context does not change the meaning
|
||||
- the wording can be reused in multiple unrelated domains
|
||||
- the wording does not name a specific entity or workflow
|
||||
- the wording stays natural with the same grammatical role across locales; if
|
||||
callers need different inflection, gender, number, part of speech, or
|
||||
label-vs-status behavior, do not force one shared key just because English
|
||||
matches
|
||||
- if the text names a product entity such as `graph`, `page`, or `server`, use
|
||||
that product domain instead
|
||||
- do not use `notification` for feature-specific toast text just because it is
|
||||
shown via `notification/show!`; the delivery mechanism does not determine
|
||||
owner
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:ui/close
|
||||
:ui/save
|
||||
:nav/home
|
||||
:notification/copied
|
||||
:search/no-result
|
||||
:select/default-prompt
|
||||
:format/bold
|
||||
:color/red
|
||||
```
|
||||
|
||||
### 3. Product Domains
|
||||
|
||||
This is the default class. Most keys should be here.
|
||||
|
||||
Use when the text belongs to a first-class product feature, entity, or workflow.
|
||||
|
||||
The 5 groups below are taxonomy buckets, not a priority order. Do not infer
|
||||
ownership priority from subsection order. When multiple product domains seem
|
||||
plausible, use the conflict rules below.
|
||||
|
||||
#### 3.1 Workspace and content domains
|
||||
|
||||
- `graph`
|
||||
- `file`
|
||||
- `page`
|
||||
- `block`
|
||||
- `node`
|
||||
- `journal`
|
||||
- `library`
|
||||
- `date`
|
||||
- `editor`
|
||||
- `reference`
|
||||
- `property`
|
||||
- `class`
|
||||
- `view`
|
||||
- `query`
|
||||
- `icon`
|
||||
- `asset`
|
||||
- `pdf`
|
||||
- `flashcard`
|
||||
|
||||
#### 3.2 Data movement and publishing domains
|
||||
|
||||
- `import`
|
||||
- `export`
|
||||
- `publish`
|
||||
|
||||
#### 3.3 Customization, AI, and extensibility domains
|
||||
|
||||
- `settings`
|
||||
- `theme`
|
||||
- `plugin`
|
||||
- `ai`
|
||||
- `youtube`
|
||||
- `zotero`
|
||||
- `server`
|
||||
- `storage`
|
||||
|
||||
`ai` is a reserved owner for future built-in AI feature UI. Current AI-related
|
||||
settings copy may still live under `settings.ai/*`.
|
||||
|
||||
#### 3.4 Account, cloud, and security domains
|
||||
|
||||
- `account`
|
||||
- `sync`
|
||||
- `collaboration`
|
||||
- `encryption`
|
||||
|
||||
#### 3.5 Support, diagnostics, and lifecycle domains
|
||||
|
||||
- `onboarding`
|
||||
- `help`
|
||||
- `bug-report`
|
||||
- `shell`
|
||||
- `profiler`
|
||||
- `updater`
|
||||
- `deeplink`
|
||||
|
||||
#### Product domain boundaries
|
||||
|
||||
Use this table to resolve common conflicts.
|
||||
|
||||
| Namespace | Owns | Does not own |
|
||||
|---|---|---|
|
||||
| `graph` | Graph lifecycle, graph switching, graph-level state, graph visualization entry points | Individual pages, blocks, raw files |
|
||||
| `file` | Raw file browser, file metadata, file-level errors | Graph switching, page semantics, import workflow |
|
||||
| `page` | Page metadata and page-level workflows | Active editing mechanics |
|
||||
| `block` | Block as stored content entity | Active editing session behavior |
|
||||
| `node` | Generic node vocabulary intentionally shared across page/block/tag/property-like entities | Page-only, block-only, property-only, or class-only workflows |
|
||||
| `editor` | Active authoring behavior: selection, cursor actions, paste, heading changes, inline creation | Command registry text, page metadata, property schema |
|
||||
| `journal` | Journal-only behavior | Generic page behavior |
|
||||
| `library` | Library page copy and library-specific add/remove flows | Generic page search or generic page metadata outside the library feature |
|
||||
| `date` | Relative-date vocabulary, natural-language date phrases, and date-only labeling/parsing copy | Journal-only workflows, editor command registries |
|
||||
| `reference` | Backlinks, linked references, block refs, page refs | Generic search or editing text |
|
||||
| `property` | Property schema, values, choices, dialogs, validation | Query semantics and result presentation |
|
||||
| `class` | Class/tag schema and class-specific configuration | Generic property schema |
|
||||
| `query` | Query definition, query source, query inputs, live-query semantics | Table sorting, grouping, row selection |
|
||||
| `view` | Result presentation, table controls, grouping, sorting, columns, selection, representation modes | Query source semantics or property schema |
|
||||
| `icon` | Icon picker, emoji/icon browsing, icon-search tabs and counts | Generic search vocabulary or generic select/picker wording outside icon picking |
|
||||
| `asset` | Attachments and embedded media assets | Generic file browser or export |
|
||||
| `pdf` | PDF viewer and PDF-specific reading/annotation behavior | Generic asset browsing or generic reference behavior |
|
||||
| `flashcard` | Card review, card study flow, card-specific review UI | Generic editor actions or generic query/view controls |
|
||||
| `import` | Import workflows, import source parsing, import options, and import-specific validation/feedback | Export, publish, or generic file browser wording |
|
||||
| `export` | Export workflows, export format/options, export progress, and export-specific feedback | Import flows, publish lifecycle, or generic file browser wording |
|
||||
| `publish` | Publish and unpublish flows, publish access settings, and publish status/failure messages | Generic export formats/backups, sync state, or account identity |
|
||||
| `settings` | Built-in settings shell copy: settings sections, built-in setting labels, descriptions, and feedback about changing built-in settings | Child feature workflows or subsystem state merely rendered inside settings |
|
||||
| `theme` | Theme selection and theme-specific customization | Generic settings |
|
||||
| `plugin` | Plugin lifecycle, marketplace, plugin configuration, install/update/remove flows | Built-in settings or theme selection |
|
||||
| `ai` | Semantic search, embedding model selection, model download states, and other built-in AI feature UI | Generic settings scaffolding or non-AI search vocabulary |
|
||||
| `youtube` | YouTube-specific embed and timestamp behavior | Generic asset/video wording or generic mobile warnings |
|
||||
| `zotero` | Built-in Zotero integration, Zotero attachment access, Zotero-linked or imported file affordances, and Zotero-specific defaults | Generic file browser, generic import/export, or plugin lifecycle |
|
||||
| `server` | Local HTTP API, MCP, local server setup and diagnostics | Cloud sync or account identity |
|
||||
| `storage` | Local persistence, sqlite/local-db storage errors, recycle UI and recycle storage constraints | Cloud sync lifecycle or file browser UI |
|
||||
| `account` | Login, identity, plan, membership, billing-facing account state, and account-authentication actions such as resetting the account password | Graph sync state or passwords/keys that gate encrypted data |
|
||||
| `sync` | Graph sync, storage usage, invitations, remote graph lifecycle | Login identity or password management |
|
||||
| `collaboration` | Collaborators, participants, collaboration-only permissions and presence | Generic sync storage accounting |
|
||||
| `encryption` | Passwords, keypairs, encrypted graph access, and key reset flows for encrypted data | Login/billing identity state or account-authentication actions |
|
||||
| `onboarding` | First-run setup and initial import/graph setup | General settings or ongoing help |
|
||||
| `help` | Help hub copy: documentation, handbook, shortcut help, and community/support entry points | Child workflows launched from help, such as bug-reporting |
|
||||
| `bug-report` | Bug reporting, diagnostics, issue helpers | General help navigation |
|
||||
| `shell` | Built-in shell command runner UI and its workflow | Built-in command descriptions or generic terminal wording outside the shell runner feature |
|
||||
| `profiler` | Built-in profiling and diagnostics UI for developers or advanced users | Bug reporting copy, generic settings, or runtime performance logs |
|
||||
| `updater` | App-release update lifecycle: checking, availability, download/install progress, restart/install actions, and updater-specific errors/status | Settings-shell copy, plugin-update UI, or other container/entry-point copy |
|
||||
| `deeplink` | `logseq://` or deep-link open flows and deep-link resolution errors | Generic navigation labels or route names |
|
||||
|
||||
#### Product domain conflict rules
|
||||
|
||||
When multiple product domains could plausibly own the same text, apply these
|
||||
rules in order:
|
||||
|
||||
1. Choose the narrowest stable owner that names the feature, entity,
|
||||
integration, or workflow itself.
|
||||
2. Container or hub owners own only their own shell copy. They do not own child
|
||||
feature copy just because it is rendered there.
|
||||
3. Status, progress, result, validation, and error copy belongs to the subsystem
|
||||
or workflow emitting that state.
|
||||
4. Render location, launch point, or current screen does not determine owner.
|
||||
5. If the same feature text can appear in multiple places, keep one
|
||||
feature-owned key instead of forking container-specific duplicates.
|
||||
|
||||
Conflict examples:
|
||||
|
||||
```clojure
|
||||
:settings.general/check-for-updates
|
||||
:updater/checking-for-updates
|
||||
:help.shortcuts/title
|
||||
:bug-report.inspector/title
|
||||
```
|
||||
|
||||
More product-domain examples:
|
||||
|
||||
```clojure
|
||||
:page/delete
|
||||
:page.validation/name-no-hash
|
||||
:page.convert/cant-be-block
|
||||
:editor/remove-heading
|
||||
:editor.slash/node-reference
|
||||
:date.nlp/today
|
||||
:node/built-in-cant-delete-error
|
||||
:property/default-value
|
||||
:view.table/sort-ascending
|
||||
:plugin/install
|
||||
:settings.editor/show-brackets
|
||||
:sync/invitation-sent
|
||||
:encryption/reset-password
|
||||
:bug-report.inspector/title
|
||||
```
|
||||
|
||||
### 4. Shell Surfaces
|
||||
|
||||
Use only when the meaning depends on the shell surface itself.
|
||||
|
||||
| Namespace | Use for |
|
||||
|---|---|
|
||||
| `header` | Header-only actions and labels |
|
||||
| `sidebar.left` | Left sidebar shell affordances |
|
||||
| `sidebar.right` | Right sidebar shell affordances |
|
||||
| `context-menu` | Context menu-only affordances |
|
||||
| `window` | Window chrome actions |
|
||||
|
||||
Use this class when:
|
||||
|
||||
- moving the text to another surface would change its meaning
|
||||
- the text describes pane controls, sidebar controls, or window chrome
|
||||
- the text is not reused in another surface or runtime with the same meaning
|
||||
|
||||
Do not use a surface namespace for:
|
||||
|
||||
- feature titles rendered inside a surface
|
||||
- route or destination labels rendered inside a surface
|
||||
- domain workflows that happen to be launched from a surface
|
||||
- text that already appears with the same meaning in another surface; move it to
|
||||
`ui`, `nav`, or the feature owner
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:header/go-back
|
||||
:sidebar.left/favorites
|
||||
:sidebar.right/close
|
||||
:context-menu/set-icon
|
||||
:window/minimize
|
||||
```
|
||||
|
||||
### 5. Platform Runtimes
|
||||
|
||||
Use only when the text exists because one runtime has a unique implementation.
|
||||
|
||||
| Namespace | Use for |
|
||||
|---|---|
|
||||
| `mobile` | Mobile-only runtime behavior |
|
||||
| `electron` | Electron-only runtime behavior |
|
||||
|
||||
Use this class when:
|
||||
|
||||
- the workflow exists only on one runtime
|
||||
- the wording refers to a native/runtime-only capability
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:mobile.tab/graphs
|
||||
:mobile.settings/version
|
||||
:electron/new-window
|
||||
:electron/add-to-dictionary
|
||||
```
|
||||
|
||||
## Step 2: Apply the Decision Tree
|
||||
|
||||
Choose the first matching branch and stop.
|
||||
|
||||
1. Is the text owned by an interaction system? Use `command.*`,
|
||||
`shortcut.category`, `keymap`, or `cmdk`.
|
||||
2. Is the text a shared primitive reused across unrelated domains? Use `ui`,
|
||||
`nav`, `notification`, `search`, `select`, `format`, or `color`.
|
||||
3. Is the text owned by a product domain? Use the matching product domain
|
||||
namespace. If multiple product domains seem possible, apply `Product domain
|
||||
conflict rules` and then stop.
|
||||
4. Is the text owned by a shell surface? Use `header`, `sidebar.left`,
|
||||
`sidebar.right`, `context-menu`, or `window`.
|
||||
5. Is the text runtime-exclusive? Use `mobile` or `electron`.
|
||||
|
||||
If none fits, define a new product domain only when the feature has a clear,
|
||||
long-lived product boundary. Otherwise, keep the nearest existing product domain
|
||||
and use a more specific leaf.
|
||||
|
||||
## Owner Constraints
|
||||
|
||||
- Do not create roots from implementation modules or component files such as
|
||||
`outliner`, `content`, or `views`.
|
||||
- Do not create plural owner roots such as `flashcards` or `views`. Use the
|
||||
singular owner.
|
||||
- Do not use implementation acronyms such as `e2ee` when the product-facing
|
||||
owner is `encryption`.
|
||||
- Do not use implementation state holders such as `state` as owners. Use the
|
||||
semantic feature owner such as `journal.default-query/*`.
|
||||
- Do not use a surface owner for a destination label. Use `nav/*` or the feature
|
||||
owner.
|
||||
- Do not use a container or hub owner such as `settings` or `help` for child
|
||||
feature text just because the feature is rendered there.
|
||||
- Do not use a validator or storage engine as owner for a domain rule.
|
||||
Validation copy belongs to the constrained domain.
|
||||
- Treat a new root with fewer than ~5 plausible near-term keys as a smell, not a
|
||||
goal. Small roots are acceptable only when they name a first-class product
|
||||
feature, entity, or integration with a clear independent boundary.
|
||||
- When keeping a new root, update this taxonomy in the same change so the
|
||||
standard stays aligned with `src/resources/dicts/en.edn`.
|
||||
- Not every existing key in `en.edn` is a good naming precedent. Prefer this
|
||||
standard even when some legacy keys remain unchanged for compatibility.
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:reference.filter/title
|
||||
:help.handbook/title
|
||||
:page.validation/name-blank
|
||||
:property.choice/already-exists
|
||||
:class.validation/extends-cycle
|
||||
```
|
||||
|
||||
## Established Namespace Notes
|
||||
|
||||
These namespaces already exist in `en.edn` and are acceptable patterns, but
|
||||
they have specific reuse guidance.
|
||||
|
||||
| Namespace | Status | Guidance |
|
||||
|---|---|---|
|
||||
| `property.built-in`, `class.built-in` | Intentional | Stable built-in schema vocabularies under the `property` and `class` owners. |
|
||||
| `block.macro`, `property.repeat-recur-unit` | Intentional | Stable representation/enum groups. This pattern is acceptable when the subdomain names a real user-facing concept. |
|
||||
|
||||
## Step 3: Decide Whether a Subdomain Is Needed
|
||||
|
||||
Use a dotted subdomain only for one of these 4 cases.
|
||||
|
||||
### 1. Stable section
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:nav.all-pages/title
|
||||
:settings/account
|
||||
:settings.editor/show-brackets
|
||||
```
|
||||
|
||||
### 2. Stable workflow
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:page.delete/confirm-title
|
||||
:page.delete/warning
|
||||
:page.delete/success
|
||||
:plugin.install-from-file/title
|
||||
:editor.slash/group-basic
|
||||
```
|
||||
|
||||
### 3. Stable representation or mode
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:view.table/default-title
|
||||
:view.table/sort-ascending
|
||||
:mobile.toolbar/undo
|
||||
:server.status/running
|
||||
:cmdk.action/open
|
||||
```
|
||||
|
||||
### 4. Stable validator, conversion, or settings section
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:page.validation/name-no-hash
|
||||
:page.convert/cant-be-block
|
||||
:property.choice/already-exists
|
||||
:settings/account
|
||||
:help.shortcuts/title
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- use `.validation/` when the message is the direct result of a validation
|
||||
check, constraint violation, or failed precondition
|
||||
- use an existing workflow subdomain such as `.convert/` or `.delete/` for
|
||||
workflow-specific actions, confirmations, and blockers
|
||||
- do not create a narrower workflow-variant subdomain when an existing workflow
|
||||
already owns the text
|
||||
- for built-in settings tab labels, use flat keys such as `:settings/general`;
|
||||
reserve `:settings.<section>/*` for copy inside that settings section
|
||||
- choose compact concept names for subdomains; do not copy a long UI label
|
||||
phrase into a subdomain when a shorter stable concept name exists
|
||||
- prefer a flat key when the dotted subdomain would only contain one string for
|
||||
now
|
||||
- if an owner already has a flat canonical key for the concept, prefer flat
|
||||
role-suffixed siblings such as `about-title` or `auto-update-check-feedback`
|
||||
over introducing a dotted subdomain just to add another role
|
||||
- a flat leaf with a structured suffix such as `about-title` or `terms-title` is
|
||||
acceptable when the same owner already needs the base leaf such as `about` or
|
||||
`terms` for a different role, and creating a dotted singleton namespace would
|
||||
be worse
|
||||
- create a new dotted subdomain only when at least one of these is true:
|
||||
- the namespace already has sibling keys
|
||||
- the workflow or section clearly needs multiple roles such as `title` +
|
||||
`desc`, `confirm-title` + `confirm-desc`, or `empty` + `empty-desc`
|
||||
- the flat leaf would become less readable than the dotted form
|
||||
- a prefix with 2 to 4 keys is often healthy; the main smell is a singleton
|
||||
dotted subdomain shape such as `help.about/*` or `graph.delete-local/*`
|
||||
|
||||
Good examples:
|
||||
|
||||
```clojure
|
||||
:page.convert/tag-to-page-action
|
||||
:page.convert/tag-to-page-confirm-desc
|
||||
:property.validation/invalid-name
|
||||
:help/about-title
|
||||
:help/about
|
||||
:graph/delete-local-confirm-desc
|
||||
```
|
||||
|
||||
Do not use a subdomain for:
|
||||
|
||||
- component names
|
||||
- implementation names
|
||||
- generic layout slices
|
||||
- words like `main`, `section`, `btn`, or `modal` when they are only
|
||||
implementation layout terms and not real user-facing modes, surfaces, or
|
||||
scopes
|
||||
|
||||
Bad shapes for new keys:
|
||||
|
||||
- `:<owner>.main/*`
|
||||
- `:command.command/*`
|
||||
|
||||
## Step 4: Choose the Leaf
|
||||
|
||||
The leaf describes the text's role inside its owner.
|
||||
|
||||
### 1. Canonical labels
|
||||
|
||||
Use a bare subject or action when the text is the canonical label itself.
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:ui/save
|
||||
:page/backlinks
|
||||
:property/default-value
|
||||
:plugin/install
|
||||
```
|
||||
|
||||
### 2. Structured role suffixes
|
||||
|
||||
Use these suffixes consistently.
|
||||
|
||||
| Suffix | Use for |
|
||||
|---|---|
|
||||
| `title` | Panel, section, page, dialog, modal title |
|
||||
| `desc` | Supporting description |
|
||||
| `label` | Control, nav, picker, or form label when it is not the title |
|
||||
| `prompt` | Short picker or flow prompt |
|
||||
| `placeholder` | Input placeholder |
|
||||
| `hint` | Short inline help |
|
||||
| `tip` | Advice or explanatory tip |
|
||||
| `tooltip` | Hover text |
|
||||
| `empty` | Empty-state heading or label |
|
||||
| `empty-desc` | Empty-state description |
|
||||
| `confirm-title` | Confirmation title |
|
||||
| `confirm-desc` | Confirmation body |
|
||||
| `success` | Success feedback |
|
||||
| `error` | Error feedback |
|
||||
| `warning` | Warning feedback |
|
||||
| `feedback` | Neutral or severity-agnostic feedback |
|
||||
| `count` | Parameterized count text |
|
||||
| `action` | Action label when a bare verb would be ambiguous |
|
||||
|
||||
Additional rules:
|
||||
|
||||
- use `desc`, not `description`, for the textual role suffix
|
||||
- this does not ban the literal word `Description` when it is the product term
|
||||
being named
|
||||
- use `prompt`, not `message`, for short chooser, picker, or action-sheet
|
||||
instructions
|
||||
- use `label` when the text prefixes an inline value such as an ID, date, or
|
||||
selected item
|
||||
- do not split one sentence into `*-prefix` and `*-suffix` keys; keep a single
|
||||
translation entry and insert links, shortcuts, or styled fragments with
|
||||
placeholders
|
||||
- use `error` or `warning` for failure feedback; prefer `*-error` or `*-warning`
|
||||
over `*-failed`
|
||||
- for success, error, and warning feedback, prefer an action or condition stem
|
||||
such as `update-success`, `unpublish-error`, or `invalid-date-warning` instead
|
||||
of past-tense English like `updated` or `failed`
|
||||
- do not mechanically shorten a leaf just because one word also appears in the
|
||||
owner; keep the action or condition name when it distinguishes a workflow or
|
||||
condition inside that owner, for example `:publish/publish-error`,
|
||||
`:import/zip-import-error`, or `:date/invalid-date-warning`
|
||||
- use `feedback` when the same toast or callout may appear with varying
|
||||
severity, or when the severity is incidental to the wording
|
||||
- use `status` only when the text names a status field, status value, or status
|
||||
representation in the product model; do not use `status` as a catch-all suffix
|
||||
for post-action toasts
|
||||
- when the base concept is already a fixed product term, keep it intact even if
|
||||
the role suffix repeats an English word, for example `:ui/error-boundary-error`
|
||||
|
||||
Examples:
|
||||
|
||||
```clojure
|
||||
:help.shortcuts/label
|
||||
:graph.switch/select-prompt
|
||||
:nav.all-pages/title
|
||||
:server.config/port-label
|
||||
:graph/delete-local-confirm-desc
|
||||
:plugin/auto-update-check-feedback
|
||||
:property/update-success
|
||||
:publish/unpublish-error
|
||||
:plugin.install-from-file/success
|
||||
:graph.switch/empty-desc
|
||||
:page.convert/tag-to-page-action
|
||||
```
|
||||
|
||||
Use `error` or `warning` for failure feedback, based on the feedback severity
|
||||
shown in the UI. Do not use `failure` as a leaf.
|
||||
|
||||
## Reuse Rules
|
||||
|
||||
Do not reuse a key only because the English text matches.
|
||||
|
||||
Reuse a key only when both are the same:
|
||||
|
||||
1. semantic owner
|
||||
2. textual role
|
||||
|
||||
Examples:
|
||||
|
||||
- toolbar `"Bold"` and command `"Bold"` are different keys
|
||||
- dialog `"Close"` and window `"Close"` are different keys
|
||||
- `"Copied!"` may be shared only if it is intentionally a reusable cross-domain
|
||||
notification
|
||||
- settings-shell `"Check for updates"` and updater-state `"Checking for
|
||||
updates"` are different keys because the owner differs
|
||||
|
||||
When two keys are truly duplicates:
|
||||
|
||||
- keep the key that already follows the standard
|
||||
- deprecate the duplicate key
|
||||
- do not merge keys when one message carries extra workflow or domain-specific
|
||||
detail
|
||||
|
||||
## Naming Workflow
|
||||
|
||||
For every new string:
|
||||
|
||||
1. Identify the owner with the decision tree.
|
||||
2. Choose the root namespace from the owner taxonomy.
|
||||
3. Add a subdomain only if the string belongs to a stable section, workflow, or
|
||||
representation.
|
||||
4. Choose the leaf from the role rules.
|
||||
5. Search `src/resources/dicts/en.edn` for an existing key with the same owner
|
||||
and role.
|
||||
6. Reuse only on exact semantic match.
|
||||
7. If the new name would create a new root or a singleton dotted subdomain,
|
||||
justify why convergence would be worse without it.
|
||||
8. Add the English source text to `src/resources/dicts/en.edn`.
|
||||
9. After editing dict files, run `bb lang:format-dicts`.
|
||||
|
||||
## Canonical Examples
|
||||
|
||||
| Need | Correct key |
|
||||
|---|---|
|
||||
| Generic dialog close button | `:ui/close` |
|
||||
| Header back button tooltip | `:header/go-back` |
|
||||
| Window close button | `:window/close` |
|
||||
| Graph local deletion confirmation body | `:graph/delete-local-confirm-desc` |
|
||||
| Page name validation error | `:page.validation/name-no-hash` |
|
||||
| Active editor action `"Remove heading"` | `:editor/remove-heading` |
|
||||
| Built-in node delete validation | `:node/built-in-cant-delete-error` |
|
||||
| Property name input placeholder | `:property/name-placeholder` |
|
||||
| Recycle item restore action | `:storage.recycle/restore` |
|
||||
| Recycle page deletion metadata | `:storage.recycle/page-deleted-at` |
|
||||
| Graph switch picker prompt | `:graph.switch/select-prompt` |
|
||||
| Export copied page data feedback | `:export/page-data-copied` |
|
||||
| Live query table title | `:view.table/live-query-title` |
|
||||
| Table sort ascending action | `:view.table/sort-ascending` |
|
||||
| Plugin install-from-file success | `:plugin.install-from-file/success` |
|
||||
| Command palette open action | `:cmdk.action/open` |
|
||||
| Mobile-only graph tab | `:mobile.tab/graphs` |
|
||||
| Server running status | `:server.status/running` |
|
||||
@@ -7,7 +7,8 @@
|
||||
"watch:ui:examples": "parcel serve ./examples/index.html",
|
||||
"build:ui:only": "parcel build --target ui",
|
||||
"build:ui": "rm -rf .parcel-cache && yarn build:ui:only",
|
||||
"postinstall": "yarn build:ui"
|
||||
"postinstall": "yarn build:ui",
|
||||
"test": "node --experimental-strip-types --test src/i18n.test.mts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
|
||||
42
packages/ui/src/amplify/errors.ts
Normal file
42
packages/ui/src/amplify/errors.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
type AuthErrorLike = {
|
||||
code?: string
|
||||
name?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
function getAuthErrorName(error: unknown) {
|
||||
const authError = (error ?? {}) as AuthErrorLike
|
||||
return authError.name || authError.code || ''
|
||||
}
|
||||
|
||||
export function getAuthErrorMessageKey(error: unknown) {
|
||||
switch (getAuthErrorName(error)) {
|
||||
case 'UserNotFoundException':
|
||||
return 'AUTH_ERROR_USER_NOT_FOUND'
|
||||
case 'NotAuthorizedException':
|
||||
return 'AUTH_ERROR_INVALID_CREDENTIALS'
|
||||
case 'UserNotConfirmedException':
|
||||
return 'AUTH_ERROR_USER_NOT_CONFIRMED'
|
||||
case 'UsernameExistsException':
|
||||
return 'AUTH_ERROR_USERNAME_EXISTS'
|
||||
case 'InvalidPasswordException':
|
||||
return 'PW_POLICY_TIP'
|
||||
case 'CodeMismatchException':
|
||||
return 'AUTH_ERROR_CODE_MISMATCH'
|
||||
case 'ExpiredCodeException':
|
||||
return 'AUTH_ERROR_CODE_EXPIRED'
|
||||
case 'LimitExceededException':
|
||||
case 'TooManyRequestsException':
|
||||
return 'AUTH_ERROR_TOO_MANY_REQUESTS'
|
||||
case 'TooManyFailedAttemptsException':
|
||||
return 'AUTH_ERROR_TOO_MANY_ATTEMPTS'
|
||||
case 'CodeDeliveryFailureException':
|
||||
return 'AUTH_ERROR_CODE_DELIVERY_FAILED'
|
||||
case 'UserAlreadyAuthenticatedException':
|
||||
return 'AUTH_ERROR_ALREADY_AUTHENTICATED'
|
||||
case 'InvalidParameterException':
|
||||
return 'AUTH_ERROR_INVALID_PARAMETER'
|
||||
default:
|
||||
return 'AUTH_ERROR_GENERIC'
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,14 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input, InputProps } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { FormHTMLAttributes, useEffect, useState } from 'react'
|
||||
import { FormHTMLAttributes, useEffect, useRef, useState } from 'react'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { AlertCircleIcon, Loader2Icon, LucideEye, LucideEyeClosed, LucideX } from 'lucide-react'
|
||||
import { AuthFormRootContext, t, useAuthFormState } from './core'
|
||||
import * as Auth from 'aws-amplify/auth'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import * as React from 'react'
|
||||
import { getAuthErrorMessageKey } from './errors'
|
||||
|
||||
function ErrorTip({ error, removeError }: {
|
||||
error: string | { variant?: 'warning' | 'destructive', title?: string, message: string | any },
|
||||
@@ -108,14 +109,26 @@ function validatePasswordPolicy(password: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthErrorMessage(error: unknown) {
|
||||
return t(getAuthErrorMessageKey(error))
|
||||
}
|
||||
|
||||
function useCountDown() {
|
||||
const [countDownNum, setCountDownNum] = useState<number>(0)
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const startCountDown = () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
setCountDownNum(60)
|
||||
const interval = setInterval(() => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCountDownNum((num) => {
|
||||
if (num <= 1) {
|
||||
clearInterval(interval)
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return num - 1
|
||||
@@ -125,7 +138,10 @@ function useCountDown() {
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setCountDownNum(0)
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -224,10 +240,10 @@ export function LoginForm() {
|
||||
await loadSession()
|
||||
return
|
||||
default:
|
||||
throw new Error('Unsupported sign-in step: ' + nextStep)
|
||||
throw new Error(`${t('Unsupported sign-in step:')} ${nextStep}`)
|
||||
}
|
||||
} catch (e) {
|
||||
setErrors({ password: { message: (e as Error).message, title: t('Bad Response.') } })
|
||||
setErrors({ password: { message: getAuthErrorMessage(e), title: t('Bad Response.') } })
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -338,7 +354,7 @@ export function SignupForm() {
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
const error = { title: t('Bad Response.'), message: (e as Error).message }
|
||||
const error = { title: t('Bad Response.'), message: getAuthErrorMessage(e) }
|
||||
let k = 'confirm_password'
|
||||
if (e.name === 'UsernameExistsException') {
|
||||
k = 'username'
|
||||
@@ -423,21 +439,25 @@ export function ResetPasswordForm() {
|
||||
setIsSentCode(true)
|
||||
} catch (error) {
|
||||
console.error('Error sending reset code:', error)
|
||||
setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
|
||||
setErrors({ email: { message: getAuthErrorMessage(error), title: t('Bad Response.') } })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
} else {
|
||||
// confirm reset password
|
||||
if ((data.password as string)?.length < 8) {
|
||||
try {
|
||||
validatePasswordPolicy(data.password as string)
|
||||
} catch (error) {
|
||||
setErrors({
|
||||
password: {
|
||||
message: t('Password must be at least 8 characters.'),
|
||||
message: (error as Error).message,
|
||||
title: t('Invalid Password')
|
||||
}
|
||||
})
|
||||
return
|
||||
} else if (data.password !== data.confirm_password) {
|
||||
}
|
||||
|
||||
if (data.password !== data.confirm_password) {
|
||||
setErrors({
|
||||
confirm_password: {
|
||||
message: t('Passwords do not match.'),
|
||||
@@ -458,7 +478,7 @@ export function ResetPasswordForm() {
|
||||
setCurrentTab('login')
|
||||
} catch (error) {
|
||||
console.error('Error confirming reset password:', error)
|
||||
setErrors({ 'confirm_password': { message: (error as Error).message, title: t('Bad Response.') } })
|
||||
setErrors({ 'confirm_password': { message: getAuthErrorMessage(error), title: t('Bad Response.') } })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -470,7 +490,7 @@ export function ResetPasswordForm() {
|
||||
<div className={'w-full opacity-60 flex justify-end relative h-0 z-[2]'}>
|
||||
{countDownNum > 0 ? (
|
||||
<span className={'text-sm opacity-50 select-none absolute top-3 right-0'}>
|
||||
{countDownNum}s
|
||||
{countDownNum}{t('COUNTDOWN_SUFFIX')}
|
||||
</span>
|
||||
) : (<a onClick={async () => {
|
||||
startCountDown()
|
||||
@@ -479,7 +499,7 @@ export function ResetPasswordForm() {
|
||||
console.debug('[Auth] reset pw code re-sent: ', ret)
|
||||
} catch (error) {
|
||||
console.error('Error resending reset code:', error)
|
||||
setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
|
||||
setErrors({ email: { message: getAuthErrorMessage(error), title: t('Bad Response.') } })
|
||||
} finally {}
|
||||
}} className={'text-sm opacity-70 hover:opacity-90 underline absolute top-3 right-0 select-none'}>
|
||||
{t('Resend code')}
|
||||
@@ -589,7 +609,7 @@ export function ConfirmWithCodeForm(
|
||||
console.debug('confirmSignIn: ', ret)
|
||||
}
|
||||
} catch (e) {
|
||||
setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
|
||||
setErrors({ code: { message: getAuthErrorMessage(e), title: t('Bad Response.') } })
|
||||
console.error(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -613,7 +633,7 @@ export function ConfirmWithCodeForm(
|
||||
<span className={'w-full flex justify-end relative h-0 z-10'}>
|
||||
{countDownNum > 0 ? (
|
||||
<span className={'text-sm opacity-50 select-none absolute -bottom-8'}>
|
||||
{countDownNum}s
|
||||
{countDownNum}{t('COUNTDOWN_SUFFIX')}
|
||||
</span>
|
||||
) : <a
|
||||
className={'text-sm opacity-50 hover:opacity-80 active:opacity-50 select-none underline absolute -bottom-8'}
|
||||
@@ -632,7 +652,7 @@ export function ConfirmWithCodeForm(
|
||||
// await Auth.resendSignInCode(props.user)
|
||||
}
|
||||
} catch (e) {
|
||||
setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
|
||||
setErrors({ code: { message: getAuthErrorMessage(e), title: t('Bad Response.') } })
|
||||
setCountDownNum(0)
|
||||
console.error(e)
|
||||
} finally {}
|
||||
@@ -707,4 +727,4 @@ export function LSAuthenticator(props: any) {
|
||||
</div>
|
||||
</AuthFormRootContext.Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
48
packages/ui/src/i18n.test.mts
Normal file
48
packages/ui/src/i18n.test.mts
Normal file
@@ -0,0 +1,48 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { setLocale, setNSDicts, setTranslate, translate } from './i18n'
|
||||
import { getAuthErrorMessageKey } from './amplify/errors'
|
||||
|
||||
test('translate uses the selected locale when the namespace dict contains it', () => {
|
||||
setTranslate((locale, dicts, key, ...args) => dicts[locale]?.[key] ?? args[0] ?? key)
|
||||
setNSDicts('locale', {
|
||||
en: { greeting: 'Hello' },
|
||||
'zh-CN': { greeting: '你好' }
|
||||
})
|
||||
setLocale('zh-CN')
|
||||
|
||||
assert.equal(translate('locale', 'greeting'), '你好')
|
||||
})
|
||||
|
||||
test('translate falls back to English when the current locale is unavailable', () => {
|
||||
setTranslate((locale, dicts, key, ...args) => dicts[locale]?.[key] ?? args[0] ?? key)
|
||||
setNSDicts('fallback', {
|
||||
en: { greeting: 'Hello' }
|
||||
})
|
||||
setLocale('zh-CN')
|
||||
|
||||
assert.equal(translate('fallback', 'greeting'), 'Hello')
|
||||
})
|
||||
|
||||
test('translate falls back to English when the current locale dict has no corresponding key', () => {
|
||||
setTranslate((locale, dicts, key, ...args) => dicts[locale]?.[key] ?? args[0] ?? key)
|
||||
setNSDicts('fallback', {
|
||||
en: { greeting: 'Hello' },
|
||||
'zh-CN': { farewell: '再见' }
|
||||
})
|
||||
setLocale('zh-CN')
|
||||
|
||||
assert.equal(translate('fallback', 'greeting'), 'Hello')
|
||||
})
|
||||
|
||||
test('getAuthErrorMessageKey maps common Cognito errors to localized keys', () => {
|
||||
assert.equal(getAuthErrorMessageKey({ name: 'NotAuthorizedException' }), 'AUTH_ERROR_INVALID_CREDENTIALS')
|
||||
assert.equal(getAuthErrorMessageKey({ name: 'CodeMismatchException' }), 'AUTH_ERROR_CODE_MISMATCH')
|
||||
assert.equal(getAuthErrorMessageKey({ name: 'InvalidPasswordException' }), 'PW_POLICY_TIP')
|
||||
})
|
||||
|
||||
test('getAuthErrorMessageKey falls back to a generic localized key for unknown errors', () => {
|
||||
assert.equal(getAuthErrorMessageKey({ name: 'SomethingUnexpected' }), 'AUTH_ERROR_GENERIC')
|
||||
assert.equal(getAuthErrorMessageKey(new Error('plain error')), 'AUTH_ERROR_GENERIC')
|
||||
})
|
||||
@@ -13,7 +13,7 @@ let _translate: TranslateFn = (
|
||||
key: string,
|
||||
...args: any
|
||||
) => {
|
||||
return dicts[locale]?.[key] || args[0] || key
|
||||
return dicts[locale]?.[key] ?? args[0] ?? key
|
||||
}
|
||||
|
||||
export function setTranslate(t: TranslateFn) {
|
||||
@@ -24,7 +24,7 @@ export function setLocale(locale: string) {
|
||||
_locale = locale
|
||||
}
|
||||
|
||||
export function setNSDicts(ns: string, dicts: Record<string, string>) {
|
||||
export function setNSDicts(ns: string, dicts: Record<string, any>) {
|
||||
(_nsDicts as any)[ns] = dicts
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const translate = (
|
||||
...args: any
|
||||
) => {
|
||||
const dicts = (_nsDicts as any)[ns] || {}
|
||||
return _translate(
|
||||
_nsDicts?.hasOwnProperty(_locale) ? _locale : 'en',
|
||||
dicts, key, ...args)
|
||||
const localeDict = dicts[_locale]
|
||||
const locale = (localeDict && Object.prototype.hasOwnProperty.call(localeDict, key)) ? _locale : 'en'
|
||||
return _translate(locale, dicts, key, ...args)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ You're Clojure(script) expert, you're responsible to check those common errors:
|
||||
- Replace `js/console.warn` with `log/warn`.
|
||||
- Replace `js/console.log` with `log/info`.
|
||||
- NOTE: `log/<level>` function takes key-value pairs as arguments
|
||||
|
||||
|
||||
- After adding a new property in `logseq.db.frontend.property/built-in-properties`, you need to add a corresponding migration in `frontend.worker.db.migrate/schema-version->updates`.
|
||||
- e.g. `["65.10" {:properties [:block/journal-day]}]`
|
||||
|
||||
@@ -27,7 +27,44 @@ You're Clojure(script) expert, you're responsible to check those common errors:
|
||||
|
||||
- A function that returns a promise, and its function name starts with "<".
|
||||
|
||||
## i18n review rules
|
||||
|
||||
- Use `.i18n-lint.toml` as the source of truth for i18n lint scope, covered UI
|
||||
helpers, translated attributes, exclusions, and allowlists.
|
||||
- Inside that scope, all shipped user-facing UI text must use helpers from
|
||||
`frontend.context.i18n`. Console text is exempt. Keep out-of-scope
|
||||
developer-only `(Dev)` labels inline in code/config, not in translation
|
||||
dictionaries.
|
||||
- If a new user-facing surface is not represented in `.i18n-lint.toml`, flag
|
||||
the missing lint coverage.
|
||||
- Reuse existing `src/resources/dicts/en.edn` keys only on exact semantic owner
|
||||
and textual role match. Otherwise follow `docs/i18n-key-naming.md`.
|
||||
- Add new English source text to `src/resources/dicts/en.edn`. Add non-English
|
||||
entries only when providing actual translations. When renaming or removing
|
||||
keys, clean up stale keys in affected locale files.
|
||||
- `notification/show!` and translated attributes from `.i18n-lint.toml`
|
||||
(placeholder/title/aria/label-like UI text) must not receive raw English
|
||||
string literals unless proven non-user-facing.
|
||||
- For plain dynamic text, use placeholders like `{1}` and pre-format arguments
|
||||
in the caller before passing them to `t`.
|
||||
- Keep complete sentences in one translation entry. Use
|
||||
`interpolate-rich-text`, `interpolate-sentence`, `locale-join-rich-text`, and
|
||||
`locale-format-*` from `frontend.context.i18n` instead of assembling text ad
|
||||
hoc in the caller.
|
||||
- Function-valued translations are allowed only for real logic or hiccup rich
|
||||
text, and may only use `str`, `when`, `if`, and `=`.
|
||||
- Rich text and inline links must stay in a single translation entry, not split
|
||||
across multiple keys.
|
||||
- Preserve emoji/icon glyphs from `en.edn`, and use punctuation natural to each
|
||||
locale.
|
||||
- Pluralization is locale-specific. Do not force English singular/plural
|
||||
structure onto other locales.
|
||||
- After changing keys run `bb lang:validate-translations`; after changing UI
|
||||
text run `bb lang:lint-hardcoded`; after editing dictionary files run
|
||||
`bb lang:format-dicts`.
|
||||
- If you add a new linted helper or attribute, update `.i18n-lint.toml`.
|
||||
|
||||
- Prohibit converting js/Uint8Array to vector. e.g. `(vec uint8-array)`
|
||||
- This operation is very slow when the Uint8Array is large (e.g. an asset).
|
||||
- This operation is very slow when the Uint8Array is large (e.g. an asset).
|
||||
|
||||
- `:block/content` attribute is not used in the DB version; `:block/title` is the attribute that stores the main content of the block.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"name": "nbb-dev-scripts",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "yarn nbb-logseq -cp test -m logseq.tasks.test-runner"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v34"
|
||||
},
|
||||
|
||||
@@ -21807,7 +21807,7 @@
|
||||
{
|
||||
"@id": "schema:SpecialAnnouncement",
|
||||
"@type": "rdfs:Class",
|
||||
"rdfs:comment": "A SpecialAnnouncement combines a simple date-stamped textual information update\n with contextualized Web links and other structured data. It represents an information update made by a\n locally-oriented organization, for example schools, pharmacies, healthcare providers, community groups, police,\n local government.\n\nFor work in progress guidelines on Coronavirus-related markup see [this doc](https://docs.google.com/document/d/14ikaGCKxo50rRM7nvKSlbUpjyIk2WMQd3IkB1lItlrM/edit#).\n\nThe motivating scenario for SpecialAnnouncement is the [Coronavirus pandemic](https://en.wikipedia.org/wiki/2019%E2%80%9320_coronavirus_pandemic), and the initial vocabulary is oriented to this urgent situation. Schema.org\nexpect to improve the markup iteratively as it is deployed and as feedback emerges from use. In addition to our\nusual [Github entry](https://github.com/schemaorg/schemaorg/issues/2490), feedback comments can also be provided in [this document](https://docs.google.com/document/d/1fpdFFxk8s87CWwACs53SGkYv3aafSxz_DTtOQxMrBJQ/edit#).\n\n\nWhile this schema is designed to communicate urgent crisis-related information, it is not the same as an emergency warning technology like [CAP](https://en.wikipedia.org/wiki/Common_Alerting_Protocol), although there may be overlaps. The intent is to cover\nthe kinds of everyday practical information being posted to existing websites during an emergency situation.\n\nSeveral kinds of information can be provided:\n\nWe encourage the provision of \"name\", \"text\", \"datePosted\", \"expires\" (if appropriate), \"category\" and\n\"url\" as a simple baseline. It is important to provide a value for \"category\" where possible, most ideally as a well known\nURL from Wikipedia or Wikidata. In the case of the 2019-2020 Coronavirus pandemic, this should be \"https://en.wikipedia.org/w/index.php?title=2019-20\\_coronavirus\\_pandemic\" or \"https://www.wikidata.org/wiki/Q81068910\".\n\nFor many of the possible properties, values can either be simple links or an inline description, depending on whether a summary is available. For a link, provide just the URL of the appropriate page as the property's value. For an inline description, use a [[WebContent]] type, and provide the url as a property of that, alongside at least a simple \"[[text]]\" summary of the page. It is\nunlikely that a single SpecialAnnouncement will need all of the possible properties simultaneously.\n\nWe expect that in many cases the page referenced might contain more specialized structured data, e.g. contact info, [[openingHours]], [[Event]], [[FAQPage]] etc. By linking to those pages from a [[SpecialAnnouncement]] you can help make it clearer that the events are related to the situation (e.g. Coronavirus) indicated by the [[category]] property of the [[SpecialAnnouncement]].\n\nMany [[SpecialAnnouncement]]s will relate to particular regions and to identifiable local organizations. Use [[spatialCoverage]] for the region, and [[announcementLocation]] to indicate specific [[LocalBusiness]]es and [[CivicStructure]]s. If the announcement affects both a particular region and a specific location (for example, a library closure that serves an entire region), use both [[spatialCoverage]] and [[announcementLocation]].\n\nThe [[about]] property can be used to indicate entities that are the focus of the announcement. We now recommend using [[about]] only\nfor representing non-location entities (e.g. a [[Course]] or a [[RadioStation]]). For places, use [[announcementLocation]] and [[spatialCoverage]]. Consumers of this markup should be aware that the initial design encouraged the use of [[about]] for locations too.\n\nThe basic content of [[SpecialAnnouncement]] is similar to that of an [RSS](https://en.wikipedia.org/wiki/RSS) or [Atom](https://en.wikipedia.org/wiki/Atom_(Web_standard)) feed. For publishers without such feeds, basic feed-like information can be shared by posting\n[[SpecialAnnouncement]] updates in a page, e.g. using JSON-LD. For sites with Atom/RSS functionality, you can point to a feed\nwith the [[webFeed]] property. This can be a simple URL, or an inline [[DataFeed]] object, with [[encodingFormat]] providing\nmedia type information, e.g. \"application/rss+xml\" or \"application/atom+xml\".\n",
|
||||
"rdfs:comment": "A SpecialAnnouncement combines a simple date-stamped textual information update\n with contextualized Web links and other structured data. It represents an information update made by a\n locally-oriented organization, for example schools, pharmacies, healthcare providers, community groups, police,\n local government.\n\nFor work in progress guidelines on Coronavirus-related markup see [this doc](https://docs.google.com/document/d/14ikaGCKxo50rRM7nvKSlbUpjyIk2WMQd3IkB1lItlrM/edit#).\n\nThe motivating scenario for SpecialAnnouncement is the [Coronavirus pandemic](https://en.wikipedia.org/wiki/2019%E2%80%9320_coronavirus_pandemic), and the initial vocabulary is oriented to this urgent situation. Schema.org\nexpect to improve the markup iteratively as it is deployed and as feedback emerges from use. In addition to our\nusual [GitHub entry](https://github.com/schemaorg/schemaorg/issues/2490), feedback comments can also be provided in [this document](https://docs.google.com/document/d/1fpdFFxk8s87CWwACs53SGkYv3aafSxz_DTtOQxMrBJQ/edit#).\n\n\nWhile this schema is designed to communicate urgent crisis-related information, it is not the same as an emergency warning technology like [CAP](https://en.wikipedia.org/wiki/Common_Alerting_Protocol), although there may be overlaps. The intent is to cover\nthe kinds of everyday practical information being posted to existing websites during an emergency situation.\n\nSeveral kinds of information can be provided:\n\nWe encourage the provision of \"name\", \"text\", \"datePosted\", \"expires\" (if appropriate), \"category\" and\n\"url\" as a simple baseline. It is important to provide a value for \"category\" where possible, most ideally as a well known\nURL from Wikipedia or Wikidata. In the case of the 2019-2020 Coronavirus pandemic, this should be \"https://en.wikipedia.org/w/index.php?title=2019-20\\_coronavirus\\_pandemic\" or \"https://www.wikidata.org/wiki/Q81068910\".\n\nFor many of the possible properties, values can either be simple links or an inline description, depending on whether a summary is available. For a link, provide just the URL of the appropriate page as the property's value. For an inline description, use a [[WebContent]] type, and provide the url as a property of that, alongside at least a simple \"[[text]]\" summary of the page. It is\nunlikely that a single SpecialAnnouncement will need all of the possible properties simultaneously.\n\nWe expect that in many cases the page referenced might contain more specialized structured data, e.g. contact info, [[openingHours]], [[Event]], [[FAQPage]] etc. By linking to those pages from a [[SpecialAnnouncement]] you can help make it clearer that the events are related to the situation (e.g. Coronavirus) indicated by the [[category]] property of the [[SpecialAnnouncement]].\n\nMany [[SpecialAnnouncement]]s will relate to particular regions and to identifiable local organizations. Use [[spatialCoverage]] for the region, and [[announcementLocation]] to indicate specific [[LocalBusiness]]es and [[CivicStructure]]s. If the announcement affects both a particular region and a specific location (for example, a library closure that serves an entire region), use both [[spatialCoverage]] and [[announcementLocation]].\n\nThe [[about]] property can be used to indicate entities that are the focus of the announcement. We now recommend using [[about]] only\nfor representing non-location entities (e.g. a [[Course]] or a [[RadioStation]]). For places, use [[announcementLocation]] and [[spatialCoverage]]. Consumers of this markup should be aware that the initial design encouraged the use of [[about]] for locations too.\n\nThe basic content of [[SpecialAnnouncement]] is similar to that of an [RSS](https://en.wikipedia.org/wiki/RSS) or [Atom](https://en.wikipedia.org/wiki/Atom_(Web_standard)) feed. For publishers without such feeds, basic feed-like information can be shared by posting\n[[SpecialAnnouncement]] updates in a page, e.g. using JSON-LD. For sites with Atom/RSS functionality, you can point to a feed\nwith the [[webFeed]] property. This can be a simple URL, or an inline [[DataFeed]] object, with [[encodingFormat]] providing\nmedia type information, e.g. \"application/rss+xml\" or \"application/atom+xml\".\n",
|
||||
"rdfs:label": "SpecialAnnouncement",
|
||||
"rdfs:subClassOf": {
|
||||
"@id": "schema:CreativeWork"
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as string]
|
||||
[frontend.dicts :as dicts]
|
||||
[logseq.tasks.util :as task-util]))
|
||||
[logseq.tasks.lang-lint :as lang-lint]
|
||||
[logseq.tasks.util :as task-util]
|
||||
[rewrite-clj.node :as node]))
|
||||
|
||||
(defn- get-dicts
|
||||
[]
|
||||
@@ -19,29 +21,49 @@
|
||||
(map (juxt :value :label))
|
||||
(into {})))
|
||||
|
||||
(defn list-langs
|
||||
"List translated languages with their number of translations"
|
||||
[]
|
||||
(let [dicts (get-dicts)
|
||||
en-count (count (dicts :en))
|
||||
langs (get-languages)]
|
||||
(->> dicts
|
||||
(map (fn [[locale dicts]]
|
||||
[locale
|
||||
(Math/round (* 100.0 (/ (count dicts) en-count)))
|
||||
(count dicts)
|
||||
(langs locale)]))
|
||||
(sort-by #(nth % 2) >)
|
||||
(map #(zipmap [:locale :percent-translated :translation-count :language] %))
|
||||
task-util/print-table)))
|
||||
|
||||
(defn- shorten [s length]
|
||||
(if (< (count s) length)
|
||||
s
|
||||
(string/replace (str (subs s 0 length) "...")
|
||||
;; Escape newlines for multi-line translations like tutorials
|
||||
;; Keep shortened table rows single-line for multi-line translations.
|
||||
"\n" "\\n")))
|
||||
|
||||
(defn list-langs
|
||||
"List translated languages with their number of translations"
|
||||
[]
|
||||
(let [dicts (get-dicts)
|
||||
langs (get-languages)]
|
||||
(->> (lang-lint/translation-summary-stats dicts)
|
||||
(lang-lint/sort-translation-summary-stats)
|
||||
(map (fn [{:keys [lang translation-count untranslated-count same-as-en-count]}]
|
||||
{:locale lang
|
||||
:translation-count translation-count
|
||||
:untranslated-count (if (= lang :en) "-" untranslated-count)
|
||||
:same-as-en-count (if (= lang :en) "-" same-as-en-count)
|
||||
:language (langs lang)}))
|
||||
task-util/print-table)))
|
||||
|
||||
(defn list-pseudo
|
||||
"List translations for LOCALE whose localized value is identical to English."
|
||||
[& args]
|
||||
(let [lang (or (some-> (first args) keyword)
|
||||
(task-util/print-usage "LOCALE"))
|
||||
langs (get-languages)
|
||||
dicts (get-dicts)]
|
||||
(when-not (contains? langs lang)
|
||||
(println "Language" lang "does not have an entry in frontend.dicts/languages")
|
||||
(System/exit 1))
|
||||
(let [findings (->> (lang-lint/identical-translation-findings dicts lang)
|
||||
(map (fn [{:keys [translation-key default-value]}]
|
||||
{:translation-key translation-key
|
||||
:same-as-en-value default-value
|
||||
:file (str "dicts/" (-> lang name string/lower-case) ".edn")}))
|
||||
(sort-by (juxt :file :translation-key)))]
|
||||
(if (empty? findings)
|
||||
(println "Language" lang "does not contain translations identical to English!")
|
||||
(task-util/print-table
|
||||
(map #(update % :same-as-en-value shorten 50) findings))))))
|
||||
|
||||
(defn list-missing
|
||||
"List missing translations for a given language"
|
||||
[& args]
|
||||
@@ -49,7 +71,7 @@
|
||||
(task-util/print-usage "LOCALE [--copy]"))
|
||||
options (cli/parse-opts (rest args) {:coerce {:copy :boolean}})
|
||||
_ (when-not (contains? (get-languages) lang)
|
||||
(println "Language" lang "does not have an entry in dicts/core.cljs")
|
||||
(println "Language" lang "does not have an entry in frontend.dicts/languages")
|
||||
(System/exit 1))
|
||||
dicts (get-dicts)
|
||||
all-missing (select-keys (dicts :en)
|
||||
@@ -83,21 +105,138 @@
|
||||
result invalid-keys))]
|
||||
(spit (fs/file path) new-content))))
|
||||
|
||||
(def ^:private dicts-dir
|
||||
(fs/path "src/resources/dicts"))
|
||||
|
||||
(def ^:private ignored-dict-node-tags
|
||||
#{:comment :newline :whitespace})
|
||||
|
||||
(defn- ignored-dict-node?
|
||||
[node]
|
||||
(contains? ignored-dict-node-tags (node/tag node)))
|
||||
|
||||
(defn- dict-map-node
|
||||
[root]
|
||||
(->> (:children root)
|
||||
(remove ignored-dict-node?)
|
||||
first))
|
||||
|
||||
(defn- parse-dict-entries
|
||||
[text]
|
||||
(let [root (rewrite/parse-string text)
|
||||
map-node (dict-map-node root)]
|
||||
(when-not (= :map (node/tag map-node))
|
||||
(println "Expected a top-level map in dictionary file.")
|
||||
(System/exit 1))
|
||||
(let [entry-nodes (->> (:children map-node)
|
||||
(remove ignored-dict-node?))]
|
||||
(when (odd? (count entry-nodes))
|
||||
(println "Encountered an uneven number of top-level dictionary nodes.")
|
||||
(System/exit 1))
|
||||
(mapv (fn [[key-node value-node]]
|
||||
{:key (rewrite/sexpr key-node)
|
||||
:value-node value-node})
|
||||
(partition 2 entry-nodes)))))
|
||||
|
||||
(defn- render-dict-entry
|
||||
[{:keys [key value-node]}]
|
||||
(str " " key " " value-node))
|
||||
|
||||
(defn- key-namespace-root
|
||||
[key]
|
||||
(some-> key namespace (string/split #"\.") first))
|
||||
|
||||
(defn- key-leaf
|
||||
[key]
|
||||
(name key))
|
||||
|
||||
(defn- compare-dict-keys
|
||||
[key-a key-b]
|
||||
(let [namespace-a (namespace key-a)
|
||||
namespace-b (namespace key-b)
|
||||
root-a (key-namespace-root key-a)
|
||||
root-b (key-namespace-root key-b)
|
||||
root-diff (compare root-a root-b)]
|
||||
(cond
|
||||
(not= 0 root-diff)
|
||||
root-diff
|
||||
|
||||
(not= namespace-a root-a)
|
||||
(if (= namespace-b root-b) 1
|
||||
(let [namespace-diff (compare namespace-a namespace-b)]
|
||||
(if (zero? namespace-diff)
|
||||
(compare (key-leaf key-a) (key-leaf key-b))
|
||||
namespace-diff)))
|
||||
|
||||
(not= namespace-b root-b)
|
||||
-1
|
||||
|
||||
:else
|
||||
(compare (key-leaf key-a) (key-leaf key-b)))))
|
||||
|
||||
(defn- render-dict
|
||||
[entries]
|
||||
(let [sorted-entries (sort #(neg? (compare-dict-keys (:key %1) (:key %2))) entries)
|
||||
lines (loop [remaining sorted-entries
|
||||
previous-namespace nil
|
||||
acc ["{"]]
|
||||
(if-let [{:keys [key] :as entry} (first remaining)]
|
||||
(let [current-namespace (namespace key)
|
||||
acc (cond-> acc
|
||||
(and previous-namespace
|
||||
(not= previous-namespace current-namespace))
|
||||
(conj "")
|
||||
true
|
||||
(conj (render-dict-entry entry)))]
|
||||
(recur (next remaining) current-namespace acc))
|
||||
(conj acc "}")))]
|
||||
(str (string/join "\n" lines) "\n")))
|
||||
|
||||
(defn- dict-file-paths
|
||||
[]
|
||||
(->> (fs/list-dir dicts-dir)
|
||||
(filter #(string/ends-with? (str %) ".edn"))
|
||||
(sort-by fs/file-name)))
|
||||
|
||||
(defn format-dicts
|
||||
"Formats dictionary files by full-key sort order and inserts a blank line
|
||||
between namespace groups. Use --check to fail when any file would change."
|
||||
[& args]
|
||||
(let [check? (contains? (set args) "--check")
|
||||
changed? (volatile! false)]
|
||||
(doseq [path (dict-file-paths)]
|
||||
(let [file-name (fs/file-name path)
|
||||
current-text (slurp (str path))
|
||||
output-text (-> current-text
|
||||
parse-dict-entries
|
||||
render-dict)]
|
||||
(if (= current-text output-text)
|
||||
(println file-name ": already formatted")
|
||||
(do
|
||||
(vreset! changed? true)
|
||||
(if check?
|
||||
(println file-name "would change")
|
||||
(do
|
||||
(spit (str path) output-text)
|
||||
(println file-name ": formatted")))))))
|
||||
(when (and check? @changed?)
|
||||
(System/exit 1))))
|
||||
|
||||
(defn- validate-non-default-languages
|
||||
"This validation finds any translation keys that don't exist in the default
|
||||
language English. Logseq needs to work out of the box with its default
|
||||
language. This catches mistakes where another language has accidentally typoed
|
||||
keys or added ones without updating :en"
|
||||
[{:keys [fix?]}]
|
||||
[fix?]
|
||||
(let [dicts (get-dicts)
|
||||
;; For now defined as :en but clj-kondo analysis could be more thorough
|
||||
valid-keys (set (keys (dicts :en)))
|
||||
invalid-dicts
|
||||
(->> (dissoc dicts :en)
|
||||
(mapcat (fn [[lang get-dicts]]
|
||||
(mapcat (fn [[lang lang-dicts]]
|
||||
(map
|
||||
#(hash-map :language lang :invalid-key %)
|
||||
(set/difference (set (keys get-dicts))
|
||||
(set/difference (set (keys lang-dicts))
|
||||
valid-keys)))))]
|
||||
(if (empty? invalid-dicts)
|
||||
(println "All non-default translations have valid keys!")
|
||||
@@ -107,131 +246,83 @@
|
||||
(when fix?
|
||||
(delete-invalid-non-default-languages
|
||||
(update-vals (group-by :language invalid-dicts) #(map :invalid-key %)))
|
||||
(println "These invalid non-language keys have been removed."))
|
||||
(println "These invalid translation keys have been removed from non-default dictionaries."))
|
||||
(System/exit 1)))))
|
||||
|
||||
;; Command to check for manual entries:
|
||||
;; grep -E -oh '\(t [^ ):]+' -r src/main
|
||||
(def manual-ui-dicts
|
||||
"Manual list of ui translations because they are dynamic i.e. keyword isn't
|
||||
first arg. Only map values are used in linter as keys are for easily scanning
|
||||
grep result."
|
||||
(def ^:private i18n-lint-launcher-path
|
||||
(fs/absolutize "bin/logseq-i18n-lint"))
|
||||
|
||||
{"(t (shortcut-helper/decorate-namespace" [] ;; shortcuts related so can ignore
|
||||
"(t (keyword" [:color/yellow :color/red :color/pink :color/green :color/blue
|
||||
:color/purple :color/gray]
|
||||
"(tt (keyword" [:left-side-bar/assets :left-side-bar/tasks]
|
||||
(def ^:private i18n-lint-config-path
|
||||
(fs/absolutize ".i18n-lint.toml"))
|
||||
|
||||
;; from 3 files
|
||||
"(t (if" [:asset/show-in-folder :asset/open-in-browser
|
||||
:search-item/page
|
||||
:page/make-private :page/make-public]
|
||||
"(t (name" [] ;; shortcuts related
|
||||
"(t (dh/decorate-namespace" [] ;; shortcuts related
|
||||
"(t prompt-key" [:select/default-prompt :select/default-select-multiple :select.graph/prompt]
|
||||
;; All args to ui/make-confirm-modal are not keywords
|
||||
"(t title" []
|
||||
"(t (or title-key" [:views.table/live-query-title :views.table/default-title :all-pages/table-title]
|
||||
"(t subtitle" [:asset/physical-delete]})
|
||||
|
||||
(defn- delete-not-used-key-from-dict-file
|
||||
[invalid-keys]
|
||||
(let [paths (fs/list-dir "src/resources/dicts")]
|
||||
(doseq [path paths]
|
||||
(let [result (rewrite/parse-string (String. (fs/read-all-bytes path)))
|
||||
new-content (str (reduce
|
||||
(fn [result k]
|
||||
(rewrite/dissoc result k))
|
||||
result invalid-keys))]
|
||||
(spit (fs/file path) new-content)))))
|
||||
|
||||
(defn- validate-ui-translations-are-used
|
||||
"This validation checks to see that translations done by (t ...) are equal to
|
||||
the ones defined for the default :en lang. This catches translations that have
|
||||
been added in UI but don't have an entry or translations no longer used in the UI"
|
||||
[{:keys [fix?]}]
|
||||
(let [actual-dicts (->> (shell {:out :string}
|
||||
;; This currently assumes all ui translations
|
||||
;; use (t and src/main. This can easily be
|
||||
;; tweaked as needed
|
||||
"grep -E -oh '\\(tt? :[^ )]+' -r src/main")
|
||||
:out
|
||||
string/split-lines
|
||||
(map #(keyword (subs % 4)))
|
||||
(concat (mapcat val manual-ui-dicts))
|
||||
;; Temporarily unused as they will be brought back soon
|
||||
(concat [:download])
|
||||
set)
|
||||
expected-dicts (set (remove #(re-find #"^(command|shortcut)\." (str (namespace %)))
|
||||
(keys (:en (get-dicts)))))
|
||||
actual-only (set/difference actual-dicts expected-dicts)
|
||||
expected-only (set/difference expected-dicts actual-dicts)]
|
||||
(if (and (empty? actual-only) (empty? expected-only))
|
||||
(println "All defined :en translation keys match the ones that are used!")
|
||||
(do
|
||||
(when (seq actual-only)
|
||||
(println "\nThese translation keys are invalid because they are used in the UI but not defined:")
|
||||
(task-util/print-table (map #(hash-map :invalid-key %) actual-only)))
|
||||
(when (seq expected-only)
|
||||
(println "\nThese translation keys are invalid because they are not used in the UI:")
|
||||
(task-util/print-table (map #(hash-map :invalid-key %) expected-only))
|
||||
(when fix?
|
||||
(delete-not-used-key-from-dict-file expected-only)
|
||||
(println "These invalid ui keys have been removed.")))
|
||||
(System/exit 1)))))
|
||||
|
||||
(def allowed-duplicates
|
||||
"Allows certain keys in a language to have the same translation
|
||||
as English. Happens more in romance languages but pretty rare otherwise"
|
||||
{:fr #{:port :type :help/docs :search-item/page :shortcut.category/navigating :text/image
|
||||
:settings-of-plugins :code :shortcut.category/plugins}
|
||||
:de #{:graph :host :plugins :port
|
||||
:settings-of-plugins :shortcut.category/navigating
|
||||
:settings-page/enable-tooltip :settings-page/plugin-system}
|
||||
:ca #{:port :settings-page/tab-editor :settings-page/tab-general}
|
||||
:es #{:settings-page/tab-general :settings-page/tab-editor}
|
||||
:it #{:home :handbook/home :host :help/awesome-logseq
|
||||
:settings-page/tab-account :settings-page/tab-editor}
|
||||
:nl #{:plugins :type :left-side-bar/nav-recent-pages :plugin/update}
|
||||
:pl #{:port :home :host :plugin/marketplace}
|
||||
:pt-BR #{:plugins :right-side-bar/flashcards :settings-page/enable-flashcards :page/backlinks
|
||||
:host :settings-page/tab-editor :shortcut.category/plugins :settings-of-plugins
|
||||
:on-boarding/quick-tour-journal-page-desc-2 :plugin/downloads :plugin/popular
|
||||
:settings-page/plugin-system}
|
||||
:pt-PT #{:plugins :settings-of-plugins :plugin/downloads :right-side-bar/flashcards
|
||||
:settings-page/enable-flashcards :settings-page/plugin-system}
|
||||
:nb-NO #{:port :type :right-side-bar/flashcards :settings-page/enable-flashcards
|
||||
:settings-page/tab-editor :linked-references/filter-heading}
|
||||
:tr #{:help/awesome-logseq}
|
||||
:id #{:host :port}
|
||||
:cs #{:host :port :help/blog :settings-page/tab-editor}})
|
||||
|
||||
(defn- validate-languages-dont-have-duplicates
|
||||
"Looks up duplicates for all languages"
|
||||
(defn- ensure-i18n-lint-ready!
|
||||
[]
|
||||
(let [dicts (get-dicts)
|
||||
en-dicts (dicts :en)
|
||||
invalid-dicts
|
||||
(->> (dissoc dicts :en)
|
||||
(mapcat
|
||||
(fn [[lang lang-dicts]]
|
||||
(keep
|
||||
#(when (= (en-dicts %) (lang-dicts %))
|
||||
{:translation-key %
|
||||
:lang lang
|
||||
:duplicate-value (shorten (lang-dicts %) 70)})
|
||||
(keys (apply dissoc lang-dicts (allowed-duplicates lang))))))
|
||||
(sort-by (juxt :lang :translation-key)))]
|
||||
(when-not (fs/exists? i18n-lint-launcher-path)
|
||||
(println "logseq-i18n-lint launcher not found at" (str i18n-lint-launcher-path))
|
||||
(System/exit 1))
|
||||
(when-not (fs/exists? i18n-lint-config-path)
|
||||
(println "i18n lint config not found at" (str i18n-lint-config-path))
|
||||
(System/exit 1)))
|
||||
|
||||
(defn- run-i18n-lint-command!
|
||||
[subcommand cli-args]
|
||||
(ensure-i18n-lint-ready!)
|
||||
(let [cmd (into ["bash"
|
||||
(str i18n-lint-launcher-path)
|
||||
"-c"
|
||||
(str i18n-lint-config-path)
|
||||
subcommand]
|
||||
cli-args)
|
||||
result (apply shell {:continue true
|
||||
:out :inherit
|
||||
:err :inherit}
|
||||
cmd)]
|
||||
(when (pos? (:exit result))
|
||||
(System/exit (:exit result)))))
|
||||
|
||||
(defn- check-translation-keys
|
||||
"Use logseq-i18n-lint to detect unused translation keys."
|
||||
[args]
|
||||
(run-i18n-lint-command! "check-keys" args))
|
||||
|
||||
(defn- validate-rich-translations
|
||||
"Checks that localized rich translations remain rich zero-arg functions.
|
||||
Missing translations are allowed, but once a locale defines a rich key it
|
||||
must preserve the same renderable contract as English."
|
||||
[]
|
||||
(let [invalid-dicts (lang-lint/rich-translation-mismatch-findings (get-dicts))]
|
||||
(if (empty? invalid-dicts)
|
||||
(println "All languages have no duplicate English values!")
|
||||
(println "All rich translations preserve English render contracts!")
|
||||
(do
|
||||
(println "These translations keys are invalid because they are just copying the English value:")
|
||||
(println "These translation keys are invalid because they no longer preserve English rich render contracts:")
|
||||
(task-util/print-table invalid-dicts)
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn- validate-translation-placeholders
|
||||
"Checks that every localized string uses the same placeholder set as English.
|
||||
Missing translations are allowed because Tongue falls back to :en, but once
|
||||
a locale defines a string it must preserve the placeholder contract."
|
||||
[]
|
||||
(let [invalid-dicts (lang-lint/placeholder-mismatch-findings (get-dicts))]
|
||||
(if (empty? invalid-dicts)
|
||||
(println "All translations preserve English placeholder contracts!")
|
||||
(do
|
||||
(println "These translation keys are invalid because their placeholders do not match English:")
|
||||
(task-util/print-table
|
||||
(map #(dissoc % :default-value :localized-value) invalid-dicts))
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn validate-translations
|
||||
"Runs multiple translation validations that fail fast if one of them is invalid"
|
||||
[& args]
|
||||
(validate-non-default-languages {:fix? (contains? (set args) "--fix")})
|
||||
(validate-ui-translations-are-used {:fix? (contains? (set args) "--fix")})
|
||||
(validate-languages-dont-have-duplicates))
|
||||
(validate-non-default-languages (contains? (set args) "--fix"))
|
||||
(check-translation-keys args)
|
||||
(validate-rich-translations)
|
||||
(validate-translation-placeholders))
|
||||
|
||||
(defn lint-hardcoded
|
||||
"Run logseq-i18n-lint to lint likely hardcoded user-facing strings in UI-oriented source files.
|
||||
Use -w or --warn-only to report findings without failing and -g or --git-changed to scan
|
||||
only files changed in git status."
|
||||
[& args]
|
||||
(run-i18n-lint-command! "lint" args))
|
||||
|
||||
149
scripts/src/logseq/tasks/lang_lint.cljc
Normal file
149
scripts/src/logseq/tasks/lang_lint.cljc
Normal file
@@ -0,0 +1,149 @@
|
||||
(ns logseq.tasks.lang-lint)
|
||||
|
||||
;; Matches numbered placeholders like `{1}` in translation strings.
|
||||
(def ^:private translation-placeholder-pattern
|
||||
#"\{(\d+)\}")
|
||||
|
||||
(defn translation-placeholders
|
||||
"Return the placeholder indexes referenced by translation string `value`.
|
||||
|
||||
Non-string values return an empty set."
|
||||
[value]
|
||||
(if (string? value)
|
||||
(->> (re-seq translation-placeholder-pattern value)
|
||||
(map second)
|
||||
set)
|
||||
#{}))
|
||||
|
||||
(defn- placeholders-compatible?
|
||||
[default-value localized-value]
|
||||
(or (not (string? default-value))
|
||||
(not (string? localized-value))
|
||||
(= (translation-placeholders default-value)
|
||||
(translation-placeholders localized-value))))
|
||||
|
||||
(defn placeholder-mismatch-findings
|
||||
"Return localized string findings whose placeholder set diverges from
|
||||
English."
|
||||
[dicts]
|
||||
(let [en-dicts (:en dicts)]
|
||||
(->> (dissoc dicts :en)
|
||||
(mapcat
|
||||
(fn [[lang lang-dicts]]
|
||||
(keep (fn [[translation-key localized-value]]
|
||||
(let [default-value (get en-dicts translation-key)]
|
||||
(when (and (string? default-value)
|
||||
(string? localized-value)
|
||||
(not (placeholders-compatible? default-value localized-value)))
|
||||
{:lang lang
|
||||
:translation-key translation-key
|
||||
:expected-placeholders (sort (translation-placeholders default-value))
|
||||
:actual-placeholders (sort (translation-placeholders localized-value))
|
||||
:default-value default-value
|
||||
:localized-value localized-value})))
|
||||
lang-dicts)))
|
||||
(sort-by (juxt :lang :translation-key))
|
||||
vec)))
|
||||
|
||||
(defn- rich-translation-value?
|
||||
"Return true when `value` preserves the rich zero-arg translation contract.
|
||||
|
||||
Babashka reads dictionary `fn` forms as lists, while tests may pass actual
|
||||
function values, so both representations are treated as rich translations."
|
||||
[value]
|
||||
(or (fn? value)
|
||||
(and (seq? value)
|
||||
(= 'fn (first value))
|
||||
(= [] (second value)))))
|
||||
|
||||
(defn- value-kind
|
||||
[value]
|
||||
(cond
|
||||
(rich-translation-value? value) :fn
|
||||
(string? value) :string
|
||||
(nil? value) :nil
|
||||
:else :other))
|
||||
|
||||
(defn rich-translation-mismatch-findings
|
||||
"Return localized rich-translation findings whose value kind no longer
|
||||
matches the English zero-arg function contract."
|
||||
[dicts]
|
||||
(let [en-dicts (:en dicts)]
|
||||
(->> (dissoc dicts :en)
|
||||
(mapcat
|
||||
(fn [[lang lang-dicts]]
|
||||
(keep (fn [[translation-key default-value]]
|
||||
(let [localized-present? (contains? lang-dicts translation-key)
|
||||
localized-value (get lang-dicts translation-key)]
|
||||
(when (and localized-present?
|
||||
(rich-translation-value? default-value)
|
||||
(not (rich-translation-value? localized-value)))
|
||||
{:lang lang
|
||||
:translation-key translation-key
|
||||
:expected-value-kind :fn
|
||||
:actual-value-kind (value-kind localized-value)})))
|
||||
en-dicts)))
|
||||
(sort-by (juxt :lang :translation-key))
|
||||
vec)))
|
||||
|
||||
(defn identical-translation-findings
|
||||
"Return localized translation findings whose defined value is identical to
|
||||
English for the same key."
|
||||
[dicts lang]
|
||||
(let [en-dicts (:en dicts)
|
||||
lang-dicts (get dicts lang)]
|
||||
(->> lang-dicts
|
||||
(keep (fn [[translation-key localized-value]]
|
||||
(let [default-value (get en-dicts translation-key ::missing)]
|
||||
(when (= default-value localized-value)
|
||||
{:lang lang
|
||||
:translation-key translation-key
|
||||
:default-value default-value}))))
|
||||
(sort-by :translation-key)
|
||||
vec)))
|
||||
|
||||
(defn identical-translation-stats
|
||||
"Return per-locale identical-to-English counts for defined translations."
|
||||
[dicts]
|
||||
(let [en-dicts (:en dicts)]
|
||||
(->> dicts
|
||||
(map (fn [[lang lang-dicts]]
|
||||
{:lang lang
|
||||
:translation-count (count lang-dicts)
|
||||
:same-as-en-count
|
||||
(count
|
||||
(filter (fn [[translation-key localized-value]]
|
||||
(= (get en-dicts translation-key ::missing)
|
||||
localized-value))
|
||||
lang-dicts))}))
|
||||
(sort-by (juxt (comp - :same-as-en-count) (comp - :translation-count) :lang))
|
||||
vec)))
|
||||
|
||||
(defn translation-summary-stats
|
||||
"Return per-locale translation summary stats for overview tables."
|
||||
[dicts]
|
||||
(let [en-count (count (:en dicts))
|
||||
same-as-en-counts (->> (identical-translation-stats dicts)
|
||||
(map (juxt :lang :same-as-en-count))
|
||||
(into {}))]
|
||||
(->> dicts
|
||||
(map (fn [[lang lang-dicts]]
|
||||
{:lang lang
|
||||
:translation-count (count lang-dicts)
|
||||
:untranslated-count (when-not (= lang :en)
|
||||
(max 0 (- en-count (count lang-dicts))))
|
||||
:same-as-en-count (when-not (= lang :en)
|
||||
(get same-as-en-counts lang 0))}))
|
||||
vec)))
|
||||
|
||||
(defn sort-translation-summary-stats
|
||||
"Sort translation summary stats with English first, then by untranslated
|
||||
count descending, then by identical-to-English count descending."
|
||||
[stats]
|
||||
(let [en-stats (filter #(= :en (:lang %)) stats)
|
||||
other-stats (remove #(= :en (:lang %)) stats)]
|
||||
(into (vec en-stats)
|
||||
(sort-by (juxt (comp - :untranslated-count)
|
||||
(comp - :same-as-en-count)
|
||||
:lang)
|
||||
other-stats))))
|
||||
@@ -1,21 +0,0 @@
|
||||
(ns logseq.tasks.util
|
||||
"Utils for tasks"
|
||||
(:require [clojure.pprint :as pprint]
|
||||
[babashka.fs :as fs]))
|
||||
|
||||
(defn file-modified-later-than?
|
||||
[file comparison-instant]
|
||||
(pos? (.compareTo (fs/file-time->instant (fs/last-modified-time file))
|
||||
comparison-instant)))
|
||||
|
||||
(defn print-usage [arg-str]
|
||||
(println (format
|
||||
"Usage: bb %s %s"
|
||||
(System/getProperty "babashka.task")
|
||||
arg-str))
|
||||
(System/exit 1))
|
||||
|
||||
(defn print-table
|
||||
[rows]
|
||||
(pprint/print-table rows)
|
||||
(println "Total:" (count rows)))
|
||||
123
scripts/src/logseq/tasks/util.cljc
Normal file
123
scripts/src/logseq/tasks/util.cljc
Normal file
@@ -0,0 +1,123 @@
|
||||
(ns logseq.tasks.util
|
||||
"Utils for tasks"
|
||||
(:require [clojure.string :as string]
|
||||
#?(:clj [babashka.fs :as fs])))
|
||||
|
||||
(defn- in-range?
|
||||
[code-point [start end]]
|
||||
(<= start code-point end))
|
||||
|
||||
(def ^:private zero-width-ranges
|
||||
[[0x0300 0x036F]
|
||||
[0x1AB0 0x1AFF]
|
||||
[0x1DC0 0x1DFF]
|
||||
[0x200C 0x200F]
|
||||
[0x202A 0x202E]
|
||||
[0x2060 0x206F]
|
||||
[0x20D0 0x20FF]
|
||||
[0xFE00 0xFE0F]
|
||||
[0xFE20 0xFE2F]])
|
||||
|
||||
(def ^:private wide-ranges
|
||||
[[0x1100 0x115F]
|
||||
[0x2329 0x232A]
|
||||
[0x2E80 0xA4CF]
|
||||
[0xAC00 0xD7A3]
|
||||
[0xF900 0xFAFF]
|
||||
[0xFE10 0xFE19]
|
||||
[0xFE30 0xFE6F]
|
||||
[0xFF00 0xFF60]
|
||||
[0xFFE0 0xFFE6]
|
||||
[0x1F300 0x1FAFF]
|
||||
[0x20000 0x3FFFD]])
|
||||
|
||||
(defn- code-point-width
|
||||
[code-point]
|
||||
(cond
|
||||
(or (<= 0x0000 code-point 0x001F)
|
||||
(<= 0x007F code-point 0x009F)
|
||||
(some #(in-range? code-point %) zero-width-ranges))
|
||||
0
|
||||
|
||||
(some #(in-range? code-point %) wide-ranges)
|
||||
2
|
||||
|
||||
:else
|
||||
1))
|
||||
|
||||
(defn display-width
|
||||
[value]
|
||||
(let [text (str value)]
|
||||
(loop [index 0
|
||||
width 0]
|
||||
(if (< index #?(:clj (.length text) :cljs (.-length text)))
|
||||
(let [code-point #?(:clj (.codePointAt text index)
|
||||
:cljs (.codePointAt text index))]
|
||||
(recur (+ index #?(:clj (Character/charCount code-point)
|
||||
:cljs (if (> code-point 0xFFFF) 2 1)))
|
||||
(+ width (code-point-width code-point))))
|
||||
width))))
|
||||
|
||||
(defn- pad-left
|
||||
[text width]
|
||||
(let [padding (- width (display-width text))]
|
||||
(str (apply str (repeat (max 0 padding) " ")) text)))
|
||||
|
||||
(defn- column-widths
|
||||
[columns rows]
|
||||
(reduce
|
||||
(fn [widths column]
|
||||
(assoc widths
|
||||
column
|
||||
(apply max
|
||||
(display-width (str column))
|
||||
(map #(display-width (str (get % column ""))) rows))))
|
||||
{}
|
||||
columns))
|
||||
|
||||
(defn- render-separator
|
||||
[columns widths]
|
||||
(str "|" (string/join "+" (map #(apply str (repeat (+ 2 (get widths %)) "-")) columns)) "|"))
|
||||
|
||||
(defn- render-row
|
||||
[columns widths row]
|
||||
(str "|"
|
||||
(string/join "|" (map #(str " "
|
||||
(pad-left (str (get row % "")) (get widths %))
|
||||
" ")
|
||||
columns))
|
||||
"|"))
|
||||
|
||||
(defn render-table
|
||||
[rows]
|
||||
(when-let [columns (seq (keys (first rows)))]
|
||||
(let [rows (vec rows)
|
||||
widths (column-widths columns rows)
|
||||
header-row (zipmap columns columns)]
|
||||
(str (string/join
|
||||
"\n"
|
||||
(concat [(render-row columns widths header-row)
|
||||
(render-separator columns widths)]
|
||||
(map #(render-row columns widths %) rows)))
|
||||
"\n"))))
|
||||
|
||||
#?(:clj
|
||||
(defn file-modified-later-than?
|
||||
[file comparison-instant]
|
||||
(pos? (.compareTo (fs/file-time->instant (fs/last-modified-time file))
|
||||
comparison-instant))))
|
||||
|
||||
(defn print-usage [arg-str]
|
||||
(println (str "Usage: bb "
|
||||
#?(:clj (System/getProperty "babashka.task")
|
||||
:cljs "task")
|
||||
" "
|
||||
arg-str))
|
||||
#?(:clj (System/exit 1)
|
||||
:cljs (js/process.exit 1)))
|
||||
|
||||
(defn print-table
|
||||
[rows]
|
||||
(when-some [rendered-table (render-table rows)]
|
||||
(print rendered-table))
|
||||
(println "Total:" (count rows)))
|
||||
@@ -15,8 +15,8 @@
|
||||
(map (comp :block/title :page) batch)))
|
||||
(is (= ["id-0" "id-4"]
|
||||
(map (comp :block/uuid :page) batch)))
|
||||
(is (= [["Block" "Block" "Block"]
|
||||
["Block" "Block" "Block"]]
|
||||
(is (= [[(#'sut/build-block-title 10 0) (#'sut/build-block-title 10 1) (#'sut/build-block-title 10 2)]
|
||||
[(#'sut/build-block-title 11 0) (#'sut/build-block-title 11 1) (#'sut/build-block-title 11 2)]]
|
||||
(map (fn [{:keys [blocks]}]
|
||||
(mapv :block/title blocks))
|
||||
batch)))
|
||||
|
||||
127
scripts/test/logseq/tasks/lang_test.cljs
Normal file
127
scripts/test/logseq/tasks/lang_test.cljs
Normal file
@@ -0,0 +1,127 @@
|
||||
(ns logseq.tasks.lang-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[logseq.tasks.lang-lint :as lang-lint]))
|
||||
|
||||
(deftest translation-placeholders-detect-placeholder-sets
|
||||
(is (= #{"1" "2"}
|
||||
(lang-lint/translation-placeholders "Open {1} from {2}")))
|
||||
(is (= #{}
|
||||
(lang-lint/translation-placeholders "Search with Google"))))
|
||||
|
||||
(deftest placeholder-mismatch-findings-detect-non-default-locale-errors
|
||||
(testing "a localized value must match English placeholders exactly once it is defined"
|
||||
(let [findings (lang-lint/placeholder-mismatch-findings
|
||||
{:en {:electron/link-open-confirm "Are you sure?\n\n{1}"
|
||||
:electron/write-file-failed-with-backup "Write failed {1} {2} {3}."}
|
||||
:zh-Hant {:electron/link-open-confirm "確定要開啟此外部連結嗎?"
|
||||
:electron/write-file-failed-with-backup "寫入失敗。備份檔案:{1}"}
|
||||
:zh-CN {:electron/link-open-confirm "确定要打开此链接吗?\n\n{1}"
|
||||
:electron/write-file-failed-with-backup "写入文件 {1} 失败,{2}。备份文件已保存到 {3}。"}})]
|
||||
(is (= [{:lang :zh-Hant
|
||||
:translation-key :electron/link-open-confirm
|
||||
:expected-placeholders ["1"]
|
||||
:actual-placeholders []
|
||||
:default-value "Are you sure?\n\n{1}"
|
||||
:localized-value "確定要開啟此外部連結嗎?"}
|
||||
{:lang :zh-Hant
|
||||
:translation-key :electron/write-file-failed-with-backup
|
||||
:expected-placeholders ["1" "2" "3"]
|
||||
:actual-placeholders ["1"]
|
||||
:default-value "Write failed {1} {2} {3}."
|
||||
:localized-value "寫入失敗。備份檔案:{1}"}]
|
||||
findings)))))
|
||||
|
||||
(deftest translation-rich-validation-findings-report-rich-contract-mismatches
|
||||
(testing "a localized rich translation must remain a zero-arg function once defined"
|
||||
(is (= [{:lang :zh-Hant
|
||||
:translation-key :e2ee/cloud-password-rich
|
||||
:expected-value-kind :fn
|
||||
:actual-value-kind :string}
|
||||
{:lang :zh-Hant
|
||||
:translation-key :on-boarding/main-title
|
||||
:expected-value-kind :fn
|
||||
:actual-value-kind :string}]
|
||||
(lang-lint/rich-translation-mismatch-findings
|
||||
{:en {:on-boarding/main-title (fn [] ["Welcome to " [:strong "Logseq!"]])
|
||||
:e2ee/cloud-password-rich (fn [] ["Cloud sentence " [:span "Local sentence"]])
|
||||
:e2ee/remember-password-rich (fn [] [[:span "Remember "] "your password."])}
|
||||
:zh-Hant {:on-boarding/main-title "歡迎使用 Logseq"
|
||||
:e2ee/cloud-password-rich "雲端密碼"
|
||||
:e2ee/remember-password-rich (fn [] [[:span "請記住"] "你的密碼。"])}})))))
|
||||
|
||||
(deftest identical-translation-findings-report-defined-values-equal-to-english
|
||||
(is (= [{:lang :ko
|
||||
:translation-key :ui/cancel
|
||||
:default-value "Cancel"}
|
||||
{:lang :ko
|
||||
:translation-key :ui/save
|
||||
:default-value "Save"}]
|
||||
(lang-lint/identical-translation-findings
|
||||
{:en {:ui/cancel "Cancel"
|
||||
:ui/save "Save"
|
||||
:ui/close "Close"}
|
||||
:ko {:ui/cancel "Cancel"
|
||||
:ui/save "Save"
|
||||
:ui/close "닫기"}
|
||||
:fr {:ui/cancel "Annuler"}}
|
||||
:ko))))
|
||||
|
||||
(deftest identical-translation-stats-count-defined-values-equal-to-english
|
||||
(is (= [{:lang :en
|
||||
:translation-count 2
|
||||
:same-as-en-count 2}
|
||||
{:lang :ko
|
||||
:translation-count 2
|
||||
:same-as-en-count 1}
|
||||
{:lang :fr
|
||||
:translation-count 1
|
||||
:same-as-en-count 0}]
|
||||
(lang-lint/identical-translation-stats
|
||||
{:en {:ui/cancel "Cancel"
|
||||
:ui/save "Save"}
|
||||
:ko {:ui/cancel "Cancel"
|
||||
:ui/save "저장"}
|
||||
:fr {:ui/cancel "Annuler"}}))))
|
||||
|
||||
(deftest translation-summary-stats-report-untranslated-and-same-as-en-count
|
||||
(is (= [{:lang :en
|
||||
:translation-count 4
|
||||
:untranslated-count nil
|
||||
:same-as-en-count nil}
|
||||
{:lang :fr
|
||||
:translation-count 2
|
||||
:untranslated-count 2
|
||||
:same-as-en-count 1}
|
||||
{:lang :ko
|
||||
:translation-count 3
|
||||
:untranslated-count 1
|
||||
:same-as-en-count 2}]
|
||||
(->> (lang-lint/translation-summary-stats
|
||||
{:en {:ui/cancel "Cancel"
|
||||
:ui/save "Save"
|
||||
:ui/close "Close"
|
||||
:ui/delete "Delete"}
|
||||
:ko {:ui/cancel "Cancel"
|
||||
:ui/save "저장"
|
||||
:ui/close "Close"}
|
||||
:fr {:ui/cancel "Annuler"
|
||||
:ui/save "Save"}})
|
||||
(sort-by :lang)
|
||||
vec))))
|
||||
|
||||
(deftest sort-translation-summary-stats-keeps-en-first-then-sorts-by-untranslated-and-same-as-en-count
|
||||
(is (= [:en :ko :fr :zh-Hant]
|
||||
(->> [{:lang :fr
|
||||
:untranslated-count 3
|
||||
:same-as-en-count 1}
|
||||
{:lang :en
|
||||
:untranslated-count nil
|
||||
:same-as-en-count nil}
|
||||
{:lang :zh-Hant
|
||||
:untranslated-count 1
|
||||
:same-as-en-count 3}
|
||||
{:lang :ko
|
||||
:untranslated-count 3
|
||||
:same-as-en-count 2}]
|
||||
lang-lint/sort-translation-summary-stats
|
||||
(mapv :lang)))))
|
||||
@@ -1,11 +1,15 @@
|
||||
(ns logseq.tasks.test-runner
|
||||
(:require [cljs.test :as test]
|
||||
[logseq.tasks.db-graph.create-graph-with-clojure-irc-history-test]
|
||||
[logseq.tasks.db-graph.create-graph-with-large-sizes-test]))
|
||||
[logseq.tasks.db-graph.create-graph-with-large-sizes-test]
|
||||
[logseq.tasks.lang-test]
|
||||
[logseq.tasks.util-test]))
|
||||
|
||||
(defn -main [& _]
|
||||
(let [{:keys [fail error]}
|
||||
(test/run-tests 'logseq.tasks.db-graph.create-graph-with-large-sizes-test
|
||||
'logseq.tasks.db-graph.create-graph-with-clojure-irc-history-test)]
|
||||
'logseq.tasks.db-graph.create-graph-with-clojure-irc-history-test
|
||||
'logseq.tasks.lang-test
|
||||
'logseq.tasks.util-test)]
|
||||
(when (pos? (+ fail error))
|
||||
(js/process.exit 1))))
|
||||
|
||||
19
scripts/test/logseq/tasks/util_test.cljs
Normal file
19
scripts/test/logseq/tasks/util_test.cljs
Normal file
@@ -0,0 +1,19 @@
|
||||
(ns logseq.tasks.util-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[logseq.tasks.util :as util]))
|
||||
|
||||
(deftest display-width-handles-wide-and-combining-characters
|
||||
(is (= 5 (util/display-width "hello")))
|
||||
(is (= 4 (util/display-width "中文")))
|
||||
(is (= 4 (util/display-width "한국")))
|
||||
(is (= 1 (util/display-width "e\u0301"))))
|
||||
|
||||
(deftest render-table-right-aligns-columns-using-display-width
|
||||
(is (= (str "| :locale | :language |\n"
|
||||
"|---------+-----------|\n"
|
||||
"| :en | English |\n"
|
||||
"| :ja | 日本語 |\n"
|
||||
"| :ko | 한국 |\n")
|
||||
(util/render-table [{:locale :en :language "English"}
|
||||
{:locale :ja :language "日本語"}
|
||||
{:locale :ko :language "한국"}]))))
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns electron.context-menu
|
||||
(:require [electron.utils :as utils]
|
||||
(:require [electron.i18n :refer [t]]
|
||||
[electron.utils :as utils]
|
||||
["electron" :refer [Menu MenuItem shell nativeImage clipboard] :as electron]
|
||||
["electron-dl" :refer [download]]))
|
||||
|
||||
@@ -27,19 +28,18 @@
|
||||
#(. web-contents replaceMisspelling suggestion)}))))
|
||||
(when-let [misspelled-word (not-empty (.-misspelledWord params))]
|
||||
(. menu append
|
||||
(MenuItem. (clj->js {:label
|
||||
"Add to dictionary"
|
||||
(MenuItem. (clj->js {:label (t :electron/add-to-dictionary)
|
||||
:click
|
||||
#(.. web-contents -session (addWordToSpellCheckerDictionary misspelled-word))})))
|
||||
(. menu append (MenuItem. #js {:type "separator"})))
|
||||
|
||||
(when (and utils/mac? has-text? (not link-url))
|
||||
(. menu append
|
||||
(MenuItem. #js {:label (str "Look Up “" selection-text "”")
|
||||
(MenuItem. #js {:label (t :electron/look-up)
|
||||
:click #(. web-contents showDefinitionForSelection)})))
|
||||
(when has-text?
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Search with Google"
|
||||
(when has-text?
|
||||
(. menu append
|
||||
(MenuItem. #js {:label (t :electron/search-with-google)
|
||||
:click #(let [url (js/URL. "https://www.google.com/search")]
|
||||
(.. url -searchParams (set "q" selection-text))
|
||||
(.. shell (openExternal (.toString url))))}))
|
||||
@@ -48,26 +48,26 @@
|
||||
(when editable?
|
||||
(when has-text?
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Cut"
|
||||
(MenuItem. #js {:label (t :editor/cut)
|
||||
:enabled (.-canCut edit-flags)
|
||||
:role "cut"}))
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Copy"
|
||||
(MenuItem. #js {:label (t :ui/copy)
|
||||
:enabled (.-canCopy edit-flags)
|
||||
:role "copy"})))
|
||||
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Paste"
|
||||
(MenuItem. #js {:label (t :editor/paste)
|
||||
:enabled (.-canPaste edit-flags)
|
||||
:role "paste"}))
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Select All"
|
||||
(MenuItem. #js {:label (t :view.table/select-all)
|
||||
:enabled (.-canSelectAll edit-flags)
|
||||
:role "selectAll"})))
|
||||
|
||||
(when (= media-type "image")
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Save Image"
|
||||
(MenuItem. #js {:label (t :electron/save-image)
|
||||
:click (fn [menu-item]
|
||||
(let [url (.-srcURL params)
|
||||
url (if (.-transform menu-item)
|
||||
@@ -76,7 +76,7 @@
|
||||
(download win url)))}))
|
||||
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Save Image As..."
|
||||
(MenuItem. #js {:label (t :electron/save-image-as)
|
||||
:click (fn [menu-item]
|
||||
(let [url (.-srcURL params)
|
||||
url (if (.-transform menu-item)
|
||||
@@ -85,7 +85,7 @@
|
||||
(download win url #js {:saveAs true})))}))
|
||||
|
||||
(. menu append
|
||||
(MenuItem. #js {:label "Copy Image"
|
||||
(MenuItem. #js {:label (t :electron/copy-image)
|
||||
:click (fn []
|
||||
(. clipboard writeImage (. nativeImage createFromPath (subs (.-srcURL params) 7))))})))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[electron.db :as db]
|
||||
[electron.exceptions :as exceptions]
|
||||
[electron.handler :as handler]
|
||||
[electron.i18n :as i18n :refer [t]]
|
||||
[electron.logger :as logger]
|
||||
[electron.server :as server]
|
||||
[electron.updater :refer [init-updater] :as updater]
|
||||
@@ -173,7 +174,7 @@
|
||||
(let [about-fn (fn []
|
||||
(.showMessageBox dialog (clj->js {:title "Logseq"
|
||||
:icon (node-path/join js/__dirname "icons/logseq.png")
|
||||
:message (str "Version " updater/electron-version)})))
|
||||
:message (t :electron/version updater/electron-version)})))
|
||||
template (if mac?
|
||||
[{:label (.-name app)
|
||||
:submenu [{:role "about"}
|
||||
@@ -188,7 +189,7 @@
|
||||
[])
|
||||
template (conj template
|
||||
{:role "fileMenu"
|
||||
:submenu [{:label "New Window"
|
||||
:submenu [{:label (t :electron/new-window)
|
||||
:click (fn [] (handler/open-new-window! nil))
|
||||
:accelerator (if mac?
|
||||
"CommandOrControl+N"
|
||||
@@ -211,13 +212,13 @@
|
||||
template (conj template
|
||||
(if mac?
|
||||
{:role "help"
|
||||
:submenu [{:label "Official Documentation"
|
||||
:submenu [{:label (t :electron/official-docs)
|
||||
:click #(.openExternal shell "https://docs.logseq.com/")}]}
|
||||
{:role "help"
|
||||
:submenu [{:label "Official Documentation"
|
||||
:submenu [{:label (t :electron/official-docs)
|
||||
:click #(.openExternal shell "https://docs.logseq.com/")}
|
||||
{:role "about"
|
||||
:label "About Logseq"
|
||||
:label (t :electron/about)
|
||||
:click about-fn}]}))
|
||||
;; Enable Cmd/Ctrl+= Zoom In
|
||||
template (conj template
|
||||
@@ -334,6 +335,7 @@
|
||||
|
||||
(register-default-protocol-client! app)
|
||||
(set-app-menu!)
|
||||
(i18n/on-locale-change! set-app-menu!)
|
||||
(setup-deeplink!)
|
||||
|
||||
(.on app "second-instance"
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
(ns electron.exceptions
|
||||
(:require [electron.logger :as logger]
|
||||
[electron.utils :as utils]
|
||||
[clojure.string :as string]))
|
||||
[electron.utils :as utils]))
|
||||
|
||||
(defonce uncaughtExceptionChan "uncaughtException")
|
||||
|
||||
(defn show-error-tip
|
||||
[& msg]
|
||||
(utils/send-to-renderer "notification"
|
||||
{:type "error"
|
||||
:payload (string/join "\n" msg)}))
|
||||
|
||||
(defn- app-uncaught-handler
|
||||
[^js e]
|
||||
(let [msg (.-message e)
|
||||
stack (.-stack e)]
|
||||
(show-error-tip "[Main Exception]" msg stack))
|
||||
(utils/send-to-renderer "notification"
|
||||
{:type "error"
|
||||
:payload (str "[Main Exception]\n" msg "\n" stack)
|
||||
:i18n-key :electron/main-exception
|
||||
:i18n-args [msg stack]}))
|
||||
|
||||
;; for debug log
|
||||
(logger/error uncaughtExceptionChan (str e)))
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
[electron.db :as db]
|
||||
[electron.find-in-page :as find]
|
||||
[electron.handler-interface :refer [handle]]
|
||||
[electron.i18n :as i18n]
|
||||
[electron.keychain :as keychain]
|
||||
[electron.logger :as logger]
|
||||
[electron.plugin :as plugin]
|
||||
@@ -121,15 +122,16 @@
|
||||
(backup-file/backup-file repo :backup-dir path (node-path/extname path) content)
|
||||
(catch :default e
|
||||
(logger/error ::write-file "backup file failed:" e)))]
|
||||
(utils/send-to-renderer window "notification" {:type "error"
|
||||
:payload (str "Write to the file " path
|
||||
" failed, "
|
||||
e
|
||||
(when backup-path
|
||||
(str ". A backup file was saved to "
|
||||
backup-path
|
||||
".")))}))))))
|
||||
|
||||
(utils/send-to-renderer window "notification"
|
||||
(if backup-path
|
||||
{:type "error"
|
||||
:payload (str "Write to the file " path " failed, " e ". A backup file was saved to " backup-path ".")
|
||||
:i18n-key :electron/write-file-error-with-backup
|
||||
:i18n-args [path e backup-path]}
|
||||
{:type "error"
|
||||
:payload (str "Write to the file " path " failed, " e)
|
||||
:i18n-key :electron/write-file-error
|
||||
:i18n-args [path e]})))))))
|
||||
(defmethod handle :rename [_window [_ old-path new-path]]
|
||||
(logger/info ::rename "from" old-path "to" new-path)
|
||||
(fs/renameSync old-path new-path))
|
||||
@@ -181,9 +183,12 @@
|
||||
:files (get-files path)}))
|
||||
(catch js/Error e
|
||||
(do
|
||||
(utils/send-to-renderer window "notification" {:type "error"
|
||||
:payload (str "Opening the specified directory failed.\n"
|
||||
(or (pretty-print-js-error e) (str "Unexpected error: " e)))})
|
||||
(utils/send-to-renderer window "notification"
|
||||
{:type "error"
|
||||
:payload (str "Opening the specified directory failed.\n"
|
||||
(or (pretty-print-js-error e) (str "Unexpected error: " e)))
|
||||
:i18n-key :electron/open-dir-error
|
||||
:i18n-args [(or (pretty-print-js-error e) (str "Unexpected error: " e))]})
|
||||
(p/rejected e))))
|
||||
|
||||
(p/rejected (js/Error "path empty")))))
|
||||
@@ -313,6 +318,9 @@
|
||||
(when graph-name
|
||||
(set-current-graph! window (utils/get-graph-dir graph-name))))
|
||||
|
||||
(defmethod handle :updateElectronLocale [_window [_ locale]]
|
||||
(i18n/update-locale! locale))
|
||||
|
||||
(defmethod handle :runCli [window [_ {:keys [command args returnResult]}]]
|
||||
(try
|
||||
(let [on-data-handler (fn [message]
|
||||
|
||||
40
src/electron/electron/i18n.cljs
Normal file
40
src/electron/electron/i18n.cljs
Normal file
@@ -0,0 +1,40 @@
|
||||
(ns electron.i18n
|
||||
"I18n support for the Electron main process.
|
||||
|
||||
The renderer only syncs the active locale. The main process loads dictionary
|
||||
resources locally so it can translate with the same Tongue fallback behavior
|
||||
as the renderer without shipping non-serializable translation values over
|
||||
IPC."
|
||||
(:require [frontend.dicts :as dicts]
|
||||
[lambdaisland.glogi :as log]
|
||||
[tongue.core :as tongue]))
|
||||
|
||||
(def ^:private translate
|
||||
(tongue/build-translate (assoc dicts/dicts :tongue/fallback :en)))
|
||||
|
||||
(defonce ^:private *locale (atom :en))
|
||||
(defonce ^:private *on-locale-change (atom nil))
|
||||
|
||||
(defn on-locale-change!
|
||||
"Register a callback to be invoked when translations are updated"
|
||||
[f]
|
||||
(reset! *on-locale-change f))
|
||||
|
||||
(defn update-locale!
|
||||
"Update the active locale from the frontend renderer."
|
||||
[language]
|
||||
(reset! *locale (or (some-> language keyword) :en))
|
||||
(when-let [f @*on-locale-change]
|
||||
(f)))
|
||||
|
||||
(defn t
|
||||
"Translate `k` in the current Electron locale using Tongue fallback rules."
|
||||
[& args]
|
||||
(try
|
||||
(apply translate @*locale args)
|
||||
(catch :default e
|
||||
(log/error :failed-translation {:error e
|
||||
:arguments args
|
||||
:lang @*locale})
|
||||
(when (not= @*locale :en)
|
||||
(apply translate :en args)))))
|
||||
@@ -57,7 +57,7 @@
|
||||
endpoint (api url-suffix)
|
||||
^js res (fetch endpoint {:timeout (* 1000 5)})
|
||||
illegal-text (when-not (= 200 (.-status res)) (.text res))
|
||||
_ (when-not (string/blank? illegal-text) (throw (js/Error. (str "Github API Failed(" (.-status res) ") " illegal-text))))
|
||||
_ (when-not (string/blank? illegal-text) (throw (js/Error. (str "GitHub API Failed(" (.-status res) ") " illegal-text))))
|
||||
_ (debug "Release latest:" endpoint ":status" (.-status res))
|
||||
res (response-transform res)
|
||||
res (.json res)
|
||||
@@ -184,7 +184,7 @@
|
||||
includes the following keys:
|
||||
* :only-check - When set to true, this only fetches the latest version without installing
|
||||
* :plugin-action - When set to 'install', installs the specific :version given
|
||||
* :repo - A Github repo, not a logseq repo, e.g. user/repo"
|
||||
* :repo - A GitHub repo, not a logseq repo, e.g. user/repo"
|
||||
[{:keys [version repo only-check plugin-action] :as item}]
|
||||
(if repo
|
||||
(let [action (keyword plugin-action)
|
||||
|
||||
@@ -25,9 +25,12 @@
|
||||
[graph-identifier]
|
||||
(if (not-empty graph-identifier)
|
||||
(send-to-renderer "notification" {:type "error"
|
||||
:payload (str "Failed to open link. Cannot match graph identifier `" graph-identifier "` to any linked graph.")})
|
||||
:payload (str "Failed to open link. Cannot match graph identifier `" graph-identifier "` to any linked graph.")
|
||||
:i18n-key :electron/link-open-failed-no-graph
|
||||
:i18n-args [graph-identifier]})
|
||||
(send-to-renderer "notification" {:type "error"
|
||||
:payload "Failed to open link. Missing graph identifier after `logseq://graph/`."})))
|
||||
:payload "Failed to open link. Missing graph identifier after `logseq://graph/`."
|
||||
:i18n-key :electron/link-open-failed-missing-graph})))
|
||||
|
||||
(defn local-url-handler
|
||||
"Given a URL with `graph identifier` as path, `page` (optional) and `block-id`
|
||||
@@ -86,7 +89,9 @@
|
||||
(send-to-focused-renderer "notification" {:type "error"
|
||||
:payload (str "Unimplemented x-callback-url action: `"
|
||||
action
|
||||
"`.")} win))))
|
||||
"`.")
|
||||
:i18n-key :electron/unimplemented-callback
|
||||
:i18n-args [action]} win))))
|
||||
|
||||
(defn logseq-url-handler
|
||||
"win - the main window"
|
||||
@@ -112,4 +117,6 @@
|
||||
(send-to-renderer :notification
|
||||
{:type "error"
|
||||
:payload (str "Failed to open link. Cannot match `" url-host
|
||||
"` to any target.")}))))
|
||||
"` to any target.")
|
||||
:i18n-key :electron/link-open-failed-no-target
|
||||
:i18n-args [url-host]}))))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[clojure.string :as string]
|
||||
[electron.configs :as cfgs]
|
||||
[electron.context-menu :as context-menu]
|
||||
[electron.i18n :refer [t]]
|
||||
[electron.logger :as logger]
|
||||
[electron.state :as state]
|
||||
[electron.utils :refer [mac? win32? linux? dev? open] :as utils]))
|
||||
@@ -126,10 +127,10 @@
|
||||
(when-let [^js res (and (fn? default-open)
|
||||
(.showMessageBoxSync dialog
|
||||
#js {:type "warning"
|
||||
:message (str "Are you sure you want to open this link? \n\n" url)
|
||||
:message (t :electron/link-open-confirm url)
|
||||
:defaultId 1
|
||||
:cancelId 0
|
||||
:buttons #js ["Cancel" "OK"]}))]
|
||||
:buttons #js [(t :electron/cancel) (t :electron/ok)]}))]
|
||||
(when (= res 1)
|
||||
(default-open url)))))))
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
[clojure.string :as string]
|
||||
[dommy.core :as dom]
|
||||
[electron.ipc :as ipc]
|
||||
[electron.locale :as electron-locale]
|
||||
[frontend.context.i18n :as i18n]
|
||||
[frontend.db :as db]
|
||||
[frontend.db.async :as db-async]
|
||||
[frontend.handler.notification :as notification]
|
||||
@@ -26,9 +28,13 @@
|
||||
[]
|
||||
(safe-api-call "notification"
|
||||
(fn [data]
|
||||
(let [{:keys [type payload]} (bean/->clj data)
|
||||
(let [{:keys [type payload i18n-key i18n-args]} (bean/->clj data)
|
||||
type (keyword type)
|
||||
comp [:div (str payload)]]
|
||||
i18n-key (when i18n-key (keyword i18n-key))
|
||||
content (if i18n-key
|
||||
(apply i18n/t i18n-key i18n-args)
|
||||
(str payload))
|
||||
comp [:div content]]
|
||||
(notification/show! comp type false))))
|
||||
|
||||
(safe-api-call "rebuildSearchIndice"
|
||||
@@ -67,7 +73,7 @@
|
||||
(p/let [block (db-async/<get-block (state/get-current-repo) block-id {:children? false})]
|
||||
(if block
|
||||
(route-handler/redirect-to-page! block-id)
|
||||
(notification/show! (str "Open link failed. Block-id `" block-id "` doesn't exist in the graph.") :error false)))))))
|
||||
(notification/show! (i18n/t :electron/block-not-exist block-id) :error false)))))))
|
||||
|
||||
(safe-api-call "foundInPage"
|
||||
(fn [data]
|
||||
@@ -133,4 +139,5 @@
|
||||
|
||||
(defn listen!
|
||||
[]
|
||||
(listen-to-electron!))
|
||||
(listen-to-electron!)
|
||||
(electron-locale/push-locale! (state/sub :preferred-language)))
|
||||
|
||||
7
src/main/electron/locale.cljs
Normal file
7
src/main/electron/locale.cljs
Normal file
@@ -0,0 +1,7 @@
|
||||
(ns electron.locale
|
||||
"Electron locale synchronization helpers."
|
||||
(:require [electron.ipc :as ipc]))
|
||||
|
||||
(defn push-locale!
|
||||
[language]
|
||||
(ipc/ipc :updateElectronLocale (or (some-> language keyword) :en)))
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.commands
|
||||
"Provides functionality for commands and advanced commands"
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.context.i18n :refer [interpolate-sentence t]]
|
||||
[frontend.date :as date]
|
||||
[frontend.db :as db]
|
||||
[frontend.extensions.video.youtube :as youtube]
|
||||
@@ -17,6 +18,7 @@
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.util.block-ref :as block-ref]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.graph-parser.property :as gp-property]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -27,9 +29,10 @@
|
||||
(defonce command-ask "\\")
|
||||
(defonce *current-command (atom nil))
|
||||
|
||||
(def query-doc
|
||||
(defn query-doc
|
||||
[]
|
||||
[:div {:on-pointer-down (fn [e] (.stopPropagation e))}
|
||||
[:div.font-medium.text-lg.mb-2 "Query examples:"]
|
||||
[:div.font-medium.text-lg.mb-2 (t :query/examples-title)]
|
||||
[:ul.mb-1
|
||||
[:li.mb-1 [:code "{{query #tag}}"]]
|
||||
[:li.mb-1 [:code "{{query [[page]]}}"]]
|
||||
@@ -39,38 +42,43 @@
|
||||
[:li.mb-1 [:code "{{query (and (between -7d +7d) (task Done))}}"]]
|
||||
[:li.mb-1 [:code "{{query (property key value)}}"]]
|
||||
[:li.mb-1 [:code "{{query (tags #tag)}}"]]]
|
||||
|
||||
[:p "Check more examples at "
|
||||
[:a {:href "https://docs.logseq.com/#/page/queries"
|
||||
:target "_blank"}
|
||||
"Queries documentation"]
|
||||
"."]])
|
||||
[:p
|
||||
(interpolate-sentence
|
||||
(t :query/examples-desc)
|
||||
:links [{:href "https://docs.logseq.com/#/page/queries"
|
||||
:target "_blank"}])]])
|
||||
|
||||
(defn link-steps []
|
||||
[[:editor/input (str command-trigger "link")]
|
||||
[:editor/show-input [{:command :link
|
||||
:id :link
|
||||
:placeholder "Link"
|
||||
:placeholder (t :ui/link)
|
||||
:autoFocus true}
|
||||
{:command :link
|
||||
:id :label
|
||||
:placeholder "Label"}]]])
|
||||
:placeholder (t :ui/label)}]]])
|
||||
|
||||
(defn image-link-steps []
|
||||
[[:editor/input (str command-trigger "link")]
|
||||
[:editor/show-input [{:command :image-link
|
||||
:id :link
|
||||
:placeholder "Link"
|
||||
:placeholder (t :ui/link)
|
||||
:autoFocus true}
|
||||
{:command :image-link
|
||||
:id :label
|
||||
:placeholder "Label"}]]])
|
||||
:placeholder (t :ui/label)}]]])
|
||||
|
||||
(def *extend-slash-commands (atom []))
|
||||
|
||||
(defn register-slash-command [cmd]
|
||||
(swap! *extend-slash-commands conj cmd))
|
||||
|
||||
(defn- resolve-slash-command
|
||||
[command]
|
||||
(if (fn? command)
|
||||
(command)
|
||||
command))
|
||||
|
||||
(defn ->marker
|
||||
[marker]
|
||||
[[:editor/clear-current-slash]
|
||||
@@ -92,8 +100,7 @@
|
||||
|
||||
(defn db-based-statuses
|
||||
[]
|
||||
(map (fn [e] (:block/title e))
|
||||
(db-pu/get-closed-property-values :logseq.property/status)))
|
||||
(db-pu/get-closed-property-values :logseq.property/status))
|
||||
|
||||
(defn db-based-embed-block
|
||||
[]
|
||||
@@ -142,38 +149,41 @@
|
||||
|
||||
(defn get-statuses
|
||||
[]
|
||||
(let [result (->>
|
||||
(let [group-label (t :editor.slash/group-task-status)
|
||||
result (->>
|
||||
(db-based-statuses)
|
||||
(mapv (fn [command]
|
||||
(let [icon (case command
|
||||
(mapv (fn [status]
|
||||
(let [command (:block/title status)
|
||||
label (db-property/built-in-display-title status t)
|
||||
icon (case command
|
||||
"Canceled" "Cancelled"
|
||||
"Doing" "InProgress50"
|
||||
command)]
|
||||
[command (->marker command) (str "Set status to " command) icon]))))]
|
||||
[label (->marker command) (t :editor.slash/status-desc label) icon]))))]
|
||||
(when (seq result)
|
||||
(map (fn [v] (conj v "TASK STATUS")) result))))
|
||||
(map (fn [v] (conj v group-label)) result))))
|
||||
|
||||
(defn db-based-priorities
|
||||
[]
|
||||
(map (fn [e] (str "Priority " (:block/title e)))
|
||||
(db-pu/get-closed-property-values :logseq.property/priority)))
|
||||
(db-pu/get-closed-property-values :logseq.property/priority))
|
||||
|
||||
(defn get-priorities
|
||||
[]
|
||||
(let [with-no-priority #(cons ["No priority" (->priority nil) "" :icon/priorityLvlNone] %)
|
||||
(let [group-label (t :editor.slash/group-priority)
|
||||
with-no-priority #(cons [(t :editor.slash/no-priority) (->priority nil) "" :icon/priorityLvlNone] %)
|
||||
result (->>
|
||||
(db-based-priorities)
|
||||
(mapv (fn [item]
|
||||
(let [command item
|
||||
item (string/replace item #"^Priority " "")]
|
||||
[command
|
||||
(->priority item)
|
||||
(str "Set priority to " item)
|
||||
(str "priorityLvl" item)])))
|
||||
(mapv (fn [priority]
|
||||
(let [value (:block/title priority)
|
||||
label (db-property/built-in-display-title priority t)]
|
||||
[(t :editor.slash/priority-label label)
|
||||
(->priority value)
|
||||
(t :editor.slash/priority-desc label)
|
||||
(str "priorityLvl" value)])))
|
||||
(with-no-priority)
|
||||
(vec))]
|
||||
(when (seq result)
|
||||
(map (fn [v] (into v ["PRIORITY"])) result))))
|
||||
(map (fn [v] (into v [group-label])) result))))
|
||||
|
||||
;; Credits to roamresearch.com
|
||||
|
||||
@@ -185,10 +195,15 @@
|
||||
|
||||
(defn- headings
|
||||
[]
|
||||
(into [["Normal text" (->heading nil) "Clear heading and set to normal text" :icon/text "Heading"]]
|
||||
(into [[(t :editor.slash/normal-text)
|
||||
(->heading nil)
|
||||
(t :editor.slash/normal-text-desc)
|
||||
:icon/text
|
||||
(t :editor.slash/group-heading)]]
|
||||
(mapv (fn [level]
|
||||
(let [heading (str "Heading " level)]
|
||||
[heading (->heading level) heading (str "h-" level) "Heading"])) (range 1 7))))
|
||||
(let [heading (t :editor.slash/heading-label level)]
|
||||
[heading (->heading level) heading (str "h-" level) (t :editor.slash/group-heading)]))
|
||||
(range 1 7))))
|
||||
|
||||
(defonce *latest-matched-command (atom ""))
|
||||
(defonce *matched-commands (atom nil))
|
||||
@@ -200,36 +215,36 @@
|
||||
(->>
|
||||
(concat
|
||||
;; basic
|
||||
[["Node reference"
|
||||
[[(t :editor.slash/node-reference)
|
||||
[[:editor/input page-ref/left-and-right-brackets {:backward-pos 2}]
|
||||
[:editor/search-page]]
|
||||
"Create a backlink to a node (a page or a block)"
|
||||
(t :editor.slash/node-reference-desc)
|
||||
:icon/pageRef
|
||||
"BASIC"]
|
||||
["Node embed"
|
||||
(t :editor.slash/group-basic)]
|
||||
[(t :editor.slash/node-embed)
|
||||
(embed-block)
|
||||
"Embed a node here"
|
||||
(t :editor.slash/node-embed-desc)
|
||||
:icon/blockEmbed]]
|
||||
|
||||
;; format
|
||||
[["Link" (link-steps) "Create a HTTP link" :icon/link "FORMAT"]
|
||||
["Image link" (image-link-steps) "Create a HTTP link to a image" :icon/photoLink]
|
||||
[[(t :ui/link) (link-steps) (t :editor.slash/link-desc) :icon/link (t :editor.slash/group-format)]
|
||||
[(t :editor.slash/image-link) (image-link-steps) (t :editor.slash/image-link-desc) :icon/photoLink]
|
||||
(when (state/markdown?)
|
||||
["Underline" [[:editor/input "<ins></ins>"
|
||||
{:last-pattern command-trigger
|
||||
:backward-pos 6}]] "Create a underline text decoration"
|
||||
[(t :editor.slash/underline) [[:editor/input "<ins></ins>"
|
||||
{:last-pattern command-trigger
|
||||
:backward-pos 6}]] (t :editor.slash/underline-desc)
|
||||
:icon/underline])
|
||||
["Code block"
|
||||
[(t :editor.slash/code-block)
|
||||
(code-block-steps)
|
||||
"Insert code block"
|
||||
(t :editor.slash/code-block-desc)
|
||||
:icon/code]
|
||||
["Quote"
|
||||
[(t :class.built-in/quote-block)
|
||||
(quote-block-steps)
|
||||
"Create a quote block"
|
||||
(t :editor.slash/quote-desc)
|
||||
:icon/quote]
|
||||
["Math block"
|
||||
[(t :editor.slash/math-block)
|
||||
(math-block-steps)
|
||||
"Create a latex block"
|
||||
(t :editor.slash/math-block-desc)
|
||||
:icon/math]]
|
||||
|
||||
(headings)
|
||||
@@ -238,79 +253,80 @@
|
||||
(get-statuses)
|
||||
|
||||
;; task date
|
||||
[["Deadline"
|
||||
[[(t :property.built-in/deadline)
|
||||
[[:editor/clear-current-slash]
|
||||
[:editor/set-deadline]]
|
||||
""
|
||||
:icon/calendar-stats
|
||||
"TASK DATE"]
|
||||
["Scheduled"
|
||||
(t :editor.slash/group-task-date)]
|
||||
[(t :property.built-in/scheduled)
|
||||
[[:editor/clear-current-slash]
|
||||
[:editor/set-scheduled]]
|
||||
""
|
||||
:icon/calendar-month
|
||||
"TASK DATE"]]
|
||||
(t :editor.slash/group-task-date)]]
|
||||
|
||||
;; priority
|
||||
(get-priorities)
|
||||
|
||||
;; time & date
|
||||
[["Tomorrow"
|
||||
#(get-page-ref-text (date/tomorrow))
|
||||
"Insert the date of tomorrow"
|
||||
[[(t :date.nlp/tomorrow)
|
||||
#(get-page-ref-text (db/get-journal-page-title (date/tomorrow)))
|
||||
(t :editor.slash/tomorrow-desc)
|
||||
:icon/tomorrow
|
||||
"TIME & DATE"]
|
||||
["Yesterday" #(get-page-ref-text (date/yesterday)) "Insert the date of yesterday" :icon/yesterday]
|
||||
["Today" #(get-page-ref-text (date/today)) "Insert the date of today" :icon/calendar]
|
||||
["Current time" #(date/get-current-time) "Insert current time" :icon/clock]
|
||||
["Date picker" [[:editor/show-date-picker]] "Pick a date and insert here" :icon/calendar-dots]]
|
||||
(t :editor.slash/group-time-and-date)]
|
||||
[(t :date.nlp/yesterday) #(get-page-ref-text (db/get-journal-page-title (date/yesterday))) (t :editor.slash/yesterday-desc) :icon/yesterday]
|
||||
[(t :date.nlp/today) #(get-page-ref-text (db/get-today-journal-title)) (t :editor.slash/today-desc) :icon/calendar]
|
||||
[(t :editor.slash/current-time) #(date/get-current-time) (t :editor.slash/current-time-desc) :icon/clock]
|
||||
[(t :editor.slash/date-picker) [[:editor/show-date-picker]] (t :editor.slash/date-picker-desc) :icon/calendar-dots]]
|
||||
|
||||
;; order list
|
||||
[["Number list"
|
||||
[[(t :editor.slash/number-list)
|
||||
[[:editor/clear-current-slash]
|
||||
[:editor/toggle-own-number-list]]
|
||||
"Number list"
|
||||
(t :editor.slash/number-list)
|
||||
:icon/numberedParents
|
||||
"LIST TYPE"]
|
||||
["Number children" [[:editor/clear-current-slash]
|
||||
[:editor/toggle-children-number-list]]
|
||||
"Number children"
|
||||
(t :editor.slash/group-list-type)]
|
||||
[(t :editor.slash/number-children) [[:editor/clear-current-slash]
|
||||
[:editor/toggle-children-number-list]]
|
||||
(t :editor.slash/number-children)
|
||||
:icon/numberedChildren]]
|
||||
|
||||
;; advanced
|
||||
[["Query" (query-steps) query-doc :icon/query "ADVANCED"]
|
||||
["Advanced Query" (advanced-query-steps) "Create an advanced query block" :icon/query]
|
||||
["Query function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query function" :icon/queryCode]
|
||||
["Calculator"
|
||||
[[(t :property.built-in/query) (query-steps) (query-doc) :icon/query (t :editor.slash/group-advanced)]
|
||||
[(t :editor.slash/advanced-query) (advanced-query-steps) (t :editor.slash/advanced-query-desc) :icon/query]
|
||||
[(t :editor.slash/query-function) [[:editor/input "{{function }}" {:backward-pos 2}]] (t :editor.slash/query-function-desc) :icon/queryCode]
|
||||
[(t :editor.slash/calculator)
|
||||
(calc-steps)
|
||||
"Insert a calculator" :icon/calculator]
|
||||
(t :editor.slash/calculator-desc) :icon/calculator]
|
||||
|
||||
["Upload an asset"
|
||||
[(t :editor.slash/upload-asset)
|
||||
[[:editor/click-hidden-file-input :id]]
|
||||
"Upload file types like image, pdf, docx, etc.)"
|
||||
(t :editor.slash/upload-asset-desc)
|
||||
:icon/upload]
|
||||
|
||||
["Template" [[:editor/input command-trigger nil]
|
||||
[:editor/search-template]] "Insert a created template here"
|
||||
[(t :class.built-in/template) [[:editor/input command-trigger nil]
|
||||
[:editor/search-template]] (t :editor.slash/template-desc)
|
||||
:icon/template]
|
||||
|
||||
["Embed HTML " (->inline "html") "" :icon/htmlEmbed]
|
||||
[(t :editor.slash/embed-html) (->inline "html") "" :icon/htmlEmbed]
|
||||
|
||||
["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern command-trigger
|
||||
:backward-pos 2}]] ""
|
||||
[(t :editor.slash/embed-video-url) [[:editor/input "{{video }}" {:last-pattern command-trigger
|
||||
:backward-pos 2}]] ""
|
||||
:icon/videoEmbed]
|
||||
|
||||
["Embed Youtube timestamp" [[:youtube/insert-timestamp]] "" :icon/videoEmbed]
|
||||
[(t :editor.slash/embed-youtube-timestamp) [[:youtube/insert-timestamp]] "" :icon/videoEmbed]
|
||||
|
||||
["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern command-trigger
|
||||
:backward-pos 2}]] ""
|
||||
[(t :editor.slash/embed-twitter-tweet) [[:editor/input "{{tweet }}" {:last-pattern command-trigger
|
||||
:backward-pos 2}]] ""
|
||||
:icon/xEmbed]
|
||||
|
||||
["Add new property" [[:editor/clear-current-slash]
|
||||
[:editor/new-property]] ""
|
||||
[(t :command.editor/add-property) [[:editor/clear-current-slash]
|
||||
[:editor/new-property]] ""
|
||||
:icon/cube-plus]]
|
||||
|
||||
(let [commands (->> @*extend-slash-commands
|
||||
(map resolve-slash-command)
|
||||
(remove (fn [command] (when (map? (last command))
|
||||
(false? (:db-graph? (last command)))))))]
|
||||
commands)
|
||||
@@ -320,7 +336,7 @@
|
||||
(state/get-commands)
|
||||
(when-let [plugin-commands (seq (some->> (state/get-plugins-slash-commands)
|
||||
(mapv #(vec (concat % [nil :icon/puzzle])))))]
|
||||
(-> plugin-commands (vec) (update 0 (fn [v] (conj v "PLUGINS"))))))
|
||||
(-> plugin-commands (vec) (update 0 (fn [v] (conj v (t :editor.slash/group-plugins)))))))
|
||||
(remove nil?)
|
||||
(util/distinct-by-last-wins first))))
|
||||
|
||||
@@ -650,7 +666,7 @@
|
||||
(contains? #{:scheduled :deadline} type)
|
||||
(string/blank? (gobj/get (state/get-input) "value")))
|
||||
(do
|
||||
(notification/show! [:div "Please add some content first."] :warning)
|
||||
(notification/show! [:div (t :editor/add-content-first-warning)] :warning)
|
||||
(restore-state))
|
||||
(do
|
||||
(state/set-timestamp-block! nil)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
(defn- columns
|
||||
[]
|
||||
(->> [{:id :block/title
|
||||
:name (t :block/name)
|
||||
:name (t :page/name)
|
||||
:cell (fn [_table row _column]
|
||||
(component-block/page-cp {:show-non-exists-page? true
|
||||
:skip-async-load? true
|
||||
@@ -33,5 +33,4 @@
|
||||
(views/view {:view-parent (db/get-page common-config/views-page-name)
|
||||
:view-feature-type :all-pages
|
||||
:show-items-count? true
|
||||
:columns columns'
|
||||
:title-key :all-pages/table-title})]))
|
||||
:columns columns'})]))
|
||||
|
||||
@@ -69,18 +69,18 @@
|
||||
(do (set-dir! val dir nil)
|
||||
(shui/dialog-close!))
|
||||
(notification/show!
|
||||
(util/format "Alias name of [%s] already exists!" val) :warning))))]
|
||||
(t :asset/alias-already-exists val) :warning))))]
|
||||
|
||||
[:div.cp__assets-alias-name-content
|
||||
[:h1.text-2xl.opacity-90.mb-6 "What's the alias name of this selected directory?"]
|
||||
[:p [:strong "Directory path:"]
|
||||
[:h1.text-2xl.opacity-90.mb-6 (t :asset/alias-name-dialog-title)]
|
||||
[:p [:strong (t :asset/alias-directory-path-label)]
|
||||
[:a {:on-click #(when (util/electron?)
|
||||
(js/apis.openPath dir))} dir]]
|
||||
[:p [:strong "Alias name:"]
|
||||
[:p [:strong (t :asset/alias-name-label)]
|
||||
[:input.px-1.border.rounded
|
||||
{:autoFocus true
|
||||
:value val
|
||||
:placeholder "eg. Books"
|
||||
:placeholder (t :asset/alias-name-placeholder)
|
||||
:on-change (fn [^js e]
|
||||
(set-val! (util/trim-safe (.. e -target -value))))
|
||||
:on-key-up (fn [^js e]
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
[:div.pt-6.flex.justify-end
|
||||
(ui/button
|
||||
"Save"
|
||||
(t :ui/save)
|
||||
:disabled (string/blank? val)
|
||||
:on-click on-submit)]]))
|
||||
|
||||
@@ -181,20 +181,20 @@
|
||||
|
||||
:dune)))
|
||||
:input-opts {:class "cp__assets-alias-ext-input"
|
||||
:placeholder "E.g. mp3"
|
||||
:placeholder (t :asset/file-extension-placeholder)
|
||||
:on-blur
|
||||
#(reset! *ext-editing-dir nil)}})
|
||||
|
||||
[:small.ext-label.is-plus
|
||||
{:on-click #(reset! *ext-editing-dir dir)}
|
||||
(ui/icon "plus") "Acceptable file extensions"])]
|
||||
(ui/icon "plus") (t :asset/acceptable-file-extensions)])]
|
||||
|
||||
[:span.ctrls.flex.space-x-3.text-xs.opacity-30.hover:opacity-100.whitespace-nowrap.hidden.mt-1
|
||||
[:a {:on-click #(rm-dir dir)} (ui/icon "trash-x")]]]])]
|
||||
|
||||
[:p.pt-2
|
||||
(ui/button
|
||||
"+ Add directory"
|
||||
(t :asset/add-directory)
|
||||
:on-click #(p/let [path (ipc/ipc :openDialog)]
|
||||
(when-not (or (string/blank? path)
|
||||
(pick-exist path))
|
||||
@@ -213,7 +213,7 @@
|
||||
[:div.cp__assets-settings.panel-wrap
|
||||
[:div.it
|
||||
[:label.block.text-sm.font-medium.leading-5.opacity-70
|
||||
"Alias directories"]
|
||||
(t :asset/alias-directories)]
|
||||
[:div (ui/toggle
|
||||
alias-enabled?
|
||||
#(state/set-assets-alias-enabled! (not alias-enabled?))
|
||||
@@ -223,7 +223,7 @@
|
||||
|
||||
(when alias-enabled?
|
||||
[:div.pt-4
|
||||
[:h2.font-bold.opacity-80 "Selected directories:"]
|
||||
[:h2.font-bold.opacity-80 (t :asset/selected-directories)]
|
||||
(alias-directories)])]))
|
||||
|
||||
(rum/defc edit-external-url-form
|
||||
@@ -261,9 +261,9 @@
|
||||
(p/then #(when on-saved (on-saved asset-block false)))
|
||||
(p/catch err-handle)
|
||||
(p/finally #(set-saving? false))))))}
|
||||
[:label [:span.block.pb-2.text-sm.opacity-60 "Asset title:"]
|
||||
[:label [:span.block.pb-2.text-sm.opacity-60 (t :asset/title-label)]
|
||||
(shui/input {:small true :default-value title :name "title"})]
|
||||
[:label [:span.block.pb-2.text-sm.opacity-60 "Asset external url:"]
|
||||
[:label [:span.block.pb-2.text-sm.opacity-60 (t :asset/external-url-label)]
|
||||
[:span.flex.items-center.gap-2
|
||||
(shui/input {:small true :default-value url :name "src"})
|
||||
(when (util/electron?)
|
||||
@@ -272,14 +272,14 @@
|
||||
:on-click (fn [^js e]
|
||||
(.preventDefault e)
|
||||
(p/let [^js ret (ipc/ipc :showOpenDialog {:properties ["openFile"]
|
||||
:title "Select Asset File"})]
|
||||
:title (t :asset/select-file)})]
|
||||
(let [file-path (some-> ret (bean/->clj) :filePaths (first))]
|
||||
(when (not (string/blank? file-path))
|
||||
(let [^js input (-> (.-target e) (.closest "form") (.querySelector "input[name='src']"))]
|
||||
(set! (.-value input) file-path))))))}
|
||||
"Select from disk"))]]
|
||||
(t :asset/select-from-disk)))]]
|
||||
[:div.flex.justify-end.pt-3
|
||||
(ui/button (if create? "Create" "Save") {:disabled saving?})]]))
|
||||
(ui/button (if create? (t :ui/create) (t :ui/save)) {:disabled saving?})]]))
|
||||
|
||||
(rum/defc edit-external-url-content
|
||||
[asset-block pdf-current]
|
||||
@@ -300,7 +300,7 @@
|
||||
(shui/alert
|
||||
{:variant "warning"}
|
||||
(shui/alert-description
|
||||
"Creating a local asset from an external one. PDF annotations require a local asset to work properly."))
|
||||
(t :asset/create-local-copy-warning)))
|
||||
|
||||
(let [title (util/node-path.basename url)]
|
||||
(edit-external-url-form asset-block {:url url :title title :on-saved on-saved!}))])))])
|
||||
|
||||
@@ -274,15 +274,15 @@
|
||||
"ico" "image/x-icon"}
|
||||
mime (get ext->mime ext)]
|
||||
(if-not mime
|
||||
(notification/show! (str "Copy image is not supported for ." ext " files") :warning)
|
||||
(notification/show! (t :asset/copy-image-unsupported-extension (str "." ext)) :warning)
|
||||
(-> (p/let [binary (fs/read-file-raw nil image-src {})
|
||||
blob (js/Blob. (array binary) (clj->js {:type mime}))]
|
||||
(util/copy-image-blob-to-clipboard blob))
|
||||
(p/then #(notification/show! "Copied!" :success))
|
||||
(p/then #(notification/show! (t :notification/copied) :success))
|
||||
(p/catch (fn [error]
|
||||
(js/console.error error))))))
|
||||
(-> (util/copy-image-to-clipboard src')
|
||||
(p/then #(notification/show! "Copied!" :success))
|
||||
(p/then #(notification/show! (t :notification/copied) :success))
|
||||
(p/catch (fn [error]
|
||||
(js/console.error error))))))
|
||||
handle-delete!
|
||||
@@ -297,8 +297,10 @@
|
||||
{:default-checked @*local-selected?
|
||||
:on-checked-change #(reset! *local-selected? %)})
|
||||
(t :asset/physical-delete)])]
|
||||
{:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
|
||||
:outside-cancel? true})
|
||||
{:title (t :asset/confirm-delete-image)
|
||||
:outside-cancel? true
|
||||
:cancel-label (t :ui/cancel)
|
||||
:ok-label (t :ui/confirm)})
|
||||
(p/then (fn []
|
||||
(shui/dialog-close!)
|
||||
(editor-handler/delete-asset-of-block!
|
||||
@@ -367,7 +369,7 @@
|
||||
(ipc/ipc "openFileInFolder" image-src)
|
||||
(js/window.apis.openExternal image-src)))}
|
||||
[:span.flex.items-center.gap-1
|
||||
(ui/icon "folder-pin") (t (if local? :asset/show-in-folder :asset/open-in-browser))]))
|
||||
(ui/icon "folder-pin") (t (if local? :asset/show-file-in-folder :asset/open-in-browser))]))
|
||||
|
||||
(when-not config/publishing?
|
||||
[:<>
|
||||
@@ -594,40 +596,38 @@
|
||||
[:div.as-plain-image-link
|
||||
(resizable-image config title href metadata full_text false)])))))
|
||||
|
||||
(def timestamp-to-string export-common-handler/timestamp-to-string)
|
||||
|
||||
(defn timestamp [{:keys [active _date _time _repetition _wday] :as t} kind]
|
||||
(let [prefix (case kind
|
||||
"Scheduled"
|
||||
:scheduled
|
||||
[:i {:class "fa fa-calendar"
|
||||
:style {:margin-right 3.5}}]
|
||||
"Deadline"
|
||||
:deadline
|
||||
[:i {:class "fa fa-calendar-times-o"
|
||||
:style {:margin-right 3.5}}]
|
||||
"Date"
|
||||
:date
|
||||
nil
|
||||
"Closed"
|
||||
:closed
|
||||
nil
|
||||
"Started"
|
||||
:started
|
||||
[:i {:class "fa fa-clock-o"
|
||||
:style {:margin-right 3.5}}]
|
||||
"Start"
|
||||
"From: "
|
||||
"Stop"
|
||||
"To: "
|
||||
:start
|
||||
(t :ui/from)
|
||||
:stop
|
||||
(t :ui/to)
|
||||
nil)
|
||||
class (when (= kind "Closed")
|
||||
class (when (= kind :closed)
|
||||
"line-through")]
|
||||
[:span.timestamp (cond-> {:active (str active)}
|
||||
class
|
||||
(assoc :class class))
|
||||
prefix (timestamp-to-string t)]))
|
||||
prefix (export-common-handler/timestamp-to-string t)]))
|
||||
|
||||
(defn range [{:keys [start stop]} stopped?]
|
||||
[:div {:class "timestamp-range"
|
||||
:stopped stopped?}
|
||||
(timestamp start "Start")
|
||||
(timestamp stop "Stop")])
|
||||
(timestamp start :start)
|
||||
(timestamp stop :stop)])
|
||||
|
||||
(declare map-inline)
|
||||
(declare markup-element-cp)
|
||||
@@ -702,7 +702,7 @@
|
||||
recycled? (str " line-through opacity-70")
|
||||
untitled? (str " opacity-50"))
|
||||
:data-ref page-name
|
||||
:title (when recycled? "Deleted")
|
||||
:title (when recycled? (t :ui/deleted))
|
||||
:draggable true
|
||||
:on-drag-start (fn [e]
|
||||
(editor-handler/block->data-transfer! page-name e true))
|
||||
@@ -765,7 +765,7 @@
|
||||
|
||||
(ldb/page? page-entity)
|
||||
(if untitled?
|
||||
(t :untitled)
|
||||
(t :ui/untitled)
|
||||
(let [s (util/trim-safe (if show-unique-title?
|
||||
(block-handler/block-unique-title page-entity {:with-tags? with-tags?})
|
||||
(:block/title page-entity)))]
|
||||
@@ -1053,9 +1053,9 @@
|
||||
percent (when in-progress?
|
||||
(int (* 100 (/ loaded total))))
|
||||
label (case direction
|
||||
:upload "Uploading"
|
||||
:download "Downloading"
|
||||
"Syncing")
|
||||
:upload (t :asset/uploading)
|
||||
:download (t :asset/downloading)
|
||||
(t :asset/syncing))
|
||||
progress-view (when in-progress?
|
||||
[:div.asset-transfer-progress
|
||||
[:div.asset-transfer-progress-label (str label " " percent "%")]
|
||||
@@ -1091,7 +1091,7 @@
|
||||
(if progress-view
|
||||
[:div.asset-transfer-shell
|
||||
(or content
|
||||
[:div.asset-transfer-placeholder (str label " asset...")])
|
||||
[:div.asset-transfer-placeholder (t :asset/transfer-placeholder label)])
|
||||
progress-view]
|
||||
content)))
|
||||
|
||||
@@ -1143,7 +1143,7 @@
|
||||
(and (string? uuid-or-title) (string/ends-with? uuid-or-title ".excalidraw"))
|
||||
[:div.draw {:on-click (fn [e]
|
||||
(.stopPropagation e))}
|
||||
[:div.warning "Excalidraw is no longer supported by default, we plan to support it through plugins."]]
|
||||
[:div.warning (t :block/excalidraw-no-longer-supported)]]
|
||||
|
||||
:else
|
||||
(let [blank-title? (string/blank? (:block/title block))]
|
||||
@@ -1230,7 +1230,7 @@
|
||||
[(assoc attributes :class "inline")
|
||||
(inline-text {:add-margin? false} format macro-content)]))
|
||||
[attributes
|
||||
[:span.warning {:title (str "Unsupported macro name: " name)}
|
||||
[:span.warning {:title (t :block.macro/unsupported-name name)}
|
||||
(macro->text name arguments)]]))))
|
||||
|
||||
(rum/defc nested-link < rum/reactive
|
||||
@@ -1314,7 +1314,7 @@
|
||||
[config url s label title metadata full_text]
|
||||
(cond
|
||||
(string/blank? s)
|
||||
[:span.warning {:title "Invalid link"} full_text]
|
||||
[:span.warning {:title (t :block/invalid-link)} full_text]
|
||||
|
||||
(= \# (first s))
|
||||
(->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1))
|
||||
@@ -1372,7 +1372,7 @@
|
||||
{:keys [link-depth]} config
|
||||
link-depth (or link-depth 0)]
|
||||
(if (> link-depth max-depth-of-links)
|
||||
[:p.warning.text-sm "Block ref nesting is too deep"]
|
||||
[:p.warning.text-sm (t :block/ref-nesting-too-deep)]
|
||||
(block-reference (assoc config
|
||||
:reference? true
|
||||
:link-depth (inc link-depth)
|
||||
@@ -1540,9 +1540,9 @@
|
||||
:src src
|
||||
:width width
|
||||
:height height}]))))
|
||||
[:span.warning.mr-1 {:title "Invalid URL"}
|
||||
[:span.warning.mr-1 {:title (t :block/invalid-url)}
|
||||
(macro->text "video" arguments)])
|
||||
[:span.warning.mr-1 {:title "Empty URL"}
|
||||
[:span.warning.mr-1 {:title (t :block/empty-url)}
|
||||
(macro->text "video" arguments)]))
|
||||
|
||||
(defn- macro-else-cp
|
||||
@@ -1564,13 +1564,13 @@
|
||||
arguments)]
|
||||
(cond
|
||||
(= name "query")
|
||||
[:div.warning "{{query}} is deprecated. Use '/Query' command instead."]
|
||||
[:div.warning (t :block.macro/query-deprecated)]
|
||||
|
||||
(= name "function")
|
||||
(macro-function-cp config arguments)
|
||||
|
||||
(= name "namespace")
|
||||
[:div.warning (str "{{namespace}} is deprecated. Use the " common-config/library-page-name " feature instead.")]
|
||||
[:div.warning (t :block.macro/namespace-deprecated (t :library/title))]
|
||||
|
||||
(= name "youtube")
|
||||
(when-let [url (first arguments)]
|
||||
@@ -1617,7 +1617,7 @@
|
||||
(ui/tweet-embed id)))))
|
||||
|
||||
(= name "embed")
|
||||
[:div.warning "{{embed}} is deprecated. Use '/Node embed' command instead."]
|
||||
[:div.warning (t :block.macro/embed-deprecated)]
|
||||
|
||||
(= name "renderer")
|
||||
(when config/lsp-enabled?
|
||||
@@ -1644,7 +1644,7 @@
|
||||
[s]
|
||||
(let [result (common-util/safe-read-string s)
|
||||
result' (if (seq result) result
|
||||
[:div.warning {:title "Invalid hiccup"}
|
||||
[:div.warning {:title (t :block/invalid-hiccup)}
|
||||
s])]
|
||||
(-> result'
|
||||
(hiccups.core/html)
|
||||
@@ -1718,7 +1718,7 @@
|
||||
|
||||
["Inline_Hiccup" s] ;; String to hiccup
|
||||
(ui/catch-error
|
||||
[:div.warning {:title "Invalid hiccup"} s]
|
||||
[:div.warning {:title (t :block/invalid-hiccup)} s]
|
||||
[:span {:dangerouslySetInnerHTML
|
||||
{:__html (hiccup->html s)}}])
|
||||
|
||||
@@ -1733,15 +1733,15 @@
|
||||
["Timestamp" [(:or "Scheduled" "Deadline") _timestamp]]
|
||||
nil
|
||||
["Timestamp" ["Date" t]]
|
||||
(timestamp t "Date")
|
||||
(timestamp t :date)
|
||||
["Timestamp" ["Closed" t]]
|
||||
(timestamp t "Closed")
|
||||
(timestamp t :closed)
|
||||
["Timestamp" ["Range" t]]
|
||||
(range t false)
|
||||
["Timestamp" ["Clock" ["Stopped" t]]]
|
||||
(range t true)
|
||||
["Timestamp" ["Clock" ["Started" t]]]
|
||||
(timestamp t "Started")
|
||||
(timestamp t :started)
|
||||
|
||||
["Cookie" ["Percent" n]]
|
||||
[:span {:class "cookie-percent"}
|
||||
@@ -2111,8 +2111,8 @@
|
||||
(when-let [created-by (and (ldb/get-graph-rtc-uuid (db/get-db))
|
||||
(:logseq.property/created-by-ref block))]
|
||||
[:div (:block/title created-by)])
|
||||
[:div "Created: " (date/int->local-time-2 (:block/created-at block))]
|
||||
[:div "Last edited: " (date/int->local-time-2 (:block/updated-at block))]]))))]))
|
||||
[:div (t :block/created-label (date/int->local-time-2 (:block/created-at block)))]
|
||||
[:div (t :block/last-edited-label (date/int->local-time-2 (:block/updated-at block)))]]))))]))
|
||||
|
||||
(rum/defc dnd-separator
|
||||
[move-to]
|
||||
@@ -2224,8 +2224,8 @@
|
||||
(when *show-query? (swap! *show-query? not)))}
|
||||
(ui/icon "settings"))
|
||||
[:div.opacity-75 (if show-query?
|
||||
"Hide query"
|
||||
"Set query")]))]
|
||||
(t :block/hide-query)
|
||||
(t :block/set-query))]))]
|
||||
[:div
|
||||
(merge
|
||||
{:class (if query?
|
||||
@@ -2238,7 +2238,7 @@
|
||||
{:on-click on-title-click})))
|
||||
(cond
|
||||
(and query? blank? (or advanced-query? show-query?))
|
||||
[:span.opacity-75.hover:opacity-100 "Untitled query"]
|
||||
[:span.opacity-75.hover:opacity-100 (t :block/untitled-query)]
|
||||
(and query? blank?)
|
||||
(query-builder-component/builder query {})
|
||||
:else
|
||||
@@ -2255,8 +2255,8 @@
|
||||
:on-click (fn [e]
|
||||
(util/stop e)
|
||||
(state/pub-event! [:modal/show-cards (:db/id block)]))}
|
||||
"Practice")
|
||||
[:div "Practice cards"])])
|
||||
(t :block/practice))
|
||||
[:div (t :block/practice-cards)])])
|
||||
(when-let [property (:logseq.property/created-from-property block)]
|
||||
(when-let [message (when (= :url (:logseq.property/type property))
|
||||
(first (outliner-property/validate-property-value (db/get-db) property (:db/id block))))]
|
||||
@@ -2272,16 +2272,23 @@
|
||||
[config block {:keys [*show-query?]}]
|
||||
(let [block' (db/entity (:db/id block))
|
||||
node-display-type (:logseq.property.node/display-type block')
|
||||
display-title (:display-title config)
|
||||
db (db/get-db)
|
||||
query? (ldb/class-instance? (entity-plus/entity-memoized db :logseq.class/Query) block')]
|
||||
(cond
|
||||
(and (:page-title? config) (ldb/page? block) (string/blank? (:block/title block)))
|
||||
[:div.opacity-75 "Untitled"]
|
||||
[:div.opacity-75 (t :ui/untitled)]
|
||||
|
||||
(and (ldb/asset? block)
|
||||
(= :pdf (some-> (:logseq.property.asset/type block) string/lower-case keyword)))
|
||||
(asset-cp config block)
|
||||
|
||||
display-title
|
||||
(text-block-title (dissoc config :display-title)
|
||||
(-> block
|
||||
(assoc :block/title display-title)
|
||||
(dissoc :block.temp/ast-title :block.temp/ast-body)))
|
||||
|
||||
(:raw-title? config)
|
||||
(text-block-title (dissoc config :raw-title?) block)
|
||||
|
||||
@@ -2500,11 +2507,11 @@
|
||||
(shui/dropdown-menu-item
|
||||
{:key "Remove tag"
|
||||
:on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))}
|
||||
"Remove tag"))])
|
||||
(t :block/remove-tag)))])
|
||||
popup-opts))}
|
||||
(if (and @*hover? (not private-tag?) (not config/publishing?))
|
||||
[:a.inline-flex.text-muted-foreground
|
||||
{:title "Remove this tag"
|
||||
{:title (t :block/remove-this-tag)
|
||||
:style {:margin-top 1
|
||||
:padding-left 2
|
||||
:margin-right 2}
|
||||
@@ -2554,7 +2561,7 @@
|
||||
[:div.flex.flex-row.items-center.gap-1
|
||||
(when-not (ldb/private-tags (:db/ident tag))
|
||||
(shui/button
|
||||
{:title "Remove tag"
|
||||
{:title (t :block/remove-tag)
|
||||
:variant :ghost
|
||||
:class "!p-1 text-muted-foreground"
|
||||
:size :sm
|
||||
@@ -2665,7 +2672,7 @@
|
||||
{:variant :ghost
|
||||
:size :sm
|
||||
:class "px-1 py-0 h-6 text-muted-foreground hover:text-foreground"
|
||||
:title "Add reaction"
|
||||
:title (t :command.editor/add-reaction)
|
||||
:on-click open-picker!
|
||||
:on-pointer-down (fn [e]
|
||||
(util/stop e))}
|
||||
@@ -2676,9 +2683,9 @@
|
||||
(let [[sort-desc? set-sort-desc!] (rum/use-state true)]
|
||||
[:div.p-2.text-muted-foreground.text-sm.max-h-96
|
||||
[:div.font-medium.mb-2.flex.flex-row.gap-2.items-center
|
||||
[:div "Status history"]
|
||||
[:div (t :block/status-history)]
|
||||
(shui/button-ghost-icon (if sort-desc? :arrow-down :arrow-up)
|
||||
{:title "Sort order"
|
||||
{:title (t :block/sort-order)
|
||||
:class "text-muted-foreground !h-4 !w-4"
|
||||
:icon-props {:size 14}
|
||||
:on-click #(set-sort-desc! (not sort-desc?))})]
|
||||
@@ -2785,7 +2792,7 @@
|
||||
(when (and (> (count content) (state/block-content-max-length (state/get-current-repo)))
|
||||
(not (contains? #{:code} (:logseq.property.node/display-type block))))
|
||||
[:div.warning.text-sm
|
||||
"Large block will not be editable or searchable to not slow down the app, please use another editor to edit this block."])
|
||||
(t :block/large-block-warning)])
|
||||
[:div.flex.flex-row.justify-between.block-content-inner
|
||||
(when-not plugin-slotted?
|
||||
[:div.block-head-wrap
|
||||
@@ -2800,7 +2807,7 @@
|
||||
(when (> block-refs-count' 0)
|
||||
[:div.h-6
|
||||
(shui/button {:variant :ghost
|
||||
:title "Open block references"
|
||||
:title (t :block/open-block-references)
|
||||
:class (str "px-1 py-0 w-5 h-5 opacity-70 hover:opacity-100" (when (and (util/mobile?)
|
||||
(seq (:block/_parent block)))
|
||||
" !pr-4"))
|
||||
@@ -2839,10 +2846,9 @@
|
||||
{:on-click (fn []
|
||||
(set-editing! true)
|
||||
(editor-handler/edit-block! query :max {:container-id (:container-id config)}))}
|
||||
"Click to fix query: "
|
||||
(:block/title query)])
|
||||
(t :block/click-to-fix-query (:block/title query))])
|
||||
[:div.flex.flex-1.flex-col.w-full.gap-2
|
||||
(ui/block-error "Block Render Error:"
|
||||
(ui/block-error (t :block/render-error)
|
||||
{:content (or (:block/title query)
|
||||
(:block/title block))
|
||||
:section-attrs
|
||||
@@ -2916,7 +2922,7 @@
|
||||
{:id editor-id
|
||||
:class (util/classnames [{:opacity-50 (boolean (or (ldb/built-in? block) (ldb/journal? block)))}])}
|
||||
(ui/catch-error
|
||||
(ui/block-error "Something wrong in the editor" {})
|
||||
(ui/block-error (t :sync/something-wrong) {})
|
||||
(editor-box {:block block
|
||||
:block-id uuid
|
||||
:block-parent-id block-id
|
||||
@@ -3388,7 +3394,7 @@
|
||||
(let [element (dom/create-element "div")]
|
||||
(-> element
|
||||
(dom/set-attr! "id" "dragging-ghost-element")
|
||||
(dom/set-text! (str "Moving " (count blocks) " blocks"))
|
||||
(dom/set-text! (t :editor/moving-blocks-count (count blocks)))
|
||||
(dom/set-class! "p-2 rounded text-sm"))
|
||||
element))]
|
||||
(doseq [block blocks]
|
||||
@@ -3498,7 +3504,7 @@
|
||||
(if advanced-query?
|
||||
(src-cp (assoc config :code-block query) {:language "clojure"})
|
||||
[:div
|
||||
[:div.opacity-75.ml-5.text-sm.mb-1 "Set query:"]
|
||||
[:div.opacity-75.ml-5.text-sm.mb-1 (t :block/set-query-label)]
|
||||
(block-container config query)])]))
|
||||
|
||||
(when (and (not (or (:table? config) (:property? config)))
|
||||
@@ -3796,7 +3802,7 @@
|
||||
(when-let [langs (map (fn [m] (.-name m)) js/window.CodeMirror.modeInfo)]
|
||||
(let [options (map (fn [lang] {:label lang :value lang}) langs)]
|
||||
(select/select {:items options
|
||||
:input-default-placeholder "Choose language"
|
||||
:input-default-placeholder (t :editor/code-language-placeholder)
|
||||
:on-chosen
|
||||
(fn [chosen _ _ e]
|
||||
(let [lang (:value chosen)]
|
||||
@@ -3854,7 +3860,7 @@
|
||||
(db-property-handler/set-block-property!
|
||||
(:db/id block) :logseq.property.code/lang lang))))
|
||||
{:align :end})))}
|
||||
(or language "Choose language")
|
||||
(or language (t :editor/code-language-placeholder))
|
||||
(ui/icon "chevron-down"))
|
||||
(shui/button
|
||||
{:variant :text
|
||||
@@ -3863,9 +3869,9 @@
|
||||
(util/stop-propagation e)
|
||||
(when-let [^js cm (util/get-cm-instance (util/rec-get-node (.-target e) "ls-block"))]
|
||||
(util/copy-to-clipboard! (.getValue cm))
|
||||
(notification/show! "Copied!" :success)))}
|
||||
(notification/show! (t :notification/copied) :success)))}
|
||||
(ui/icon "copy")
|
||||
"Copy")]
|
||||
(t :ui/copy))]
|
||||
(lazy-editor/editor config (str (d/squuid)) attr code options)
|
||||
(let [options (:options options) block (:block config)]
|
||||
(when (and (= language "clojure") (contains? (set options) ":results"))
|
||||
@@ -3933,7 +3939,7 @@
|
||||
[:pre.pre-wrap-white-space
|
||||
(join-lines l)]
|
||||
["Quote" _l]
|
||||
[:div.warning "#+BEGIN_QUOTE is deprecated. Use '/Quote' command instead."]
|
||||
[:div.warning (t :block/deprecated-quote)]
|
||||
["Raw_Html" content]
|
||||
(when (not html-export?)
|
||||
[:div.raw_html.inline-block
|
||||
@@ -3945,7 +3951,7 @@
|
||||
{:__html (security/sanitize-html content)}}])
|
||||
["Hiccup" content]
|
||||
(ui/catch-error
|
||||
[:div.warning {:title "Invalid hiccup"}
|
||||
[:div.warning {:title (t :block/invalid-hiccup)}
|
||||
content]
|
||||
[:div.hiccup_html.inline
|
||||
{:dangerouslySetInnerHTML
|
||||
@@ -3954,10 +3960,10 @@
|
||||
["Export" "latex" _options content]
|
||||
(if html-export?
|
||||
(latex/html-export content true false)
|
||||
[:div.warning "'#+BEGIN_EXPORT latex' is deprecated. Use '/Math block' command instead."])
|
||||
[:div.warning (t :block/deprecated-latex-export)])
|
||||
|
||||
["Custom" "query" _options _result _content]
|
||||
[:div.warning "#+BEGIN_QUERY is deprecated. Use '/Advanced Query' command instead."]
|
||||
[:div.warning (t :block/deprecated-query-syntax)]
|
||||
|
||||
["Custom" "note" _options result _content]
|
||||
(ui/admonition "note" (markup-elements-cp config result))
|
||||
@@ -4281,7 +4287,7 @@
|
||||
(ui/foldable
|
||||
[:div.with-foldable-page
|
||||
(page-cp config page)
|
||||
(when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
|
||||
(when alias? [:span.text-sm.font-medium.opacity-50 (str " " (t :property.built-in/alias))])]
|
||||
items
|
||||
{:debug-id page})
|
||||
[:div.only-page-blocks items]))]))
|
||||
@@ -4305,7 +4311,7 @@
|
||||
(ui/foldable
|
||||
[:div
|
||||
(page-cp config page)
|
||||
(when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
|
||||
(when alias? [:span.text-sm.font-medium.opacity-50 (str " " (t :property.built-in/alias))])]
|
||||
(fn []
|
||||
(let [{top-level-blocks true others false} (group-by
|
||||
(fn [b] (= (:db/id page) (:db/id (first b))))
|
||||
@@ -4354,7 +4360,7 @@
|
||||
(ui/foldable
|
||||
[:div
|
||||
(page-cp config page)
|
||||
(when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
|
||||
(when alias? [:span.text-sm.font-medium.opacity-50 (str " " (t :property.built-in/alias))])]
|
||||
(fn []
|
||||
(blocks-container config blocks))
|
||||
{})])))))]
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
[reitit.frontend.easy :as rfe]
|
||||
[rum.core :as rum]))
|
||||
|
||||
(defn paste-shortcut-label
|
||||
[mac?]
|
||||
(if mac? "⌘+V" "Ctrl+V"))
|
||||
|
||||
(defn parse-clipboard-data-transfer
|
||||
"parse dataTransfer
|
||||
|
||||
@@ -44,7 +48,7 @@
|
||||
|
||||
copy-result-to-clipboard! (fn [result]
|
||||
(util/copy-to-clipboard! result)
|
||||
(notification/show! (t :bug-report/inspector-page-copy-notif)))
|
||||
(notification/show! (t :bug-report.inspector/copied)))
|
||||
|
||||
reset-step! (fn []
|
||||
(set-step! 0)
|
||||
@@ -58,26 +62,27 @@
|
||||
|
||||
[:div.flex.flex-col
|
||||
(when (= step 0)
|
||||
(list [:div.mx-auto (t :bug-report/inspector-page-desc-1)]
|
||||
[:div.mx-auto (t :bug-report/inspector-page-desc-2)]
|
||||
(list (for [line (string/split-lines (t :bug-report.inspector/desc
|
||||
(paste-shortcut-label util/mac?)))]
|
||||
[:div.mx-auto line])
|
||||
;; for mobile
|
||||
[:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder (t :bug-report/inspector-page-placeholder)}]
|
||||
[:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder (t :bug-report.inspector/placeholder)}]
|
||||
[:div.flex.justify-between.items-center.mt-2
|
||||
[:div (t :bug-report/inspector-page-tip)]
|
||||
(ui/button (t :bug-report/inspector-page-btn-back) :on-click #(util/open-url (rfe/href :bug-report)))]))
|
||||
[:div (t :bug-report.inspector/tip)]
|
||||
(ui/button (t :bug-report.inspector/back) :on-click #(util/open-url (rfe/href :bug-report)))]))
|
||||
|
||||
(when (= step 1)
|
||||
(list
|
||||
[:div (t :bug-report/inspector-page-desc-clipboard)]
|
||||
[:div (t :bug-report.inspector/clipboard-desc)]
|
||||
[:div.flex.justify-between.items-center.mt-2
|
||||
[:div (t :bug-report/inspector-page-desc-copy)]
|
||||
(ui/button (t :bug-report/inspector-page-btn-copy) :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
|
||||
[:div (t :bug-report.inspector/copy-desc)]
|
||||
(ui/button (t :bug-report.inspector/copy) :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
|
||||
[:div.flex.justify-between.items-center.mt-2
|
||||
[:div (t :bug-report/inspector-page-desc-create-issue)]
|
||||
(ui/button (t :bug-report/inspector-page-btn-create-issue) :href (header/bug-report-url))]
|
||||
[:div (t :bug-report.inspector/create-issue-desc)]
|
||||
(ui/button (t :bug-report.inspector/create-issue) :href (header/bug-report-url))]
|
||||
[:div.flex.justify-between.items-center.mt-2
|
||||
[:div (t :bug-report/inspector-page-tip)]
|
||||
(ui/button (t :bug-report/inspector-page-btn-back) :on-click reset-step!)]
|
||||
[:div (t :bug-report.inspector/tip)]
|
||||
(ui/button (t :bug-report.inspector/back) :on-click reset-step!)]
|
||||
|
||||
[:pre.whitespace-pre-wrap [:code (js/JSON.stringify (clj->js result) nil 2)]]))]))
|
||||
|
||||
@@ -87,7 +92,7 @@
|
||||
[:div.flex.flex-col ;; container
|
||||
(cond
|
||||
(= name "clipboard-data-inspector")
|
||||
[:h1.text-2xl.mx-auto.mb-4 (ui/icon "clipboard") " " (-> (t :bug-report/clipboard-inspector-title) (string/capitalize))])
|
||||
[:h1.text-2xl.mx-auto.mb-4 (ui/icon "clipboard") " " (-> (t :bug-report.inspector/title) (string/capitalize))])
|
||||
(cond
|
||||
(= name "clipboard-data-inspector")
|
||||
(clipboard-data-inspector))]))
|
||||
@@ -106,17 +111,17 @@
|
||||
[:div.flex.flex-col.items-center
|
||||
[:div.flex.items-center.mb-2
|
||||
(ui/icon "bug")
|
||||
[:h1.text-3xl.ml-2 (t :bug-report/main-title)]]
|
||||
[:div.opacity-60 (t :bug-report/main-desc)]]
|
||||
[:h1.text-3xl.ml-2 (t :bug-report/title)]]
|
||||
[:div.opacity-60 (t :bug-report/desc)]]
|
||||
[:div.cp__bug-report-reporter.rounded-lg.p-8.mt-8
|
||||
[:h1.text-2xl (t :bug-report/section-clipboard-title)]
|
||||
[:div.opacity-60 (t :bug-report/section-clipboard-desc)]
|
||||
(report-item-button (t :bug-report/section-clipboard-btn-title)
|
||||
(t :bug-report/section-clipboard-btn-desc)
|
||||
[:h1.text-2xl (t :bug-report.clipboard/title)]
|
||||
[:div.opacity-60 (t :bug-report.clipboard/desc)]
|
||||
(report-item-button (t :bug-report.clipboard/action-title)
|
||||
(t :bug-report.clipboard/action-desc)
|
||||
"clipboard"
|
||||
{:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
|
||||
[:div.py-2] ;; divider
|
||||
[:div.flex.flex-col
|
||||
[:h1.text-2xl (t :bug-report/section-issues-title)]
|
||||
[:div.opacity-60 (t :bug-report/section-issues-desc)]
|
||||
(report-item-button (t :bug-report/section-issues-btn-title) (t :bug-report/section-issues-btn-desc) "message-report" {:on-click #(util/open-url (header/bug-report-url))})]]])
|
||||
[:h1.text-2xl (t :bug-report.issue/title)]
|
||||
[:div.opacity-60 (t :bug-report.issue/desc)]
|
||||
(report-item-button (t :bug-report.issue/action-title) (t :bug-report.issue/action-desc) "message-report" {:on-click #(util/open-url (header/bug-report-url))})]]])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns frontend.components.class
|
||||
(:require [frontend.components.block :as block]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db.model :as model]
|
||||
[frontend.state :as state]
|
||||
[frontend.ui :as ui]
|
||||
@@ -30,7 +31,7 @@
|
||||
default-collapsed? (> (count children-pages) 30)]
|
||||
(ui/foldable
|
||||
[:div.font-medium.opacity-50
|
||||
(str "Children (" (count children-pages) ")")]
|
||||
(t :property/children-count (count children-pages))]
|
||||
[:div.ml-1.mt-2 (class-children-aux class {:default-collapsed? default-collapsed?})]
|
||||
{:default-collapsed? false
|
||||
:title-trigger? true}))))
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[frontend.components.cmdk.state :as cmdk-state]
|
||||
[frontend.components.icon :as icon-component]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.context.i18n :as i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db.async :as db-async]
|
||||
[frontend.db.model :as model]
|
||||
@@ -60,14 +60,28 @@
|
||||
(let [current-page (state/get-current-page)]
|
||||
(->>
|
||||
[(when current-page
|
||||
{:filter {:group :current-page} :text "Search only current page" :info "Add filter to search" :icon-theme :gray :icon "file"})
|
||||
{:filter {:group :nodes} :text "Search only nodes" :info "Add filter to search" :icon-theme :gray :icon "point-filled"}
|
||||
{:filter {:group :code} :text "Search only code" :info "Add filter to search" :icon-theme :gray :icon "code"}
|
||||
{:filter {:group :commands} :text "Search only commands" :info "Add filter to search" :icon-theme :gray :icon "command"}
|
||||
{:filter {:group :files} :text "Search only files" :info "Add filter to search" :icon-theme :gray :icon "file"}
|
||||
{:filter {:group :themes} :text "Search only themes" :info "Add filter to search" :icon-theme :gray :icon "palette"}]
|
||||
{:filter {:group :current-page} :text (t :cmdk.filter/current-page) :info (t :cmdk.filter/add) :icon-theme :gray :icon "file"})
|
||||
{:filter {:group :nodes} :text (t :cmdk.filter/nodes) :info (t :cmdk.filter/add) :icon-theme :gray :icon "point-filled"}
|
||||
{:filter {:group :codes} :text (t :cmdk.filter/codes) :info (t :cmdk.filter/add) :icon-theme :gray :icon "code"}
|
||||
{:filter {:group :commands} :text (t :cmdk.filter/commands) :info (t :cmdk.filter/add) :icon-theme :gray :icon "command"}
|
||||
{:filter {:group :files} :text (t :cmdk.filter/files) :info (t :cmdk.filter/add) :icon-theme :gray :icon "file"}
|
||||
{:filter {:group :themes} :text (t :cmdk.filter/themes) :info (t :cmdk.filter/add) :icon-theme :gray :icon "palette"}]
|
||||
(remove nil?))))
|
||||
|
||||
(defn- group-label
|
||||
[group]
|
||||
(case group
|
||||
:filters (t :cmdk.group/filters)
|
||||
:current-page (t :cmdk.group/current-page)
|
||||
:nodes (t :cmdk.group/nodes)
|
||||
:codes (t :cmdk.group/codes)
|
||||
:files (t :cmdk.group/files)
|
||||
:create (t :cmdk.group/create)
|
||||
:recently-updated-pages (t :cmdk.group/recently-updated)
|
||||
:commands (t :cmdk.group/commands)
|
||||
:themes (t :cmdk.group/themes)
|
||||
(name group)))
|
||||
|
||||
;; The results are separated into groups, and loaded/fetched/queried separately
|
||||
(def default-results
|
||||
{:recently-updated-pages {:status :success :show :less :items nil}
|
||||
@@ -75,7 +89,7 @@
|
||||
:favorites {:status :success :show :less :items nil}
|
||||
:current-page {:status :success :show :less :items nil}
|
||||
:nodes {:status :success :show :less :items nil}
|
||||
:code {:status :success :show :less :items nil}
|
||||
:codes {:status :success :show :less :items nil}
|
||||
:files {:status :success :show :less :items nil}
|
||||
:themes {:status :success :show :less :items nil}
|
||||
:filters {:status :success :show :less :items nil}})
|
||||
@@ -94,18 +108,18 @@
|
||||
(when (ldb/class? class)
|
||||
class))]
|
||||
(->> [{:text (cond
|
||||
class "Configure tag"
|
||||
class? "Create tag"
|
||||
:else "Create page")
|
||||
class (t :cmdk.create/configure-tag)
|
||||
class? (t :cmdk.create/tag)
|
||||
:else (t :cmdk.create/page))
|
||||
:icon (if class "settings" "new-page")
|
||||
:icon-theme :gray
|
||||
:info (cond
|
||||
class
|
||||
(str "Configure #" class-name)
|
||||
(t :cmdk.info/configure-tag class-name)
|
||||
class?
|
||||
(str "Create tag called '" class-name "'")
|
||||
(t :cmdk.info/create-tag class-name)
|
||||
:else
|
||||
(str "Create page called '" q "'"))
|
||||
(t :cmdk.info/create-page q))
|
||||
:source-create :page
|
||||
:class class}]
|
||||
(remove nil?)))))
|
||||
@@ -143,41 +157,38 @@
|
||||
[]
|
||||
|
||||
start-with-slash?
|
||||
[["Filters" :filters (visible-items :filters)]
|
||||
["Current page" :current-page (visible-items :current-page)]
|
||||
["Nodes" :nodes (visible-items :nodes)]]
|
||||
[[(group-label :filters) :filters (visible-items :filters)]
|
||||
[(group-label :current-page) :current-page (visible-items :current-page)]
|
||||
[(group-label :nodes) :nodes (visible-items :nodes)]]
|
||||
|
||||
include-slash?
|
||||
[(when-not node-exists?
|
||||
["Create" :create (create-items input)])
|
||||
[(group-label :create) :create (create-items input)])
|
||||
|
||||
["Current page" :current-page (visible-items :current-page)]
|
||||
["Nodes" :nodes (visible-items :nodes)]
|
||||
["Files" :files (visible-items :files)]
|
||||
["Filters" :filters (visible-items :filters)]]
|
||||
[(group-label :current-page) :current-page (visible-items :current-page)]
|
||||
[(group-label :nodes) :nodes (visible-items :nodes)]
|
||||
[(group-label :files) :files (visible-items :files)]
|
||||
[(group-label :filters) :filters (visible-items :filters)]]
|
||||
|
||||
filter-group
|
||||
[(when (= filter-group :nodes)
|
||||
["Current page" :current-page (visible-items :current-page)])
|
||||
[(cond
|
||||
(= filter-group :current-page) "Current page"
|
||||
(= filter-group :code) "Code"
|
||||
:else (name filter-group))
|
||||
[(group-label :current-page) :current-page (visible-items :current-page)])
|
||||
[(group-label filter-group)
|
||||
filter-group
|
||||
(visible-items filter-group)]
|
||||
(when-not node-exists?
|
||||
["Create" :create (create-items input)])]
|
||||
[(group-label :create) :create (create-items input)])]
|
||||
|
||||
:else
|
||||
(->>
|
||||
[(when-not node-exists?
|
||||
["Create" :create (create-items input)])
|
||||
["Current page" :current-page (visible-items :current-page)]
|
||||
["Nodes" :nodes (visible-items :nodes)]
|
||||
["Recently updated" :recently-updated-pages (visible-items :recently-updated-pages)]
|
||||
["Commands" :commands (visible-items :commands)]
|
||||
["Files" :files (visible-items :files)]
|
||||
["Filters" :filters (visible-items :filters)]]
|
||||
[(group-label :create) :create (create-items input)])
|
||||
[(group-label :current-page) :current-page (visible-items :current-page)]
|
||||
[(group-label :nodes) :nodes (visible-items :nodes)]
|
||||
[(group-label :recently-updated-pages) :recently-updated-pages (visible-items :recently-updated-pages)]
|
||||
[(group-label :commands) :commands (visible-items :commands)]
|
||||
[(group-label :files) :files (visible-items :files)]
|
||||
[(group-label :filters) :filters (visible-items :filters)]]
|
||||
(remove nil?)))
|
||||
order (remove nil? order*)]
|
||||
(for [[group-name group-key group-items] order]
|
||||
@@ -188,22 +199,29 @@
|
||||
(count (get-in results [group-key :items])))
|
||||
(mapv #(assoc % :group group-key :item-index (vswap! index inc)) group-items)])))
|
||||
|
||||
(defn state->highlighted-item [state]
|
||||
(or (some-> state ::highlighted-item deref)
|
||||
(first @(::all-items-cache state))))
|
||||
(defn state->highlighted-item
|
||||
([state]
|
||||
(state->highlighted-item state nil))
|
||||
([state fallback-item]
|
||||
(or (some-> state ::highlighted-item deref)
|
||||
fallback-item
|
||||
(first @(::all-items-cache state)))))
|
||||
|
||||
(defn state->action [state]
|
||||
(let [highlighted-item (state->highlighted-item state)
|
||||
(defn state->action
|
||||
([state]
|
||||
(state->action state nil))
|
||||
([state fallback-item]
|
||||
(let [highlighted-item (state->highlighted-item state fallback-item)
|
||||
action (get-action)]
|
||||
(cond (and (:source-block highlighted-item) (= action :move-blocks)) :trigger
|
||||
(:source-block highlighted-item) :open
|
||||
(:file-path highlighted-item) :open
|
||||
(:source-search highlighted-item) :search
|
||||
(:source-command highlighted-item) :trigger
|
||||
(:source-create highlighted-item) :create
|
||||
(:filter highlighted-item) :filter
|
||||
(:source-theme highlighted-item) :theme
|
||||
:else nil)))
|
||||
(cond (and (:source-block highlighted-item) (= action :move-blocks)) :trigger
|
||||
(:source-block highlighted-item) :open
|
||||
(:file-path highlighted-item) :open
|
||||
(:source-search highlighted-item) :search
|
||||
(:source-command highlighted-item) :trigger
|
||||
(:source-create highlighted-item) :create
|
||||
(:filter highlighted-item) :filter
|
||||
(:source-theme highlighted-item) :theme
|
||||
:else nil))))
|
||||
|
||||
;; Each result group has it's own load-results function
|
||||
(defmulti load-results (fn [group _state] group))
|
||||
@@ -347,14 +365,14 @@
|
||||
(swap! !results update group merge {:status :success :items items-on-current-page}))
|
||||
(swap! !results update group merge {:status :success :items items})))))
|
||||
|
||||
(defmethod load-results :code [group state]
|
||||
(defmethod load-results :codes [group state]
|
||||
(let [!input (::input state)
|
||||
!results (::results state)
|
||||
repo (state/get-current-repo)
|
||||
current-page (when-let [id (page-util/get-current-page-id)]
|
||||
(db/entity id))
|
||||
opts (cmdk-state/cmdk-block-search-options
|
||||
{:filter-group :code
|
||||
{:filter-group :codes
|
||||
:dev? config/dev?})]
|
||||
(swap! !results assoc-in [group :status] :loading)
|
||||
(p/let [blocks (search/block-search repo @!input opts)
|
||||
@@ -385,7 +403,7 @@
|
||||
themes (if (string/blank? @!input)
|
||||
themes
|
||||
(search/fuzzy-search themes @!input :limit 100 :extract-fn :name))
|
||||
themes (cons {:name "Logseq Default theme"
|
||||
themes (cons {:name (t :theme/logseq-default)
|
||||
:pid "logseq-classic-theme"
|
||||
:mode (state/sub :ui/theme)
|
||||
:url nil} themes)
|
||||
@@ -583,7 +601,7 @@
|
||||
create-class? (string/starts-with? @!input "#")
|
||||
create-page? (= :page (:source-create item))
|
||||
class (when create-class? (get-class-from-input @!input))]
|
||||
(if (and (= (:text item) "Configure tag") (:class item))
|
||||
(if (:class item)
|
||||
(state/pub-event! [:dialog/show-block (:class item) {:tag-dialog? true}])
|
||||
(p/let [result (cond
|
||||
create-class?
|
||||
@@ -797,10 +815,10 @@
|
||||
can-show-more? (< (count visible-items) (count items))
|
||||
show-less #(swap! (::results state) assoc-in [group :show] :less)
|
||||
show-more #(swap! (::results state) assoc-in [group :show] :more)]
|
||||
[:div {:class (if (= title "Create")
|
||||
[:div {:class (if (= group :create)
|
||||
"border-b border-gray-06 last:border-b-0"
|
||||
"border-b border-gray-06 pb-1 last:border-b-0")}
|
||||
(when-not (= title "Create")
|
||||
(when-not (= group :create)
|
||||
[:div {:class "text-xs py-1.5 px-3 flex justify-between items-center gap-2 text-gray-11 bg-gray-02 h-8"}
|
||||
[:div {:class "font-bold text-gray-11 pl-0.5 cursor-pointer select-none"
|
||||
:on-click (fn [_e]
|
||||
@@ -828,10 +846,10 @@
|
||||
((if (= show :more) show-less show-more)))}
|
||||
(if (= show :more)
|
||||
[:div.flex.flex-row.gap-1.items-center
|
||||
"Show less"
|
||||
(t :ui/show-less)
|
||||
(shui/shortcut "mod up" {:style :compact})]
|
||||
[:div.flex.flex-row.gap-1.items-center
|
||||
"Show more"
|
||||
(t :ui/show-more)
|
||||
(shui/shortcut "mod down" {:style :compact})])])])
|
||||
|
||||
[:div.search-results
|
||||
@@ -926,7 +944,7 @@
|
||||
(:block/properties page'))]
|
||||
(if link
|
||||
(js/window.open link)
|
||||
(notification/show! "No link found in this page's properties." :warning)))
|
||||
(notification/show! (t :cmdk.error/no-page-link) :warning)))
|
||||
|
||||
(:source-block item)
|
||||
(p/let [block-id (:block/uuid (:source-block item))
|
||||
@@ -935,9 +953,9 @@
|
||||
link (re-find editor-handler/url-regex (:block/title block))]
|
||||
(if link
|
||||
(js/window.open link)
|
||||
(notification/show! "No link found in this block's content." :warning)))
|
||||
(notification/show! (t :cmdk.error/no-block-link) :warning)))
|
||||
:else
|
||||
(notification/show! "No link for this search item." :warning))))
|
||||
(notification/show! (t :cmdk.error/no-search-item-link) :warning))))
|
||||
|
||||
(defn- keydown-handler
|
||||
[state e]
|
||||
@@ -1023,16 +1041,16 @@
|
||||
action (get-action)]
|
||||
(cond
|
||||
(= action :move-blocks)
|
||||
"Move blocks to"
|
||||
(t :cmdk.input/move-blocks-placeholder)
|
||||
|
||||
(= search-mode :graph)
|
||||
"Add graph filter"
|
||||
(t :cmdk.input/add-graph-filter-placeholder)
|
||||
|
||||
(= action :new-page)
|
||||
"Type a page name to create"
|
||||
(t :cmdk.input/type-page-name-placeholder)
|
||||
|
||||
:else
|
||||
"What are you looking for?")))
|
||||
(t :cmdk.input/default-placeholder))))
|
||||
|
||||
(rum/defc input-row
|
||||
[state all-items opts]
|
||||
@@ -1090,16 +1108,18 @@
|
||||
:on-composition-end debounced-composition-end
|
||||
:default-value input}]]))
|
||||
|
||||
(defn- tip-with-shortcut
|
||||
[template shortcut & [shortcut-opts]]
|
||||
(into [:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100]
|
||||
(i18n/interpolate-rich-text
|
||||
template
|
||||
[(shui/shortcut shortcut shortcut-opts)])))
|
||||
|
||||
(defn rand-tip
|
||||
[]
|
||||
(rand-nth
|
||||
[[:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100
|
||||
[:div "Type"]
|
||||
(shui/shortcut "/")
|
||||
[:div "to filter search results"]]
|
||||
[:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100
|
||||
(shui/shortcut ["mod" "enter"] {:style :combo})
|
||||
[:div "to open search in the sidebar"]]]))
|
||||
[(tip-with-shortcut (t :cmdk.tip/filter-results) "/")
|
||||
(tip-with-shortcut (t :cmdk.tip/open-sidebar) ["mod" "enter"] {:style :combo})]))
|
||||
|
||||
(rum/defcs tip <
|
||||
{:init (fn [state]
|
||||
@@ -1108,10 +1128,7 @@
|
||||
(let [filter' @(::filter state)]
|
||||
(cond
|
||||
filter'
|
||||
[:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100
|
||||
[:div "Type"]
|
||||
(shui/shortcut "esc")
|
||||
[:div "to clear search filter"]]
|
||||
(tip-with-shortcut (t :cmdk.tip/clear-filter) "esc")
|
||||
|
||||
:else
|
||||
(::rand-tip inner-state))))
|
||||
@@ -1137,49 +1154,53 @@
|
||||
:aria-hidden? true})))]))
|
||||
|
||||
(rum/defc hints
|
||||
[state]
|
||||
(let [action (state->action state)
|
||||
[state fallback-item]
|
||||
(let [action (state->action state fallback-item)
|
||||
button-fn (fn [text shortcut & {:as opts}]
|
||||
(hint-button text shortcut
|
||||
{:on-click #(handle-action action (assoc state :opts opts) %)
|
||||
:muted true}))]
|
||||
(when action
|
||||
[:div.hints
|
||||
[:div.text-sm.leading-6
|
||||
[:div.flex.flex-row.gap-1.items-center
|
||||
[:div.font-medium.text-gray-12 "Tip:"]
|
||||
(tip state)]]
|
||||
[:div.hints
|
||||
[:div.text-sm.leading-6
|
||||
[:div.flex.flex-row.gap-1.items-center]
|
||||
[:div.font-medium.text-gray-12 (t :cmdk.tip/label)
|
||||
(tip state)]]
|
||||
|
||||
[:div.gap-2.hidden.md:flex {:style {:margin-right -6}}
|
||||
(case action
|
||||
:open
|
||||
[:<>
|
||||
(button-fn "Open" ["return"])
|
||||
(button-fn "Open in sidebar" ["shift" "return"] {:open-sidebar? true})
|
||||
(when (:source-block @(::highlighted-item state)) (button-fn "Copy ref" ["cmd" "c"]))]
|
||||
[:div.gap-2.hidden.md:flex {:style {:margin-right -6}}
|
||||
(case action
|
||||
:open
|
||||
[:<>
|
||||
(button-fn (t :cmdk.action/open) ["return"])
|
||||
(button-fn (t :cmdk.action/open-in-sidebar) ["shift" "return"] {:open-sidebar? true})
|
||||
(when (:source-block (state->highlighted-item state fallback-item))
|
||||
(button-fn (t :cmdk.action/copy-ref) ["cmd" "c"]))]
|
||||
|
||||
:search
|
||||
[:<>
|
||||
(button-fn "Search" ["return"])]
|
||||
:search
|
||||
[:<>
|
||||
(button-fn (t :cmdk.action/search) ["return"])]
|
||||
|
||||
:trigger
|
||||
[:<>
|
||||
(button-fn "Trigger" ["return"])]
|
||||
:trigger
|
||||
[:<>
|
||||
(button-fn (t :cmdk.action/trigger) ["return"])]
|
||||
|
||||
:create
|
||||
[:<>
|
||||
(button-fn "Create" ["return"])]
|
||||
:create
|
||||
[:<>
|
||||
(button-fn (t :cmdk.action/create) ["return"])]
|
||||
|
||||
:filter
|
||||
[:<>
|
||||
(button-fn "Filter" ["return"])]
|
||||
:filter
|
||||
[:<>
|
||||
(button-fn (t :cmdk.action/filter) ["return"])]
|
||||
|
||||
nil)]])))
|
||||
:theme
|
||||
[:<>
|
||||
(button-fn (t :cmdk.action/apply-theme) ["return"])]
|
||||
|
||||
nil)]]))
|
||||
|
||||
(rum/defc search-only
|
||||
[state group-name]
|
||||
[:div.flex.flex-row.gap-1.items-center
|
||||
[:div "Search only:"]
|
||||
[:div (t :cmdk.filter/only-label)]
|
||||
[:div group-name]
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
@@ -1270,7 +1291,7 @@
|
||||
|
||||
(when group-filter
|
||||
[:div.flex.flex-col.px-3.py-1.opacity-70.text-sm
|
||||
(search-only state (string/capitalize (name group-filter)))])
|
||||
(search-only state (group-label group-filter))])
|
||||
|
||||
(let [items (filter
|
||||
(fn [[_group-name group-key group-count _group-items]]
|
||||
@@ -1288,8 +1309,8 @@
|
||||
(result-group state title group-key group-items first-item sidebar?)))
|
||||
[:div.flex.flex-col.p-4.opacity-50
|
||||
(when-not (string/blank? @*input)
|
||||
"No matched results")]))]
|
||||
(when-not sidebar? (hints state))]))
|
||||
(t :search/no-result))]))]
|
||||
(when-not sidebar? (hints state first-item))]))
|
||||
|
||||
(rum/defc cmdk-modal [props]
|
||||
[:div {:class "cp__cmdk__modal rounded-lg w-[90dvw] max-w-4xl relative"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[frontend.components.theme :as theme]
|
||||
[frontend.components.window-controls :as window-controls]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.context.i18n :refer [interpolate-rich-text-node t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.async :as db-async]
|
||||
@@ -192,31 +192,35 @@
|
||||
{:on-click state/toggle-document-mode!}
|
||||
"D"]
|
||||
[:div.p-2
|
||||
[:p.mb-2 [:b "Document mode"]]
|
||||
[:p.mb-2 [:b (t :editor.document-mode/title)]]
|
||||
[:ul
|
||||
[:li
|
||||
[:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line)
|
||||
:shortcut-id :editor/new-line)]
|
||||
[:p.inline-block "to create new block"]]
|
||||
[:li
|
||||
[:p.inline-block.mr-1 "Click `D` or type"]
|
||||
[:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode)
|
||||
:shortcut-id :ui/toggle-document-mode)]
|
||||
[:p.inline-block "to toggle document mode"]]]])))
|
||||
[:p.inline-block.mr-1
|
||||
(interpolate-rich-text-node
|
||||
(t :editor.document-mode/new-block-hint)
|
||||
[[:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line)
|
||||
:shortcut-id :editor/new-line)]])]
|
||||
[:li
|
||||
[:p.inline-block.mr-1
|
||||
(interpolate-rich-text-node
|
||||
(t :editor.document-mode/toggle-desc)
|
||||
[[:div.inline-block.mr-1
|
||||
(ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode)
|
||||
:shortcut-id :ui/toggle-document-mode)]])]]]]])))
|
||||
|
||||
(def help-menu-items
|
||||
[{:title "Handbook" :icon "book-2" :on-click #(handbooks/toggle-handbooks)}
|
||||
{:title "Keyboard shortcuts" :icon "command" :on-click #(state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings)}
|
||||
{:title "Documentation" :icon "help" :href "https://docs.logseq.com/"}
|
||||
[{:title (t :help/handbook) :icon "book-2" :on-click #(handbooks/toggle-handbooks)}
|
||||
{:title (t :help.shortcuts/label) :icon "command" :on-click #(state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings)}
|
||||
{:title (t :help/docs) :icon "help" :href "https://docs.logseq.com/"}
|
||||
:hr
|
||||
{:title "Report bug" :icon "bug" :on-click #(rfe/push-state :bug-report)}
|
||||
{:title "Request feature" :icon "git-pull-request" :href "https://discuss.logseq.com/c/feedback/feature-requests/"}
|
||||
{:title "Submit feedback" :icon "messages" :href "https://discuss.logseq.com/c/feedback/13"}
|
||||
{:title (t :help/bug) :icon "bug" :on-click #(rfe/push-state :bug-report)}
|
||||
{:title (t :help/feature) :icon "git-pull-request" :href "https://discuss.logseq.com/c/feedback/feature-requests/"}
|
||||
{:title (t :help/submit-feedback) :icon "messages" :href "https://discuss.logseq.com/c/feedback/13"}
|
||||
:hr
|
||||
{:title "Ask the community" :icon "brand-discord" :href "https://discord.com/invite/KpN4eHY"}
|
||||
{:title "Support forum" :icon "message" :href "https://discuss.logseq.com/"}
|
||||
{:title (t :help/ask-community) :icon "brand-discord" :href "https://discord.com/invite/KpN4eHY"}
|
||||
{:title (t :help/support-forum) :icon "message" :href "https://discuss.logseq.com/"}
|
||||
:hr
|
||||
{:title "Release notes" :icon "asterisk" :href "https://docs.logseq.com/#/page/changelog"}])
|
||||
{:title (t :help/release-notes) :icon "asterisk" :href "https://docs.logseq.com/#/page/changelog"}])
|
||||
|
||||
(rum/defc help-menu-popup
|
||||
[]
|
||||
@@ -258,13 +262,15 @@
|
||||
handbooks-open? (state/sub :ui/handbooks-open?)]
|
||||
[:<>
|
||||
[:div.cp__sidebar-help-btn
|
||||
[:div.inner
|
||||
{:title (t :help-shortcut-title)
|
||||
:on-click #(state/toggle! :ui/help-open?)}
|
||||
[:svg.scale-125 {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "24", :view-box "0 0 24 24", :xmlns "http://www.w3.org/2000/svg", :stroke-linecap "round", :stroke-width "2", :class "icon icon-tabler icon-tabler-help-small", :height "24"}
|
||||
[:path {:stroke "none", :d "M0 0h24v24H0z", :fill "none"}]
|
||||
[:path {:d "M12 16v.01"}]
|
||||
[:path {:d "M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"}]]]]
|
||||
(ui/tooltip
|
||||
[:div.inner
|
||||
{:on-click #(state/toggle! :ui/help-open?)}
|
||||
[:svg.scale-125 {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "24", :view-box "0 0 24 24", :xmlns "http://www.w3.org/2000/svg", :stroke-linecap "round", :stroke-width "2", :class "icon icon-tabler icon-tabler-help-small", :height "24"}
|
||||
[:path {:stroke "none", :d "M0 0h24v24H0z", :fill "none"}]
|
||||
[:path {:d "M12 16v.01"}]
|
||||
[:path {:d "M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"}]]]
|
||||
(t :help.shortcuts/desc)
|
||||
{:root-props {:delay-duration 100}})]
|
||||
|
||||
(when help-open?
|
||||
(help-menu-popup))
|
||||
@@ -304,7 +310,9 @@
|
||||
(when (context-menu-click-should-hide? target)
|
||||
(shui/popup-hide! id))))
|
||||
:data-keep-selection true}
|
||||
content])
|
||||
(if (fn? content)
|
||||
(content {:id id})
|
||||
content)])
|
||||
(merge
|
||||
{:on-before-hide state/dom-clear-selection!
|
||||
:on-after-hide state/state-clear-selection!
|
||||
@@ -316,7 +324,8 @@
|
||||
(cond
|
||||
(and page (not block-id))
|
||||
(do
|
||||
(show! (cp-content/page-title-custom-context-menu-content page-entity))
|
||||
(show! (fn [{:keys [id]}]
|
||||
(cp-content/page-title-custom-context-menu-content page-entity id)))
|
||||
(state/set-state! :page-title/context nil))
|
||||
|
||||
block-ref
|
||||
@@ -442,7 +451,7 @@
|
||||
:on-key-up (fn [e]
|
||||
(when (= "Enter" (.-key e))
|
||||
(ui/focus-element (ui/main-node))))}
|
||||
(t :accessibility/skip-to-main-content)]
|
||||
(t :nav/skip-to-main-content)]
|
||||
[:div.#app-container
|
||||
{:on-mouse-up on-mouse-up}
|
||||
[:div#left-container
|
||||
@@ -459,7 +468,7 @@
|
||||
|
||||
(if (state/sub :rtc/uploading?)
|
||||
[:div.flex.items-center.justify-center.full-height-without-header
|
||||
(ui/loading "Creating remote graph...")]
|
||||
(ui/loading (t :sync/creating-remote-graph))]
|
||||
(main {:route-match route-match
|
||||
:margin-less-pages? margin-less-pages?
|
||||
:logged? logged?
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.property.util :as pu]
|
||||
[frontend.handler.reaction :as reaction-handler]
|
||||
[frontend.modules.shortcut.data-helper :as shortcut-dh]
|
||||
[frontend.state :as state]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.util :as util]
|
||||
@@ -95,7 +96,7 @@
|
||||
(shui/dropdown-menu-item
|
||||
{:key "copy"
|
||||
:on-click #(editor-handler/copy-selection-blocks true)}
|
||||
(t :editor/copy)
|
||||
(t :ui/copy)
|
||||
(ui/dropdown-shortcut :editor/copy))
|
||||
|
||||
(shui/dropdown-menu-item
|
||||
@@ -106,12 +107,12 @@
|
||||
(shui/popup-hide!)
|
||||
(shui/dialog-open!
|
||||
#(export/export-blocks block-uuids {:export-type :selected-nodes}))))}
|
||||
(t :content/copy-export-as))
|
||||
(t :export/copy-or-export-as))
|
||||
|
||||
(shui/dropdown-menu-item
|
||||
{:key "copy block refs"
|
||||
:on-click editor-handler/copy-block-refs}
|
||||
(t :content/copy-block-ref))
|
||||
(t :block/copy-ref))
|
||||
|
||||
(shui/dropdown-menu-separator)
|
||||
|
||||
@@ -174,12 +175,12 @@
|
||||
{:key "Open in sidebar"
|
||||
:on-click (fn [_e]
|
||||
(editor-handler/open-block-in-sidebar! block-id))}
|
||||
(t :content/open-in-sidebar)
|
||||
(t :sidebar.right/open)
|
||||
(ui/dropdown-shortcut "shift+click"))
|
||||
|
||||
(shui/dropdown-menu-sub
|
||||
(shui/dropdown-menu-sub-trigger
|
||||
"Add reaction")
|
||||
(t :command.editor/add-reaction))
|
||||
(shui/dropdown-menu-sub-content
|
||||
[:div.p-1
|
||||
(icon-component/icon-search
|
||||
@@ -191,8 +192,8 @@
|
||||
(reaction-handler/toggle-reaction! block-id emoji-id)
|
||||
(state/hide-custom-context-menu!)
|
||||
(shui/popup-hide!))
|
||||
(notification/show! "Please pick an emoji reaction." :warning))))
|
||||
:tabs [[:emoji "Emojis"]]
|
||||
(notification/show! (t :block.reaction/emoji-required-warning) :warning))))
|
||||
:tabs [[:emoji (t :icon/tab-emojis)]]
|
||||
:default-tab :emoji
|
||||
:show-used? true
|
||||
:icon-value nil})]))
|
||||
@@ -230,7 +231,7 @@
|
||||
{:key "Copy block ref"
|
||||
:on-click (fn [_e]
|
||||
(editor-handler/copy-block-ref! block-id ref/->block-ref))}
|
||||
(t :content/copy-block-ref))
|
||||
(t :block/copy-ref))
|
||||
|
||||
;; TODO Logseq protocol mobile support
|
||||
(when (util/electron?)
|
||||
@@ -241,7 +242,7 @@
|
||||
tap-f (fn [block-id]
|
||||
(url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
|
||||
(editor-handler/copy-block-ref! block-id tap-f)))}
|
||||
(t :content/copy-block-url)))
|
||||
(t :block/copy-url)))
|
||||
|
||||
(when (and (util/electron?) (ldb/asset? block))
|
||||
(shui/dropdown-menu-item
|
||||
@@ -258,7 +259,7 @@
|
||||
:on-click (fn [_]
|
||||
(shui/dialog-open!
|
||||
#(export/export-blocks [block-id] {:export-type :block})))}
|
||||
(t :content/copy-export-as))
|
||||
(t :export/copy-or-export-as))
|
||||
|
||||
(when-not property-default-value?
|
||||
(shui/dropdown-menu-item
|
||||
@@ -321,29 +322,29 @@
|
||||
(shui/dropdown-menu-separator)
|
||||
(shui/dropdown-menu-sub
|
||||
(shui/dropdown-menu-sub-trigger
|
||||
"Developer tools")
|
||||
(t :context-menu/developer-tools))
|
||||
|
||||
(shui/dropdown-menu-sub-content
|
||||
(shui/dropdown-menu-item
|
||||
{:key "(Dev) Show block data"
|
||||
(shui/dropdown-menu-sub-content
|
||||
(shui/dropdown-menu-item
|
||||
{:key :dev/show-block-data
|
||||
:on-click (fn []
|
||||
(dev-common-handler/show-entity-data [:block/uuid block-id]))}
|
||||
(t :dev/show-block-data))
|
||||
(shortcut-dh/shortcut-desc-by-id :dev/show-block-data))
|
||||
(shui/dropdown-menu-item
|
||||
{:key "(Dev) Show block AST"
|
||||
{:key :dev/show-block-ast
|
||||
:on-click (fn []
|
||||
(let [block (db/entity [:block/uuid block-id])]
|
||||
(dev-common-handler/show-content-ast (:block/title block)
|
||||
(get block :block/format :markdown))))}
|
||||
(t :dev/show-block-ast))
|
||||
(shortcut-dh/shortcut-desc-by-id :dev/show-block-ast))
|
||||
(shui/dropdown-menu-item
|
||||
{:key "(Dev) Show block content history"
|
||||
{:key :dev/show-block-content-history
|
||||
:on-click
|
||||
(fn []
|
||||
(let [token (state/get-auth-id-token)
|
||||
graph-uuid (ldb/get-graph-rtc-uuid (db/get-db))]
|
||||
(p/let [blocks-versions (state/<invoke-db-worker :thread-api/rtc-get-block-content-versions token graph-uuid block-id)]
|
||||
(prn :Dev-show-block-content-history)
|
||||
(prn :dev/show-block-content-history)
|
||||
(doseq [[block-uuid versions] blocks-versions]
|
||||
(prn :block-uuid block-uuid)
|
||||
(pp/print-table [:content :created-at]
|
||||
@@ -351,7 +352,6 @@
|
||||
{:created-at (tc/from-long (* (:created-at version) 1000))
|
||||
:content (:value version)})
|
||||
versions))))))}
|
||||
|
||||
"(Dev) Show block content history")))])]))))
|
||||
|
||||
(rum/defc block-ref-custom-context-menu-content
|
||||
@@ -365,32 +365,38 @@
|
||||
(state/get-current-repo)
|
||||
block-ref-id
|
||||
:block-ref))}
|
||||
(t :content/open-in-sidebar)
|
||||
(t :sidebar.right/open)
|
||||
(ui/dropdown-shortcut "shift+click"))
|
||||
(shui/dropdown-menu-item
|
||||
{:key "copy"
|
||||
:on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
|
||||
(t :content/copy-ref))
|
||||
(t :reference/copy))
|
||||
(shui/dropdown-menu-item
|
||||
{:key "delete"
|
||||
:on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
|
||||
(t :content/delete-ref))
|
||||
(t :reference/delete))
|
||||
(shui/dropdown-menu-item
|
||||
{:key "replace-with-text"
|
||||
:on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
|
||||
(t :content/replace-with-text))
|
||||
(t :reference/replace-with-text))
|
||||
(shui/dropdown-menu-item
|
||||
{:key "replace-with-embed"
|
||||
:on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
|
||||
(t :content/replace-with-embed))]))
|
||||
(t :reference/replace-with-embed))]))
|
||||
|
||||
(rum/defc page-title-custom-context-menu-content
|
||||
[page]
|
||||
[page popup-id]
|
||||
(when page
|
||||
(let [page-menu-options (page-menu/page-menu page)]
|
||||
[:<>
|
||||
(for [{:keys [title options]} page-menu-options]
|
||||
(shui/dropdown-menu-item options title))])))
|
||||
(let [on-click (:on-click options)]
|
||||
(shui/dropdown-menu-item
|
||||
(assoc options
|
||||
:on-click (fn [e]
|
||||
(when-not (false? (when on-click (on-click e)))
|
||||
(shui/popup-hide! popup-id))))
|
||||
title)))])))
|
||||
|
||||
;; TODO: content could be changed
|
||||
;; Also, keyboard bindings should only be activated after
|
||||
@@ -400,7 +406,7 @@
|
||||
[:div {:id id}
|
||||
(if hiccup
|
||||
hiccup
|
||||
[:div.cursor (t :content/click-to-edit)])])
|
||||
[:div.cursor (t :editor/click-to-edit)])])
|
||||
|
||||
(rum/defc non-hiccup-content
|
||||
[id content on-click on-hide config format]
|
||||
@@ -421,7 +427,7 @@
|
||||
{:id id
|
||||
:on-click on-click}
|
||||
(if (string/blank? content)
|
||||
[:div.cursor (t :content/click-to-edit)]
|
||||
[:div.cursor (t :editor/click-to-edit)]
|
||||
content)]))))
|
||||
|
||||
(rum/defcs content < rum/reactive
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.components.db-based.page
|
||||
"Page components only for DB graphs"
|
||||
(:require [frontend.components.property.config :as property-config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.util :as util]
|
||||
@@ -23,4 +24,4 @@
|
||||
:align "start"
|
||||
:as-dropdown? true
|
||||
:dropdown-menu? true}))}
|
||||
"Configure property")))
|
||||
(t :property/configure))))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.components.e2ee
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.common.crypt :as crypt]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.state :as state]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.util :as util]
|
||||
@@ -19,27 +20,20 @@
|
||||
(shui/dialog-close!))]
|
||||
[:div.e2ee-password-modal-overlay
|
||||
[:div.encryption-password.max-w-2xl.e2ee-password-modal-content.flex.flex-col.gap-8.p-4
|
||||
[:div.text-2xl.font-medium "Set password for remote graphs"]
|
||||
[:div.text-2xl.font-medium (t :encryption/set-password-title)]
|
||||
|
||||
[:div.init-remote-pw-tips.space-x-4.hidden.sm:flex
|
||||
[:div.flex-1.flex.items-center
|
||||
[:span.px-3.flex (ui/icon "key")]
|
||||
[:p
|
||||
[:span "Please make sure you "]
|
||||
"remember the password you have set, as we are unable to reset or retrieve it in case you forget it, "
|
||||
[:span "and we recommend you "]
|
||||
"keep a secure backup "
|
||||
[:span "of the password."]]]
|
||||
[:p (t :encryption/remember-password-rich)]]
|
||||
|
||||
[:div.flex-1.flex.items-center
|
||||
[:span.px-3.flex (ui/icon "lock")]
|
||||
[:p
|
||||
"If you lose your password, all of your data in the cloud can’t be decrypted. "
|
||||
[:span "You will still be able to access the local version of your graph."]]]]
|
||||
[:p (t :encryption/cloud-password-rich)]]]
|
||||
|
||||
[:div.flex.flex-col.gap-4
|
||||
(shui/toggle-password
|
||||
{:placeholder "Enter password"
|
||||
{:placeholder (t :encryption/enter-password)
|
||||
:value password
|
||||
:on-change (fn [e] (set-password! (-> e .-target .-value)))
|
||||
:on-blur (fn []
|
||||
@@ -48,20 +42,20 @@
|
||||
|
||||
[:div.flex.flex-col.gap-2
|
||||
(shui/toggle-password
|
||||
{:placeholder "Enter password again"
|
||||
{:placeholder (t :encryption/enter-password-again)
|
||||
:value password-confirm
|
||||
:on-change (fn [e] (set-password-confirm! (-> e .-target .-value)))
|
||||
:on-blur (fn [] (set-matched! (= password-confirm password)))})
|
||||
|
||||
(when (false? matched?)
|
||||
[:div.text-warning.text-sm
|
||||
"Password not matched"])]
|
||||
(t :encryption/password-not-matched)])]
|
||||
|
||||
(shui/button
|
||||
{:on-click on-submit
|
||||
:disabled (or (string/blank? password)
|
||||
(false? matched?))}
|
||||
"Submit")]]]))
|
||||
(t :ui/submit))]]]))
|
||||
|
||||
(rum/defc e2ee-password-to-decrypt-private-key
|
||||
[encrypted-private-key private-key-promise refresh-token]
|
||||
@@ -78,7 +72,7 @@
|
||||
(set-decrypt-fail! true))))))]
|
||||
[:div.e2ee-password-modal-overlay
|
||||
[:div.e2ee-password-modal-content.flex.flex-col.gap-8.p-4
|
||||
[:div.text-2xl.font-medium "Enter password for remote graphs"]
|
||||
[:div.text-2xl.font-medium (t :encryption/enter-password-title)]
|
||||
[:div.flex.flex-col.gap-4
|
||||
[:div.flex.flex-col.gap-1
|
||||
(shui/toggle-password
|
||||
@@ -89,11 +83,11 @@
|
||||
:on-change (fn [e]
|
||||
(set-decrypt-fail! false)
|
||||
(set-password! (-> e .-target .-value)))})
|
||||
(when decrypt-fail? [:p.text-warning.text-sm "Wrong password"])]
|
||||
(when decrypt-fail? [:p.text-warning.text-sm (t :encryption/wrong-password)])]
|
||||
(shui/button
|
||||
{:on-click on-submit
|
||||
:disabled (string/blank? password)
|
||||
:on-key-press (fn [e]
|
||||
(when (= "Enter" (util/ekey e))
|
||||
(on-submit)))}
|
||||
"Submit")]]]))
|
||||
(t :ui/submit))]]]))
|
||||
|
||||
@@ -40,11 +40,14 @@
|
||||
(defn filter-commands
|
||||
[page? commands]
|
||||
(if page?
|
||||
(filter (fn [item]
|
||||
(or
|
||||
(= "Add new property" (first item))
|
||||
(when (= (count item) 5)
|
||||
(contains? #{"TASK STATUS" "TASK DATE" "PRIORITY"} (last item))))) commands)
|
||||
(let [task-groups #{(t :editor.slash/group-task-status)
|
||||
(t :editor.slash/group-task-date)
|
||||
(t :editor.slash/group-priority)}]
|
||||
(filter (fn [item]
|
||||
(or
|
||||
(= (t :command.editor/add-property) (first item))
|
||||
(when (= (count item) 5)
|
||||
(contains? task-groups (last item))))) commands))
|
||||
commands))
|
||||
|
||||
(defn node-render
|
||||
@@ -70,8 +73,8 @@
|
||||
(:nlp-date? block')
|
||||
(ui/icon "calendar" {:size 14})
|
||||
|
||||
(or (string/starts-with? (str (:block/title block')) (t :new-tag))
|
||||
(string/starts-with? (str (:block/title block')) (t :new-page)))
|
||||
(or (string/starts-with? (str (:block/title block')) (t :editor/new-tag))
|
||||
(string/starts-with? (str (:block/title block')) (t :editor/new-page)))
|
||||
(ui/icon "plus" {:size 14})
|
||||
|
||||
:else
|
||||
@@ -79,8 +82,8 @@
|
||||
|
||||
(let [title (let [alias (get-in block' [:alias :block/title])]
|
||||
(block-handler/block-unique-title block' {:alias alias}))]
|
||||
(if (or (string/starts-with? title (t :new-tag))
|
||||
(string/starts-with? title (t :new-page)))
|
||||
(if (or (string/starts-with? title (t :editor/new-tag))
|
||||
(string/starts-with? title (t :editor/new-page)))
|
||||
title
|
||||
(block-handler/block-title-with-icon block'
|
||||
(search-handler/highlight-exact-query title q)
|
||||
@@ -184,9 +187,9 @@
|
||||
;; Don't show 'New tag' for an internal page because it already shows 'Convert ...'
|
||||
(when-not (let [entity (db/get-page q)]
|
||||
(and (ldb/internal-page? entity) (= (:block/title entity) q)))
|
||||
[{:block/title (str (t :new-tag) " " q)}])
|
||||
[{:block/title (str (t :editor/new-tag) " " q)}])
|
||||
partial-matched-pages)
|
||||
(cons {:block/title (str (t :new-page) " " q)}
|
||||
(cons {:block/title (str (t :editor/new-page) " " q)}
|
||||
partial-matched-pages)))))
|
||||
|
||||
(defn- search-pages
|
||||
@@ -202,7 +205,7 @@
|
||||
:db/id (:db/id block)
|
||||
:block/uuid (:block/uuid block)
|
||||
:convert-page-to-tag? true
|
||||
:friendly-title (util/format "Convert \"%s\" to tag" q)} classes)
|
||||
:friendly-title (t :page.convert/page-to-tag-action q)} classes)
|
||||
classes))
|
||||
(editor-handler/<get-matched-blocks q {:nlp-pages? true
|
||||
:page-only? false}))]
|
||||
@@ -218,9 +221,7 @@
|
||||
(let [matched-pages' (if (string/blank? q)
|
||||
(if db-tag?
|
||||
(db-model/get-all-classes (state/get-current-repo) {:except-root-class? true})
|
||||
(->> (map (fn [title] {:block/title title
|
||||
:nlp-date? true})
|
||||
date/nlp-pages)
|
||||
(->> (date/nlp-pages-i18n :nlp-date? true)
|
||||
(take 10)))
|
||||
;; reorder, shortest and starts-with first.
|
||||
(if (and (seq matched-pages)
|
||||
@@ -237,8 +238,8 @@
|
||||
:item-render (fn [block _chosen?]
|
||||
(node-render block q {:db-tag? db-tag?}))
|
||||
:empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
|
||||
"Search for a tag"
|
||||
"Search for a node")]
|
||||
(t :editor/search-for-tag)
|
||||
(t :editor/search-for-node))]
|
||||
:class "black"})
|
||||
|
||||
(when (and db-tag?
|
||||
@@ -246,7 +247,7 @@
|
||||
(not= "page" (string/lower-case q)))
|
||||
[:p.px-1.opacity-50.text-sm.flex.flex-row.items-center.gap-2
|
||||
(shui/shortcut "mod+enter")
|
||||
[:span " to display this tag inline instead of at the end of this node."]])])))
|
||||
[:span (t :editor/display-tag-inline-hint)]])])))
|
||||
|
||||
(rum/defcs page-search < rum/reactive
|
||||
{:init (fn [state]
|
||||
@@ -368,7 +369,7 @@
|
||||
matched-templates
|
||||
{:on-chosen (editor-handler/template-on-chosen-handler id)
|
||||
:on-enter (fn [_state] (state/clear-editor-action!))
|
||||
:empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
|
||||
:empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm (t :editor/search-template-placeholder)]
|
||||
:item-render (fn [template]
|
||||
(:block/title template))
|
||||
:class "black"})))
|
||||
@@ -471,7 +472,7 @@
|
||||
placeholder
|
||||
(assoc :placeholder placeholder))))
|
||||
(ui/button
|
||||
"Submit"
|
||||
(t :ui/submit)
|
||||
:on-click
|
||||
(fn [e]
|
||||
(util/stop e)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[cljs-time.core :as t]
|
||||
[cljs.pprint :as pprint]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.context.i18n :refer [interpolate-rich-text-node interpolate-sentence t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.block :as block-handler]
|
||||
[frontend.handler.db-based.export :as db-export-handler]
|
||||
@@ -32,31 +32,31 @@
|
||||
repo (state/get-current-repo)]
|
||||
[:div.flex.flex-col.gap-4
|
||||
[:div.font-medium.opacity-50
|
||||
"Schedule backup"]
|
||||
(t :export.backup/schedule)]
|
||||
(if (utils/nfsSupported)
|
||||
[:<>
|
||||
(if backup-folder
|
||||
[:div.flex.flex-row.items-center.gap-1.text-sm
|
||||
[:div.opacity-50 "Backup folder:"]
|
||||
[:div.opacity-50 (t :export.backup/folder)]
|
||||
backup-folder
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:class "!px-1 !py-1"
|
||||
:title "Change backup folder"
|
||||
:title (t :export.backup/cancel)
|
||||
:on-click (fn []
|
||||
(p/do!
|
||||
(db/transact! [[:db/retractEntity :logseq.kv/graph-backup-folder]])
|
||||
(reset! *backup-folder nil)))
|
||||
:size :sm}
|
||||
(ui/icon "edit"))]
|
||||
(ui/icon "x"))]
|
||||
(shui/button
|
||||
{:variant :default
|
||||
:on-click (fn []
|
||||
(p/let [[folder-name _handle] (export/choose-backup-folder repo)]
|
||||
(reset! *backup-folder folder-name)))}
|
||||
"Set backup folder first"))
|
||||
(t :export.backup/set-folder-first)))
|
||||
[:div.opacity-50.text-sm
|
||||
"Backup will be created every hour."]
|
||||
(t :export.backup/hourly-note)]
|
||||
|
||||
(when backup-folder
|
||||
(shui/button
|
||||
@@ -66,67 +66,68 @@
|
||||
(p/let [result (export/backup-db-graph repo)]
|
||||
(case result
|
||||
true
|
||||
(notification/show! "Backup successful!" :success)
|
||||
(notification/show! (t :export/backup-successful) :success)
|
||||
:graph-not-changed
|
||||
(notification/show! "Graph has not been updated since last export." :success)
|
||||
(notification/show! (t :export/no-updates-since-last-export) :success)
|
||||
nil)
|
||||
(export/auto-db-backup! repo))
|
||||
(p/catch (fn [error]
|
||||
(println "Failed to backup.")
|
||||
(js/console.error error)))))}
|
||||
"Backup now"))]
|
||||
(t :export.backup/backup-now)))]
|
||||
[:div
|
||||
[:span "Your browser doesn't support "]
|
||||
[:a
|
||||
{:href "https://developer.chrome.com/docs/capabilities/web-apis/file-system-access"
|
||||
:target "_blank"}
|
||||
"The File System Access API"]
|
||||
[:span ", please switch to a Chromium-based browser."]])]))
|
||||
[:span
|
||||
(interpolate-sentence
|
||||
(t :export.backup/unsupported-desc)
|
||||
:links [{:href "https://developer.chrome.com/docs/capabilities/web-apis/file-system-access"
|
||||
:target "_blank"}])]])]))
|
||||
|
||||
(rum/defc export
|
||||
[]
|
||||
(when-let [current-repo (state/get-current-repo)]
|
||||
[:div.export
|
||||
[:h1.title.mb-8 (t :export)]
|
||||
[:h1.title.mb-8 (t :export/title)]
|
||||
|
||||
[:div.flex.flex-col.gap-4.ml-1
|
||||
[:div
|
||||
[:a.font-medium {:on-click #(export/export-repo-as-sqlite-db! current-repo)}
|
||||
(t :export-sqlite-db)]
|
||||
[:p.text-sm.opacity-70.mb-0 "Primary way to backup graph's content to a single .sqlite file."]]
|
||||
(t :export/sqlite-db)]
|
||||
[:p.text-sm.opacity-70.mb-0 (t :export.backup/sqlite-desc)]]
|
||||
[:div
|
||||
[:a.font-medium {:on-click #(export/export-repo-as-zip! current-repo)}
|
||||
(t :export-zip)]
|
||||
[:p.text-sm.opacity-70.mb-0 "Primary way to backup graph's content and assets to a .zip file."]]
|
||||
(t :export/zip)]
|
||||
[:p.text-sm.opacity-70.mb-0 (t :export.backup/zip-desc)]]
|
||||
|
||||
(when-not (util/mobile?)
|
||||
[:div
|
||||
[:a.font-medium {:on-click #(db-export-handler/export-repo-as-db-edn! current-repo)}
|
||||
(t :export-db-edn)]
|
||||
[:p.text-sm.opacity-70.mb-0 "Exports to a readable and editable .edn file. Don't rely on this as a primary backup."]])
|
||||
(t :export/db-edn)]
|
||||
[:p.text-sm.opacity-70.mb-0 (t :export/edn-desc)]])
|
||||
(when-not (mobile-util/native-platform?)
|
||||
[:div
|
||||
[:a.font-medium {:on-click #(export-text/export-repo-as-markdown! current-repo)}
|
||||
(t :export-markdown)]])
|
||||
(t :export/markdown)]])
|
||||
|
||||
(when (util/electron?)
|
||||
[:div
|
||||
[:a.font-medium {:on-click #(export/download-repo-as-html! current-repo)}
|
||||
(t :export-public-pages)]])
|
||||
(t :export/public-pages)]])
|
||||
|
||||
[:div
|
||||
[:a.font-medium {:on-click #(export/export-repo-as-debug-transit! current-repo)}
|
||||
"Export debug transit file"]
|
||||
[:p.text-sm.opacity-70.mb-0 "Exports to a .transit file to send to us for debugging. Any sensitive data will be removed in the exported file."]]
|
||||
(t :export/debug-transit-file)]
|
||||
[:p.text-sm.opacity-70.mb-0 (t :export/debug-transit-desc)]]
|
||||
|
||||
(if (util/electron?)
|
||||
[:div
|
||||
[:hr]
|
||||
[:div "Hourly backups are enabled for this graph, "
|
||||
[:a.ml-1 {:on-click (fn []
|
||||
(let [path (config/get-electron-backup-dir (state/get-current-repo))]
|
||||
(js/window.apis.openPath path)))}
|
||||
"open backups folder for this graph"]]]
|
||||
[:div
|
||||
(interpolate-rich-text-node
|
||||
(t :export.backup/enabled-desc)
|
||||
[[:a.ml-1 {:on-click (fn []
|
||||
(let [path (config/get-electron-backup-dir (state/get-current-repo))]
|
||||
(js/window.apis.openPath path)))}
|
||||
(t :export.backup/open-folder)]])]]
|
||||
(when (and util/web-platform?
|
||||
(not (util/mobile?)))
|
||||
[:div
|
||||
@@ -135,11 +136,11 @@
|
||||
|
||||
(def *export-block-type (atom :text))
|
||||
|
||||
(def text-indent-style-options [{:label "dashes"
|
||||
(def text-indent-style-options [{:title-key :export/indent-style-dashes
|
||||
:selected false}
|
||||
{:label "spaces"
|
||||
{:title-key :export/indent-style-spaces
|
||||
:selected false}
|
||||
{:label "no-indent"
|
||||
{:title-key :export/indent-style-none
|
||||
:selected false}])
|
||||
|
||||
(defn- export-helper
|
||||
@@ -250,7 +251,7 @@
|
||||
{:class "-m-5"}
|
||||
[:div.p-6
|
||||
[:div.flex.pb-3
|
||||
(ui/button "Text"
|
||||
(ui/button (t :export/format-text)
|
||||
:class "mr-4 w-20"
|
||||
:on-click #(do (reset! *export-block-type :text)
|
||||
(reset! *content (export-helper top-level-uuids))))
|
||||
@@ -280,26 +281,26 @@
|
||||
(if (= :png tp)
|
||||
[:div.flex.items-center.justify-center.relative
|
||||
(when (not @*content) [:div.absolute (ui/loading "")])
|
||||
[:img {:alt "export preview" :id "export-preview" :class "my-4" :style {:visibility (when (not @*content) "hidden")}}]]
|
||||
[:img {:alt (t :export/preview-alt) :id "export-preview" :class "my-4" :style {:visibility (when (not @*content) "hidden")}}]]
|
||||
|
||||
[:textarea.overflow-y-auto.h-96 {:value @*content :read-only true}])
|
||||
|
||||
(if (= :png tp)
|
||||
[:div.flex.items-center
|
||||
[:div (t :export-transparent-background)]
|
||||
[:div (t :export/transparent-background)]
|
||||
(ui/checkbox {:class "mr-2 ml-4"
|
||||
:on-change (fn [e]
|
||||
(reset! *content nil)
|
||||
(get-image-blob top-level-uuids (merge options {:transparent-bg? e.currentTarget.checked}) (fn [blob] (reset! *content blob))))})]
|
||||
(let [options (->> text-indent-style-options
|
||||
(mapv (fn [opt]
|
||||
(if (= @*text-indent-style (:label opt))
|
||||
(if (= @*text-indent-style (:title-key opt))
|
||||
(assoc opt :selected true)
|
||||
opt))))]
|
||||
[:div [:div.flex.items-center
|
||||
[:label.mr-4
|
||||
{:style {:visibility (if (= :text tp) "visible" "hidden")}}
|
||||
"Indentation style:"]
|
||||
(t :export/indent-style-label)]
|
||||
[:select.block.my-2.text-lg.rounded.border.py-0.px-1
|
||||
{:style {:visibility (if (= :text tp) "visible" "hidden")}
|
||||
:on-change (fn [e]
|
||||
@@ -307,13 +308,13 @@
|
||||
(state/set-export-block-text-indent-style! value)
|
||||
(reset! *text-indent-style value)
|
||||
(reset! *content (export-helper top-level-uuids))))}
|
||||
(for [{:keys [label value selected]} options]
|
||||
(for [{:keys [title-key value selected]} options]
|
||||
[:option (cond->
|
||||
{:key label
|
||||
:value (or value label)}
|
||||
{:key title-key
|
||||
:value (or value (name title-key))}
|
||||
selected
|
||||
(assoc :selected selected))
|
||||
label])]]
|
||||
(t title-key)])]]
|
||||
[:div.flex.items-center
|
||||
(ui/checkbox {:class "mr-2"
|
||||
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
|
||||
@@ -323,7 +324,7 @@
|
||||
(reset! *text-remove-options (state/get-export-block-text-remove-options))
|
||||
(reset! *content (export-helper top-level-uuids)))})
|
||||
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
|
||||
"[[text]] -> text"]
|
||||
(t :export/page-ref-text)]
|
||||
|
||||
(ui/checkbox {:class "mr-2 ml-4"
|
||||
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
|
||||
@@ -334,7 +335,7 @@
|
||||
(reset! *content (export-helper top-level-uuids)))})
|
||||
|
||||
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
|
||||
"remove emphasis"]
|
||||
(t :export/remove-emphasis)]
|
||||
|
||||
(ui/checkbox {:class "mr-2 ml-4"
|
||||
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
|
||||
@@ -345,7 +346,7 @@
|
||||
(reset! *content (export-helper top-level-uuids)))})
|
||||
|
||||
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
|
||||
"remove #tags"]]
|
||||
(t :export/remove-tags)]]
|
||||
|
||||
[:div.flex.items-center
|
||||
(ui/checkbox {:class "mr-2"
|
||||
@@ -357,7 +358,7 @@
|
||||
(reset! *text-other-options (state/get-export-block-text-other-options))
|
||||
(reset! *content (export-helper top-level-uuids)))})
|
||||
[:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
|
||||
"newline after block"]
|
||||
(t :export/newline-after-block)]
|
||||
|
||||
(ui/checkbox {:class "mr-2 ml-4"
|
||||
:style {:visibility (if (#{:text} tp) "visible" "hidden")}
|
||||
@@ -367,7 +368,7 @@
|
||||
(reset! *text-remove-options (state/get-export-block-text-remove-options))
|
||||
(reset! *content (export-helper top-level-uuids)))})
|
||||
[:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
|
||||
"remove properties"]]
|
||||
(t :export/remove-properties)]]
|
||||
|
||||
[:div.flex.items-center
|
||||
(ui/checkbox {:class "mr-2"
|
||||
@@ -379,11 +380,11 @@
|
||||
(reset! *text-other-options (state/get-export-block-text-other-options))
|
||||
(reset! *content (export-helper top-level-uuids)))})
|
||||
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
|
||||
"open blocks only (skip collapsed children)"]]
|
||||
(t :export/open-blocks-only)]]
|
||||
|
||||
[:div.flex.items-center
|
||||
[:label.mr-2 {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
|
||||
"level <="]
|
||||
(t :export/level-lte)]
|
||||
[:select.block.my-2.text-lg.rounded.border.px-2.py-0
|
||||
{:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
|
||||
:value (or (:keep-only-level<=N @*text-other-options) :all)
|
||||
@@ -398,14 +399,14 @@
|
||||
|
||||
(when @*content
|
||||
[:div.mt-4.flex.flex-row.gap-2
|
||||
(ui/button (if @*copied? (t :export-copied-to-clipboard) (t :export-copy-to-clipboard))
|
||||
(ui/button (if @*copied? (t :export/copied-to-clipboard) (t :ui/copy-to-clipboard))
|
||||
:class "mr-4"
|
||||
:on-click (fn []
|
||||
(if (= tp :png)
|
||||
(js/navigator.clipboard.write [(js/ClipboardItem. #js {"image/png" @*content})])
|
||||
(util/copy-to-clipboard! @*content :html (when (= tp :html) @*content)))
|
||||
(reset! *copied? true)))
|
||||
(ui/button (t :export-save-to-file)
|
||||
(ui/button (t :export/save-to-file)
|
||||
:on-click #(let [file-name (if (uuid? top-level-uuids)
|
||||
(-> (db/get-page top-level-uuids)
|
||||
(util/get-page-title))
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
[]
|
||||
[:div.flex-1.overflow-hidden
|
||||
[:h1.title
|
||||
(t :all-files)]
|
||||
(t :nav/all-files)]
|
||||
(files-all)])
|
||||
|
||||
;; FIXME: misuse of rpath and fpath
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
(ns frontend.components.filepicker
|
||||
"File picker"
|
||||
(:require [rum.core :as rum]
|
||||
(:require [cljs-drag-n-drop.core :as dnd]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[goog.dom :as gdom]
|
||||
[logseq.shui.ui :as shui]
|
||||
[cljs-drag-n-drop.core :as dnd]
|
||||
[goog.dom :as gdom]))
|
||||
[rum.core :as rum]))
|
||||
|
||||
(rum/defcs picker <
|
||||
(rum/local nil ::input)
|
||||
@@ -43,4 +44,4 @@
|
||||
:height 28}})]
|
||||
[:div {:class "flex flex-col gap-px"}
|
||||
[:div {:class "font-medium text-muted-foreground"}
|
||||
"Drag 'n' drop files here, or click to select files"]]]]]]))
|
||||
(t :asset/drop-hint)]]]]]]))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns frontend.components.find-in-page
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
@@ -27,8 +28,8 @@
|
||||
[:div.flex.w-48.relative
|
||||
[:input#search-in-page-input.form-input.block.sm:text-sm.sm:leading-5.my-2.border-none.mr-4.outline-none
|
||||
{:auto-focus true
|
||||
:placeholder "Find in page"
|
||||
:aria-label "Find in page"
|
||||
:placeholder (t :search.find-in-page/input-placeholder)
|
||||
:aria-label (t :search.find-in-page/input-placeholder)
|
||||
:value q
|
||||
:on-composition-start on-change-fn
|
||||
:on-composition-end on-change-fn
|
||||
@@ -61,7 +62,7 @@
|
||||
(debounced-search))
|
||||
:intent "link"
|
||||
:small? true
|
||||
:title "Match case"
|
||||
:title (t :search.find-in-page/match-case)
|
||||
:class (str (when match-case? "active ") "text-lg"))
|
||||
|
||||
(ui/button
|
||||
@@ -72,7 +73,7 @@
|
||||
:intent "link"
|
||||
:small? true
|
||||
:class "text-lg"
|
||||
:title "Previous result")
|
||||
:title (t :search.find-in-page/previous-result))
|
||||
|
||||
(ui/button
|
||||
(ui/icon "caret-down")
|
||||
@@ -82,7 +83,7 @@
|
||||
:intent "link"
|
||||
:small? true
|
||||
:class "text-lg"
|
||||
:title "Next result")
|
||||
:title (t :search.find-in-page/next-result))
|
||||
|
||||
(ui/button
|
||||
(ui/icon "x")
|
||||
@@ -91,7 +92,7 @@
|
||||
:intent "link"
|
||||
:small? true
|
||||
:class "text-lg"
|
||||
:title "Close")])
|
||||
:title (t :ui/close))])
|
||||
|
||||
(rum/defc search < rum/reactive
|
||||
[]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
[frontend.components.settings :as settings]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.context.i18n :as i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler :as handler]
|
||||
[frontend.handler.db-based.rtc-flows :as rtc-flows]
|
||||
@@ -44,7 +44,7 @@
|
||||
< {:key-fn #(identity "home-button")}
|
||||
[]
|
||||
(shui/button-ghost-icon :home
|
||||
{:title (t :home)
|
||||
{:title (t :nav/home)
|
||||
:on-click #(do
|
||||
(when (mobile-util/native-iphone?)
|
||||
(state/set-left-sidebar-open! false))
|
||||
@@ -74,7 +74,7 @@
|
||||
{:on-click #(shui/dialog-open!
|
||||
(fn []
|
||||
[:div.p-2.-mb-8
|
||||
[:h1.text-3xl.-mt-2.-ml-2 "Collaborators:"]
|
||||
[:h1.text-3xl.-mt-2.-ml-2 (t :collaboration/members)]
|
||||
(settings/settings-collaboration)])
|
||||
{:id :rtc-collaborators})})
|
||||
|
||||
@@ -98,9 +98,9 @@
|
||||
[{:keys [on-click]}]
|
||||
(ui/with-shortcut :ui/toggle-left-sidebar "bottom"
|
||||
[:button.#left-menu.cp__header-left-menu.button.icon
|
||||
{:title (t :header/toggle-left-sidebar)
|
||||
:on-click on-click}
|
||||
(ui/icon "menu-2" {:size ui/icon-size})]))
|
||||
{:on-click on-click}
|
||||
(ui/icon "menu-2" {:size ui/icon-size})]
|
||||
(t :header/toggle-left-sidebar)))
|
||||
|
||||
(defn bug-report-url []
|
||||
(let [ua (.-userAgent js/navigator)
|
||||
@@ -137,7 +137,7 @@
|
||||
(if favorited?
|
||||
(page-handler/<unfavorite-page! block-id-str)
|
||||
(page-handler/<favorite-page! block-id-str)))}}
|
||||
{:title "Publish page"
|
||||
{:title (t :publish/dialog-title)
|
||||
:options {:on-click #(shui/dialog-open! (fn [] (page-menu/publish-page-dialog page))
|
||||
{:class "w-auto max-w-md"})}}])))
|
||||
page-menu-and-hr (concat page-menu [{:hr true}])
|
||||
@@ -145,41 +145,41 @@
|
||||
items (fn []
|
||||
(->>
|
||||
[(when (state/enable-editing?)
|
||||
{:title (t :settings)
|
||||
{:title (t :nav/settings)
|
||||
:options {:on-click state/open-settings!}
|
||||
:icon (ui/icon "settings")})
|
||||
|
||||
(when config/lsp-enabled?
|
||||
{:title (t :plugins)
|
||||
{:title (t :nav/plugins)
|
||||
:options {:on-click #(plugin-handler/goto-plugins-dashboard!)}
|
||||
:icon (ui/icon "apps")})
|
||||
|
||||
{:title (t :appearance)
|
||||
{:title (t :nav/appearance)
|
||||
:options {:on-click #(state/pub-event! [:ui/toggle-appearance])}
|
||||
:icon (ui/icon "color-swatch")}
|
||||
|
||||
(when (db/get-page common-config/recycle-page-name)
|
||||
{:title "Recycle"
|
||||
{:title (t :storage.recycle/title)
|
||||
:options {:on-click page-handler/open-recycle!}
|
||||
:icon (ui/icon "trash")})
|
||||
|
||||
(when current-repo
|
||||
{:title (t :export-graph)
|
||||
{:title (t :export/graph)
|
||||
:options {:on-click #(shui/dialog-open! export/export)}
|
||||
:icon (ui/icon "database-export")})
|
||||
|
||||
(when (and current-repo (state/enable-editing?))
|
||||
{:title (t :import)
|
||||
{:title (t :import/title)
|
||||
:options {:href (rfe/href :import)}
|
||||
:icon (ui/icon "file-upload")})
|
||||
|
||||
(when config/publishing?
|
||||
{:title (t :toggle-theme)
|
||||
{:title (t :ui/toggle-theme)
|
||||
:options {:on-click #(state/toggle-theme!)}
|
||||
:icon (ui/icon "bulb")})
|
||||
|
||||
(when-not (or config/publishing? login?)
|
||||
{:title (t :login)
|
||||
{:title (t :ui/login)
|
||||
:options {:on-click #(state/pub-event! [:user/login])}
|
||||
:icon (ui/icon "user")})
|
||||
|
||||
@@ -189,57 +189,61 @@
|
||||
[:b.leading-none (user-handler/username)]
|
||||
[:small.opacity-70 (user-handler/email)]
|
||||
[:i.absolute.opacity-0.group-hover:opacity-100.text-red-rx-09
|
||||
{:class "right-1 top-3" :title (t :logout)}
|
||||
{:class "right-1 top-3" :title (t :ui/logout)}
|
||||
(ui/icon "logout")]]
|
||||
:options {:on-click #(user-handler/logout)
|
||||
:class "w-full"}})]
|
||||
(concat page-menu-and-hr)
|
||||
(remove nil?)))]
|
||||
|
||||
(shui/button-ghost-icon :dots
|
||||
{:title (t :header/more)
|
||||
:class "toolbar-dots-btn"
|
||||
:on-pointer-down (fn [^js e]
|
||||
(shui/popup-show! (.-target e)
|
||||
(fn [{:keys [id]}]
|
||||
(for [{:keys [hr item title options icon]} (items)]
|
||||
(let [on-click' (:on-click options)
|
||||
href (:href options)]
|
||||
(if hr
|
||||
(shui/dropdown-menu-separator)
|
||||
(shui/dropdown-menu-item
|
||||
(assoc options
|
||||
:on-click (fn [^js e]
|
||||
(when on-click'
|
||||
(when-not (false? (on-click' e))
|
||||
(shui/popup-hide! id)))))
|
||||
(or item
|
||||
(if href
|
||||
[:a.flex.items-center.w-full
|
||||
{:href href :on-click #(shui/popup-hide! id)
|
||||
:style {:color "inherit"}}
|
||||
[:span.flex.items-center.gap-1.w-full
|
||||
icon [:div title]]]
|
||||
[:span.flex.items-center.gap-1.w-full
|
||||
icon [:div title]])))))))
|
||||
{:align "end"
|
||||
:as-dropdown? true
|
||||
:content-props {:class "w-64"
|
||||
:align-offset -32}}))})))
|
||||
(ui/tooltip
|
||||
(shui/button-ghost-icon
|
||||
:dots {:class "toolbar-dots-btn"
|
||||
:on-pointer-down (fn [^js e]
|
||||
(shui/popup-show! (.-target e)
|
||||
(fn [{:keys [id]}]
|
||||
(for [{:keys [hr item title options icon]} (items)]
|
||||
(let [on-click' (:on-click options)
|
||||
href (:href options)]
|
||||
(if hr
|
||||
(shui/dropdown-menu-separator)
|
||||
(shui/dropdown-menu-item
|
||||
(assoc options
|
||||
:on-click (fn [^js e]
|
||||
(when on-click'
|
||||
(when-not (false? (on-click' e))
|
||||
(shui/popup-hide! id)))))
|
||||
(or item
|
||||
(if href
|
||||
[:a.flex.items-center.w-full
|
||||
{:href href :on-click #(shui/popup-hide! id)
|
||||
:style {:color "inherit"}}
|
||||
[:span.flex.items-center.gap-1.w-full
|
||||
icon [:div title]]]
|
||||
[:span.flex.items-center.gap-1.w-full
|
||||
icon [:div title]])))))))
|
||||
{:align "end"
|
||||
:as-dropdown? true
|
||||
:content-props {:class "w-64"
|
||||
:align-offset -32}}))})
|
||||
(t :header/more)
|
||||
{:trigger-props {:as-child true}})))
|
||||
|
||||
(rum/defc back-and-forward
|
||||
< {:key-fn #(identity "nav-history-buttons")}
|
||||
[]
|
||||
[:div.flex.flex-row
|
||||
(ui/with-shortcut :go/backward "bottom"
|
||||
(shui/button-ghost-icon :arrow-left
|
||||
{:title (t :header/go-back) :on-click #(js/window.history.back)
|
||||
:class "it navigation nav-left"}))
|
||||
(shui/button-ghost-icon
|
||||
:arrow-left {:on-click #(js/window.history.back)
|
||||
:class "it navigation nav-left"})
|
||||
(t :header/go-back))
|
||||
|
||||
(ui/with-shortcut :go/forward "bottom"
|
||||
(shui/button-ghost-icon :arrow-right
|
||||
{:title (t :header/go-forward) :on-click #(js/window.history.forward)
|
||||
:class "it navigation nav-right"}))])
|
||||
(shui/button-ghost-icon
|
||||
:arrow-right {:on-click #(js/window.history.forward)
|
||||
:class "it navigation nav-right"})
|
||||
(t :header/go-forward))])
|
||||
|
||||
(rum/defc updater-tips-new-version
|
||||
[t]
|
||||
@@ -268,7 +272,7 @@
|
||||
|
||||
(when downloaded
|
||||
[:div.cp__header-tips
|
||||
[:p (t :updater/new-version-install)
|
||||
[:p (t :updater/update-ready-to-install)
|
||||
[:a.restart.ml-2
|
||||
{:on-click #(handler/quit-and-install-new-version!)}
|
||||
(svg/reload 16) [:strong (t :updater/quit-and-install)]]]])))
|
||||
@@ -329,13 +333,13 @@
|
||||
:class "block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none"}))
|
||||
(shui/tooltip-content
|
||||
{:onPointerDownOutside (fn [e] (.preventDefault e))}
|
||||
(str "Highlight recent blocks"
|
||||
(when (not= recent-days 0)
|
||||
(str ": " recent-days " days ago")))))))
|
||||
(if (zero? recent-days)
|
||||
(t :header/highlight-recent-blocks)
|
||||
(t :header/highlight-recent-blocks-days-ago recent-days))))))
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:size :sm
|
||||
:title "Quit highlight recent blocks"
|
||||
:title (t :header/quit-highlight-recent-blocks)
|
||||
:class "opacity-50 hover:opacity-100"
|
||||
:on-click (fn [] (state/toggle-highlight-recent-blocks!))}
|
||||
(ui/icon "x" {:size 16}))]))
|
||||
@@ -373,7 +377,7 @@
|
||||
(when (and running? (= repo current-repo))
|
||||
[:div.search-index-progress
|
||||
[ui/loading ""]
|
||||
[:span.search-index-progress__text (str "Indexing " progress' "%")]
|
||||
[:span.search-index-progress__text (t :search/index-progress progress')]
|
||||
[:div.search-index-progress__bar
|
||||
[:div.search-index-progress__bar-fill {:style {:width (str progress' "%")}}]]])))
|
||||
|
||||
@@ -406,19 +410,20 @@
|
||||
(when-not (or (state/home?) custom-home-page?)
|
||||
(ui/with-shortcut :go/backward "bottom"
|
||||
[:button.it.navigation.nav-left.button.icon.opacity-70
|
||||
{:title (t :header/go-back) :on-click #(js/window.history.back)}
|
||||
(ui/icon "chevron-left" {:size 26})]))
|
||||
{:on-click #(js/window.history.back)}
|
||||
(ui/icon "chevron-left" {:size 26})]
|
||||
(t :header/go-back)))
|
||||
;; search button for non-mobile
|
||||
(when current-repo
|
||||
(ui/with-shortcut :go/search "right"
|
||||
[:button.button.icon#search-button
|
||||
{:data-keep-selection true
|
||||
:title (t :header/search)
|
||||
:on-click #(do (when (or (mobile-util/native-android?)
|
||||
(mobile-util/native-iphone?))
|
||||
(state/set-left-sidebar-open! false))
|
||||
(state/pub-event! [:go/search]))}
|
||||
(ui/icon "search" {:size ui/icon-size})])))]]
|
||||
(ui/icon "search" {:size ui/icon-size})]
|
||||
(t :nav/search))))]]
|
||||
|
||||
[:div.r.flex.drag-region.justify-between.items-center.gap-2.overflow-x-hidden.w-full
|
||||
[:div.flex.flex-1
|
||||
@@ -461,7 +466,7 @@
|
||||
|
||||
(when config/publishing?
|
||||
[:a.text-sm.font-medium.button {:href (rfe/href :graph)}
|
||||
(t :graph)])
|
||||
(t :nav/graph)])
|
||||
|
||||
(toolbar-dots-menu {:t t
|
||||
:current-repo current-repo
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[cljs-bean.core :as bean]
|
||||
[clojure.string :as string]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.search :as search]
|
||||
[frontend.storage :as storage]
|
||||
[frontend.ui :as ui]
|
||||
@@ -196,7 +197,9 @@
|
||||
|
||||
(defn- normalize-tabs
|
||||
[tabs default-tab]
|
||||
(let [tabs (or tabs [[:all "All"] [:emoji "Emojis"] [:icon "Icons"]])
|
||||
(let [tabs (or tabs [[:all (t :icon/tab-all)]
|
||||
[:emoji (t :icon/tab-emojis)]
|
||||
[:icon (t :icon/tab-icons)]])
|
||||
default-tab (or default-tab (ffirst tabs) :all)
|
||||
default-tab (if (some #(= (first %) default-tab) tabs)
|
||||
default-tab
|
||||
@@ -211,11 +214,11 @@
|
||||
(filterv #(= :emoji (:type %)) used-items))
|
||||
sections (cond-> []
|
||||
(and show-used? (seq emoji-used-items))
|
||||
(conj {:title "Frequently used"
|
||||
(conj {:title (t :ui/frequently-used)
|
||||
:items emoji-used-items
|
||||
:virtual-list? false})
|
||||
true
|
||||
(conj {:title (util/format "Emojis (%s)" (count emojis*))
|
||||
(conj {:title (t :icon/emojis-count (count emojis*))
|
||||
:items emojis*
|
||||
:virtual-list? true}))]
|
||||
sections))
|
||||
@@ -242,7 +245,7 @@
|
||||
(rum/defc icons-cp < rum/static
|
||||
[icons opts]
|
||||
(pane-section
|
||||
(util/format "Icons (%s)" (count icons))
|
||||
(t :icon/icons-count (count icons))
|
||||
icons
|
||||
opts))
|
||||
|
||||
@@ -254,11 +257,11 @@
|
||||
opts (assoc opts :virtual-list? false)]
|
||||
[:div.all-pane.pb-10
|
||||
(when (count used-items)
|
||||
(pane-section "Frequently used" used-items opts))
|
||||
(pane-section (util/format "Emojis (%s)" (count emojis))
|
||||
(pane-section (t :ui/frequently-used) used-items opts))
|
||||
(pane-section (t :icon/emojis-count (count emojis))
|
||||
emoji-items
|
||||
opts)
|
||||
(pane-section (util/format "Icons (%s)" (count (get-tabler-icons)))
|
||||
(pane-section (t :icon/icons-count (count (get-tabler-icons)))
|
||||
icon-items
|
||||
opts)]))
|
||||
|
||||
@@ -414,7 +417,10 @@
|
||||
[(shui/input
|
||||
{:auto-focus true
|
||||
:ref *input-ref
|
||||
:placeholder (util/format "Search %ss" (string/lower-case (name tab)))
|
||||
:placeholder (case tab
|
||||
:emoji (t :icon/search-emojis)
|
||||
:icon (t :icon/search-icons)
|
||||
(t :icon/search-all))
|
||||
:default-value ""
|
||||
:on-focus #(reset! *select-mode? false)
|
||||
:on-key-down (fn [^js e]
|
||||
@@ -451,7 +457,7 @@
|
||||
(let [matched (concat (:emojis result) (:icons result))]
|
||||
(when (seq matched)
|
||||
(pane-section
|
||||
(util/format "Matched (%s)" (count matched))
|
||||
(t :icon/matched-count (count matched))
|
||||
matched
|
||||
opts)))]
|
||||
[:div.flex.flex-1.flex-col.gap-1
|
||||
@@ -532,4 +538,4 @@
|
||||
(if (vector? icon-value) ; hiccup
|
||||
icon-value
|
||||
(icon icon-value (merge {:color? true} icon-props)))
|
||||
(or empty-label "Empty"))))))
|
||||
(or empty-label (t :ui/empty)))))))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[frontend.components.repo :as repo]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.context.i18n :refer [t t-en]]
|
||||
[frontend.db :as db]
|
||||
[frontend.fs :as fs]
|
||||
[frontend.handler.assets :as assets-handler]
|
||||
@@ -69,7 +69,7 @@
|
||||
[& {:keys [reload?]
|
||||
:or {reload? true}}]
|
||||
(state/pub-event! [:graph/sync-context])
|
||||
(notification/show! "Import finished!" :success)
|
||||
(notification/show! (t :import/file-finished) :success)
|
||||
(shui/dialog-close! :import-indicator)
|
||||
(route-handler/redirect-to-home!)
|
||||
(if util/web-platform?
|
||||
@@ -86,10 +86,10 @@
|
||||
(let [graph-name (string/trim graph-name)]
|
||||
(cond
|
||||
(string/blank? graph-name)
|
||||
(notification/show! "Empty graph name." :error)
|
||||
(notification/show! (t :import/empty-graph-name) :error)
|
||||
|
||||
(repo-handler/graph-already-exists? graph-name)
|
||||
(notification/show! "Please specify another name as another graph with this name already exists!" :error)
|
||||
(notification/show! (t :import/graph-name-conflict) :error)
|
||||
|
||||
:else
|
||||
(let [reader (js/FileReader.)]
|
||||
@@ -108,10 +108,10 @@
|
||||
(let [graph-name (string/trim graph-name)]
|
||||
(cond
|
||||
(string/blank? graph-name)
|
||||
(notification/show! "Empty graph name." :error)
|
||||
(notification/show! (t :import/empty-graph-name) :error)
|
||||
|
||||
(repo-handler/graph-already-exists? graph-name)
|
||||
(notification/show! "Please specify another name as another graph with this name already exists!" :error)
|
||||
(notification/show! (t :import/graph-name-conflict) :error)
|
||||
|
||||
:else
|
||||
(db-import-handler/import-from-sqlite-zip! file graph-name
|
||||
@@ -122,10 +122,10 @@
|
||||
(let [graph-name (string/trim graph-name)]
|
||||
(cond
|
||||
(string/blank? graph-name)
|
||||
(notification/show! "Empty graph name." :error)
|
||||
(notification/show! (t :import/empty-graph-name) :error)
|
||||
|
||||
(repo-handler/graph-already-exists? graph-name)
|
||||
(notification/show! "Please specify another name as another graph with this name already exists!" :error)
|
||||
(notification/show! (t :import/graph-name-conflict) :error)
|
||||
|
||||
:else
|
||||
(do
|
||||
@@ -148,7 +148,7 @@
|
||||
(.readAsText reader file)))))
|
||||
|
||||
:else
|
||||
(notification/show! "Please choose an EDN or a JSON file."
|
||||
(notification/show! (t :import/select-edn-or-json)
|
||||
:error))))
|
||||
|
||||
(rum/defcs set-graph-name-dialog
|
||||
@@ -163,7 +163,7 @@
|
||||
[:div.sm:flex.sm:items-start
|
||||
[:div.mt-3.text-center.sm:mt-0.sm:text-left
|
||||
[:h3#modal-headline.leading-6.font-medium.pb-2
|
||||
"New graph name:"]]]
|
||||
(t :import/new-graph-name)]]]
|
||||
|
||||
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2.mb-4
|
||||
{:auto-focus true
|
||||
@@ -174,7 +174,7 @@
|
||||
(on-submit)))}]
|
||||
|
||||
[:div.mt-5.sm:mt-4.flex
|
||||
(ui/button "Submit"
|
||||
(ui/button (t :ui/submit)
|
||||
{:on-click on-submit})]]))
|
||||
|
||||
(rum/defc import-file-graph-dialog
|
||||
@@ -206,9 +206,9 @@
|
||||
(shui/form-field {:name "graph-name"}
|
||||
(fn [field error]
|
||||
(shui/form-item
|
||||
(shui/form-label "New graph name")
|
||||
(shui/form-label (t :import/new-graph-name))
|
||||
(shui/form-control
|
||||
(shui/input (merge {:placeholder "Graph name"} field)))
|
||||
(shui/input (merge {:placeholder (t :import/graph-name-placeholder)} field)))
|
||||
(when error
|
||||
(shui/form-description
|
||||
[:b.text-red-800 (:message error)])))))
|
||||
@@ -217,7 +217,7 @@
|
||||
(fn [field]
|
||||
(shui/form-item
|
||||
{:class "pt-3 flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"}
|
||||
(shui/form-label "Extract inline code snippets as child blocks")
|
||||
(shui/form-label (t :import/extract-inline-code-snippets))
|
||||
(shui/form-control
|
||||
(shui/checkbox {:checked (:value field)
|
||||
:on-checked-change (:onChange field)})))))
|
||||
@@ -226,7 +226,7 @@
|
||||
(fn [field]
|
||||
(shui/form-item
|
||||
{:class "pt-3 flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"}
|
||||
(shui/form-label "Import all tags")
|
||||
(shui/form-label (t :import/all-tags))
|
||||
(shui/form-control
|
||||
(shui/checkbox {:checked (:value field)
|
||||
:on-checked-change (fn [e]
|
||||
@@ -237,18 +237,18 @@
|
||||
(fn [field _error]
|
||||
(shui/form-item
|
||||
{:class "pt-3"}
|
||||
(shui/form-label "Import specific tags")
|
||||
(shui/form-label (t :import/specific-tags))
|
||||
(shui/form-control
|
||||
(shui/input (merge field
|
||||
{:placeholder "tag 1, tag 2" :disabled convert-all-tags-input})))
|
||||
(shui/form-description "Tags are case insensitive"))))
|
||||
{:placeholder (t :import/tag-classes-placeholder) :disabled convert-all-tags-input})))
|
||||
(shui/form-description (t :import/tags-case-insensitive)))))
|
||||
|
||||
(shui/form-field {:name "remove-inline-tags?"}
|
||||
(fn [field]
|
||||
(shui/form-item
|
||||
{:class "pt-3 flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"}
|
||||
(shui/form-label "Remove inline tags")
|
||||
(shui/form-description "Default behavior for DB graphs")
|
||||
(shui/form-label (t :import/remove-inline-tags))
|
||||
(shui/form-description (t :import/default-db-graph-behavior))
|
||||
(shui/form-control
|
||||
(shui/checkbox {:checked (:value field)
|
||||
:on-checked-change (:onChange field)})))))
|
||||
@@ -257,64 +257,59 @@
|
||||
(fn [field _error]
|
||||
(shui/form-item
|
||||
{:class "pt-3"}
|
||||
(shui/form-label "Import additional tags from property values")
|
||||
(shui/form-label (t :import/property-value-tags))
|
||||
(shui/form-control
|
||||
(shui/input (merge {:placeholder "e.g. type"} field)))
|
||||
(shui/input (merge {:placeholder (t :import/property-classes-placeholder)} field)))
|
||||
(shui/form-description
|
||||
"Properties are case insensitive and separated by commas"))))
|
||||
(t :import/properties-case-insensitive-commas)))))
|
||||
|
||||
(shui/form-field {:name "property-parent-classes"}
|
||||
(fn [field _error]
|
||||
(shui/form-item
|
||||
{:class "pt-3"}
|
||||
(shui/form-label "Import tag parents from property values")
|
||||
(shui/form-label (t :import/property-value-tag-parents))
|
||||
(shui/form-control
|
||||
(shui/input (merge {:placeholder "e.g. parent"} field)))
|
||||
(shui/input (merge {:placeholder (t :import/property-parent-classes-placeholder)} field)))
|
||||
(shui/form-description
|
||||
"Properties are case insensitive and separated by commas"))))
|
||||
(t :import/properties-case-insensitive-commas)))))
|
||||
|
||||
(shui/button {:type "submit" :class "right-0 mt-3"} "Submit")]))])
|
||||
(shui/button {:type "submit" :class "right-0 mt-3"} (t :ui/submit))]))])
|
||||
|
||||
(defn- validate-imported-data
|
||||
[db import-state files]
|
||||
(when-let [org-files (seq (filter #(= "org" (path/file-ext (:path %))) files))]
|
||||
(log/info :org-files (mapv :path org-files))
|
||||
(notification/show! (str "Imported " (count org-files) " org file(s) as markdown. Support for org files will be added later.")
|
||||
(notification/show! (t :import/org-files-imported (count org-files))
|
||||
:info false))
|
||||
(when-let [ignored-files (seq @(:ignored-files import-state))]
|
||||
(notification/show! (str "Import ignored " (count ignored-files) " "
|
||||
(if (= 1 (count ignored-files)) "file" "files")
|
||||
". See the javascript console for more details.")
|
||||
(notification/show! (t :import/ignored-files (count ignored-files))
|
||||
:info false)
|
||||
(log/error :import-ignored-files {:msg (str "Import ignored " (count ignored-files) " file(s)")})
|
||||
(pprint/pprint ignored-files))
|
||||
(when-let [ignored-assets (seq @(:ignored-assets import-state))]
|
||||
(notification/show! (str "Import ignored " (count ignored-assets) " "
|
||||
(if (= 1 (count ignored-assets)) "asset" "assets")
|
||||
". See the javascript console for more details.")
|
||||
(notification/show! (t :import/ignored-assets (count ignored-assets))
|
||||
:info false)
|
||||
(log/error :import-ignored-assets {:msg (str "Import ignored " (count ignored-assets) " asset(s)")})
|
||||
(pprint/pprint ignored-assets))
|
||||
(when-let [ignored-props (seq @(:ignored-properties import-state))]
|
||||
(notification/show!
|
||||
[:.mb-2
|
||||
[:.text-lg.mb-2 (str "Import ignored " (count ignored-props) " "
|
||||
(if (= 1 (count ignored-props)) "property" "properties"))]
|
||||
[:.text-lg.mb-2 (t :import/ignored-properties (count ignored-props))]
|
||||
[:span.text-xs
|
||||
"To fix a property type, change the property value to the correct type and reimport the graph"]
|
||||
(t :import/ignored-properties-fix)]
|
||||
(->> ignored-props
|
||||
(map (fn [{:keys [property value schema location]}]
|
||||
[(str "Property " (pr-str property) " with value " (pr-str value))
|
||||
(if (= property :icon)
|
||||
(if (:page location)
|
||||
(str "Page icons can't be imported. Go to the page " (pr-str (:page location)) " to manually import it.")
|
||||
(str "Block icons can't be imported. Manually import it at the block: " (pr-str (:block location))))
|
||||
(t :import/page-icons-cannot-be-imported (pr-str (:page location)))
|
||||
(t :import/block-icons-cannot-be-imported (pr-str (:block location))))
|
||||
(if (not= (get-in schema [:type :to]) (get-in schema [:type :from]))
|
||||
(str "Property value has type " (get-in schema [:type :to]) " instead of type " (get-in schema [:type :from]))
|
||||
"Property should be imported manually"))]))
|
||||
(t :import/property-type-mismatch (get-in schema [:type :to]) (get-in schema [:type :from]))
|
||||
(t :import/property-import-manually)))]))
|
||||
(map (fn [[k v]]
|
||||
[:dl.my-2.mb-0
|
||||
[:dt.m-0 [:strong k]]
|
||||
[:dt.m-0 [:strong k]]
|
||||
[:dd {:class "text-warning"} v]])))]
|
||||
:warning false))
|
||||
(let [{:keys [errors]} (db-validate/validate-local-db! db {:verbose true})]
|
||||
@@ -322,7 +317,7 @@
|
||||
(do
|
||||
(log/error :import-errors {:msg (str "Import detected " (count errors) " invalid block(s):")})
|
||||
(pprint/pprint errors)
|
||||
(notification/show! (str "Import detected " (count errors) " invalid block(s). These blocks may be buggy when you interact with them. See the javascript console for more.")
|
||||
(notification/show! (t :import/invalid-blocks-detected (count errors))
|
||||
:warning false))
|
||||
(log/info :import-valid {:msg "Valid import!"}))))
|
||||
|
||||
@@ -337,14 +332,11 @@
|
||||
(defn- read-and-copy-asset [repo repo-dir file assets buffer-handler]
|
||||
(let [^js file-object (:file-object file)]
|
||||
(if (assets-handler/exceed-limit-size? file-object)
|
||||
(do
|
||||
(js/console.log (str "Skipped copying asset " (pr-str (:path file)) " because it is larger than the 100M max."))
|
||||
(let [path (pr-str (:path file))]
|
||||
(log/info :import-asset-skipped-too-large {:msg (t-en :import/asset-too-large-warning path)})
|
||||
;; This asset will also be included in the ignored-assets count. Better to be explicit about ignoring
|
||||
;; these so users are aware of this
|
||||
(notification/show!
|
||||
(str "Skipped copying asset " (pr-str (:path file)) " because it is larger than the 100M max.")
|
||||
:info
|
||||
false))
|
||||
(notification/show! (t :import/asset-too-large-warning path) :info false))
|
||||
(p/let [buffer (.arrayBuffer file-object)
|
||||
bytes-array (js/Uint8Array. buffer)
|
||||
checksum (db-asset/<get-file-array-buffer-checksum buffer)
|
||||
@@ -429,7 +421,7 @@
|
||||
(ignored-path? original-graph-name (.-webkitRelativePath (:file-object %))))))]
|
||||
(if-let [config-file (first (filter #(= (:path %) "logseq/config.edn") files))]
|
||||
(import-file-graph files user-inputs config-file)
|
||||
(notification/show! "Import failed as the file 'logseq/config.edn' was not found for a Logseq graph."
|
||||
(notification/show! (t :import/logseq-config-missing)
|
||||
:error)))))]
|
||||
(shui/dialog-open!
|
||||
#(import-file-graph-dialog original-graph-name
|
||||
@@ -439,7 +431,7 @@
|
||||
(repo/invalid-graph-name-warning)
|
||||
|
||||
(repo-handler/graph-already-exists? graph-name)
|
||||
(notification/show! "Please specify another name as another graph with this name already exists!" :error)
|
||||
(notification/show! (t :import/graph-name-conflict) :error)
|
||||
|
||||
:else
|
||||
(import-graph-fn user-inputs)))))))
|
||||
@@ -447,9 +439,9 @@
|
||||
(rum/defc indicator-progress < rum/reactive
|
||||
[]
|
||||
(let [{:keys [total current-idx current-page label]} (state/sub :graph/importing-state)
|
||||
label (or label (t :importing))
|
||||
label (or label (t :import/loading))
|
||||
left-label (if (and current-idx total (= current-idx total))
|
||||
[:div.flex.flex-row.font-bold "Loading ..."]
|
||||
[:div.flex.flex-row.font-bold (t :ui/loading)]
|
||||
[:div.flex.flex-row.font-bold
|
||||
label
|
||||
[:div.hidden.md:flex.flex-row
|
||||
@@ -488,14 +480,14 @@
|
||||
[:article.flex.flex-col.items-center.importer.py-16.px-8
|
||||
(when-not (util/mobile?)
|
||||
[:section.c.text-center
|
||||
[:h1 (t :on-boarding/importing-title)]
|
||||
[:h2 (t :on-boarding/importing-desc)]])
|
||||
[:h1 (t :onboarding.import/title)]
|
||||
[:h2 (t :onboarding.import/desc)]])
|
||||
[:section.d.md:flex.flex-col
|
||||
[:label.action-input.flex.items-center.mx-2.my-2
|
||||
[:span.as-flex-center [:i (svg/logo 28)]]
|
||||
[:span.flex.flex-col
|
||||
[[:strong "SQLite"]
|
||||
[:small (t :on-boarding/importing-sqlite-desc)]]]
|
||||
[[:strong "SQLite"]
|
||||
[:small (t :onboarding.import/sqlite-desc)]]]
|
||||
[:input.absolute.hidden
|
||||
{:id "import-sqlite-db"
|
||||
:type "file"
|
||||
@@ -506,8 +498,8 @@
|
||||
[:label.action-input.flex.items-center.mx-2.my-2
|
||||
[:span.as-flex-center [:i (svg/logo 28)]]
|
||||
[:span.flex.flex-col
|
||||
[[:strong "SQLite + assets (.zip)"]
|
||||
[:small "Import a zip containing db.sqlite and an assets folder"]]]
|
||||
[[:strong (t :import/sqlite-and-assets-title)]
|
||||
[:small (t :import/sqlite-and-assets-desc)]]]
|
||||
[:input.absolute.hidden
|
||||
{:id "import-sqlite-zip"
|
||||
:type "file"
|
||||
@@ -520,8 +512,8 @@
|
||||
[:label.action-input.flex.items-center.mx-2.my-2
|
||||
[:span.as-flex-center [:i (svg/logo 28)]]
|
||||
[:span.flex.flex-col
|
||||
[[:strong "File to DB graph"]
|
||||
[:small "Import a file-based Logseq graph folder into a new DB graph"]]]
|
||||
[[:strong (t :import/file-to-db-title)]
|
||||
[:small (t :import/file-to-db-desc)]]]
|
||||
;; Test form style changes
|
||||
#_[:a.button {:on-click #(import-file-to-db-handler nil {:import-graph-fn js/alert})} "Open"]
|
||||
[:input.absolute.hidden
|
||||
@@ -535,8 +527,8 @@
|
||||
[:label.action-input.flex.items-center.mx-2.my-2
|
||||
[:span.as-flex-center [:i (svg/logo 28)]]
|
||||
[:span.flex.flex-col
|
||||
[[:strong "Debug Transit"]
|
||||
[:small "Import debug transit file into a new DB graph"]]]
|
||||
[[:strong (t :import/debug-transit-title)]
|
||||
[:small (t :import/debug-transit-desc)]]]
|
||||
[:input.absolute.hidden
|
||||
{:id "import-debug-transit"
|
||||
:type "file"
|
||||
@@ -547,8 +539,8 @@
|
||||
[:label.action-input.flex.items-center.mx-2.my-2
|
||||
[:span.as-flex-center [:i (svg/logo 28)]]
|
||||
[:span.flex.flex-col
|
||||
[[:strong "EDN to DB graph"]
|
||||
[:small "Import a DB graph's EDN export into a new DB graph"]]]
|
||||
[[:strong (t :import/db-edn-title)]
|
||||
[:small (t :import/db-edn-desc)]]]
|
||||
[:input.absolute.hidden
|
||||
{:id "import-db-edn"
|
||||
:type "file"
|
||||
@@ -558,4 +550,4 @@
|
||||
|
||||
(when (= "picker" (:from query-params))
|
||||
[:section.e
|
||||
[:a.button {:on-click #(route-handler/redirect-to-home!)} "Skip"]])]))]))
|
||||
[:a.button {:on-click #(route-handler/redirect-to-home!)} (t :ui/skip)]])]))]))
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[frontend.components.icon :as icon]
|
||||
[frontend.components.repo :as repo]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t tt]]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.model :as db-model]
|
||||
@@ -15,12 +15,12 @@
|
||||
[frontend.handler.page :as page-handler]
|
||||
[frontend.handler.recent :as recent-handler]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.handler.ui :as ui-handler]
|
||||
[frontend.state :as state]
|
||||
[frontend.storage :as storage]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.util :as util]
|
||||
[goog.object :as gobj]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.shui.hooks :as hooks]
|
||||
[logseq.shui.ui :as shui]
|
||||
[reitit.frontend.easy :as rfe]
|
||||
@@ -37,13 +37,53 @@
|
||||
default-home
|
||||
(dissoc default-home :page)))))
|
||||
|
||||
(rum/defc page-title-content
|
||||
[page-id display-title tooltip-title untitled? left-sidebar-resized-at]
|
||||
(let [*title-ref (rum/use-ref nil)
|
||||
[truncated? set-truncated?!] (rum/use-state false)
|
||||
sync-truncated! (fn []
|
||||
(if-let [^js el (rum/deref *title-ref)]
|
||||
(set-truncated?! (> (.-scrollWidth el)
|
||||
(+ (.-clientWidth el) 1)))
|
||||
(set-truncated?! false)))
|
||||
title-el [:span.page-title {:ref *title-ref
|
||||
:class (when untitled? "opacity-50")}
|
||||
display-title]]
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(if-let [^js el (rum/deref *title-ref)]
|
||||
(let [observer (js/ResizeObserver. (fn [_] (sync-truncated!)))]
|
||||
(.observe observer el)
|
||||
(sync-truncated!)
|
||||
#(.disconnect observer))
|
||||
(do
|
||||
(set-truncated?! false)
|
||||
nil)))
|
||||
[page-id display-title tooltip-title])
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(let [raf-id (js/requestAnimationFrame sync-truncated!)]
|
||||
#(js/cancelAnimationFrame raf-id)))
|
||||
[left-sidebar-resized-at])
|
||||
(if (and truncated? (not (string/blank? tooltip-title)))
|
||||
(ui/tooltip title-el tooltip-title)
|
||||
title-el)))
|
||||
|
||||
(rum/defc ^:large-vars/cleanup-todo page-name < rum/reactive db-mixins/query
|
||||
[page recent?]
|
||||
(when-let [id (:db/id page)]
|
||||
(let [page (db/sub-block id)
|
||||
left-sidebar-resized-at (rum/react ui-handler/*left-sidebar-resized-at)
|
||||
icon (icon/get-node-icon-cp page {:size 16})
|
||||
title (:block/title page)
|
||||
untitled? (db-model/untitled-page? title)
|
||||
display-title (cond
|
||||
(not (db/page? page))
|
||||
(block/inline-text :markdown (string/replace (apply str (take 64 (:block/title page))) "\n" " "))
|
||||
untitled? (t :ui/untitled)
|
||||
:else (block-handler/block-unique-title page))
|
||||
tooltip-title (or (block-handler/block-unique-title page)
|
||||
(when untitled? (t :ui/untitled)))
|
||||
ctx-icon #(shui/tabler-icon %1 {:class "scale-90 pr-1 opacity-80"})
|
||||
open-in-sidebar #(state/sidebar-add-block!
|
||||
(state/get-current-repo)
|
||||
@@ -58,12 +98,12 @@
|
||||
:on-click #(page-handler/<unfavorite-page! (str (:block/uuid page)))}
|
||||
(ctx-icon "star-off")
|
||||
(t :page/unfavorite)
|
||||
(ui/dropdown-shortcut :command/toggle-favorite)))
|
||||
(ui/dropdown-shortcut :page/toggle-favorite)))
|
||||
(x-menu-item
|
||||
{:key "open in sidebar"
|
||||
:on-click open-in-sidebar}
|
||||
(ctx-icon "layout-sidebar-right")
|
||||
(t :content/open-in-sidebar)
|
||||
(t :sidebar.right/open)
|
||||
(ui/dropdown-shortcut "shift+click"))]))]
|
||||
|
||||
;; TODO: move to standalone component
|
||||
@@ -72,28 +112,19 @@
|
||||
{:on-pointer-down util/stop-propagation
|
||||
:on-pointer-up (fn [_e]
|
||||
(route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?}))}
|
||||
(cond->
|
||||
{:on-click
|
||||
(fn [e]
|
||||
(if (gobj/get e "shiftKey")
|
||||
(open-in-sidebar)
|
||||
(route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?})))
|
||||
:on-context-menu (fn [^js e]
|
||||
(shui/popup-show! e (x-menu-content)
|
||||
{:as-dropdown? true
|
||||
:content-props {:on-click (fn [] (shui/popup-hide!))
|
||||
:class "w-60"}})
|
||||
(util/stop e))}
|
||||
(ldb/object? page)
|
||||
(assoc :title (block-handler/block-unique-title page))))
|
||||
{:on-click
|
||||
(fn [e]
|
||||
(if (gobj/get e "shiftKey")
|
||||
(open-in-sidebar)
|
||||
(route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?})))
|
||||
:on-context-menu (fn [^js e]
|
||||
(shui/popup-show! e (x-menu-content)
|
||||
{:as-dropdown? true
|
||||
:content-props {:on-click (fn [] (shui/popup-hide!))
|
||||
:class "w-60"}})
|
||||
(util/stop e))})
|
||||
[:span.page-icon {:key "page-icon"} icon]
|
||||
[:span.page-title {:key "title"
|
||||
:class (when untitled? "opacity-50")}
|
||||
(cond
|
||||
(not (db/page? page))
|
||||
(block/inline-text :markdown (string/replace (apply str (take 64 (:block/title page))) "\n" " "))
|
||||
untitled? (t :untitled)
|
||||
:else (block-handler/block-unique-title page))]
|
||||
(page-title-content id display-title tooltip-title untitled? left-sidebar-resized-at)
|
||||
|
||||
;; dots trigger
|
||||
(shui/button
|
||||
@@ -132,6 +163,15 @@
|
||||
[:div.sidebar-graphs
|
||||
(repo/graphs-selector)])
|
||||
|
||||
(defn navigation-label-key
|
||||
[nav]
|
||||
(case nav
|
||||
:flashcards :nav/flashcards
|
||||
:all-pages :nav.all-pages/label
|
||||
:graph-view :nav/graph-view
|
||||
:tag/tasks :nav/tasks
|
||||
:tag/assets :nav/assets))
|
||||
|
||||
(rum/defc sidebar-navigations-edit-content
|
||||
[{:keys [_id navs checked-navs set-checked-navs!]}]
|
||||
(let [[local-navs set-local-navs!] (rum/use-state checked-navs)]
|
||||
@@ -141,8 +181,7 @@
|
||||
(set-checked-navs! local-navs))
|
||||
[local-navs])
|
||||
|
||||
(for [nav navs
|
||||
:let [name' (name nav)]]
|
||||
(for [nav navs]
|
||||
(shui/dropdown-menu-checkbox-item
|
||||
{:checked (contains? (set local-navs) nav)
|
||||
:onCheckedChange (fn [v] (set-local-navs!
|
||||
@@ -150,8 +189,7 @@
|
||||
(if v
|
||||
(conj local-navs nav)
|
||||
(filterv #(not= nav %) local-navs)))))}
|
||||
(tt (keyword "left-side-bar" name')
|
||||
(keyword "right-side-bar" name'))))))
|
||||
(t (navigation-label-key nav))))))
|
||||
|
||||
(rum/defc sidebar-content-group < rum/reactive
|
||||
[name {:keys [class count more header-props enter-show-more? collapsable?]} child]
|
||||
@@ -186,7 +224,7 @@
|
||||
[checked-navs])
|
||||
|
||||
(sidebar-content-group
|
||||
[:a.wrap-th [:strong.flex-1 "Navigations"]]
|
||||
[:a.wrap-th [:strong.flex-1 (t :sidebar.left/navigations)]]
|
||||
{:collapsable? false
|
||||
:enter-show-more? true
|
||||
:header-props {:on-click (fn [^js e] (when-let [^js _el (some-> (.-target e) (.closest ".as-edit"))]
|
||||
@@ -218,7 +256,7 @@
|
||||
{:class "journals-nav"
|
||||
:active (and (not srs-open?)
|
||||
(or (= route-name :all-journals) (= route-name :home)))
|
||||
:title (t :left-side-bar/journals)
|
||||
:title (t :nav/journals)
|
||||
:on-click-handler (fn [e]
|
||||
(if (gobj/get e "shiftKey")
|
||||
(route-handler/sidebar-journals!)
|
||||
@@ -233,7 +271,7 @@
|
||||
(let [num (state/sub :srs/cards-due-count)]
|
||||
(sidebar-item
|
||||
{:class "flashcards-nav"
|
||||
:title (t :right-side-bar/flashcards)
|
||||
:title (t :nav/flashcards)
|
||||
:icon "infinity"
|
||||
:shortcut :go/flashcards
|
||||
:active srs-open?
|
||||
@@ -245,7 +283,7 @@
|
||||
(= nav :graph-view)
|
||||
(sidebar-item
|
||||
{:class "graph-view-nav"
|
||||
:title (t :right-side-bar/graph-view)
|
||||
:title (t :nav/graph-view)
|
||||
:href (rfe/href :graph)
|
||||
:active (and (not srs-open?) (= route-name :graph))
|
||||
:icon "hierarchy"
|
||||
@@ -254,7 +292,7 @@
|
||||
(= nav :all-pages)
|
||||
(sidebar-item
|
||||
{:class "all-pages-nav"
|
||||
:title (t :right-side-bar/all-pages)
|
||||
:title (t :nav.all-pages/label)
|
||||
:href (rfe/href :all-pages)
|
||||
:active (and (not srs-open?) (= route-name :all-pages))
|
||||
:icon "files"})
|
||||
@@ -265,8 +303,7 @@
|
||||
(when-let [tag-uuid (and class-ident (:block/uuid (db/entity class-ident)))]
|
||||
(sidebar-item
|
||||
{:class (str "tag-view-nav " name'')
|
||||
:title (tt (keyword "left-side-bar" name'')
|
||||
(keyword "right-side-bar" name''))
|
||||
:title (t (navigation-label-key nav))
|
||||
:href (rfe/href :page {:name tag-uuid})
|
||||
:active (= (str tag-uuid) (get-in route-match [:path-params :name]))
|
||||
:icon "hash"})))))])))
|
||||
@@ -277,7 +314,7 @@
|
||||
favorite-entities (page-handler/get-favorites)]
|
||||
(sidebar-content-group
|
||||
[:a.wrap-th
|
||||
[:strong.flex-1 (t :left-side-bar/nav-favorites)]]
|
||||
[:strong.flex-1 (t :sidebar.left/favorites)]]
|
||||
|
||||
{:class "favorites"
|
||||
:count (count favorite-entities)
|
||||
@@ -301,7 +338,7 @@
|
||||
[]
|
||||
(let [pages (recent-handler/get-recent-pages)]
|
||||
(sidebar-content-group
|
||||
[:a.wrap-th [:strong.flex-1 (t :left-side-bar/nav-recent-pages)]]
|
||||
[:a.wrap-th [:strong.flex-1 (t :sidebar.left/recent-pages)]]
|
||||
|
||||
{:class "recent"
|
||||
:count (count pages)}
|
||||
@@ -309,8 +346,7 @@
|
||||
[:ul.text-sm
|
||||
(for [page pages]
|
||||
[:li.recent-item.select-none.font-medium
|
||||
{:key (str "recent-" (:db/id page))
|
||||
:title (block-handler/block-unique-title page)}
|
||||
{:key (str "recent-" (:db/id page))}
|
||||
(page-name page true)])])))
|
||||
|
||||
(rum/defc ^:large-vars/cleanup-todo sidebar-container
|
||||
@@ -446,7 +482,8 @@
|
||||
(.. el-doc -classList (add "is-resizing-buf"))))
|
||||
(.on "dragend" (fn []
|
||||
(.. sidebar-el -classList (remove "is-resizing"))
|
||||
(.. el-doc -classList (remove "is-resizing-buf"))))))
|
||||
(.. el-doc -classList (remove "is-resizing-buf"))
|
||||
(reset! ui-handler/*left-sidebar-resized-at (js/Date.now))))))
|
||||
#()))
|
||||
[])
|
||||
[:span.left-sidebar-resizer {:ref *el-ref}]))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"Library page"
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.components.select :as components-select]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.search :as search]
|
||||
@@ -47,7 +48,7 @@
|
||||
{:outliner-op :save-block})
|
||||
(set-selected-choices! (disj selected-choices chosen)))))
|
||||
:multiple-choices? true
|
||||
:input-default-placeholder "Add pages"
|
||||
:input-default-placeholder (t :library/add-pages)
|
||||
:show-new-when-not-exact-match? false
|
||||
:on-input set-input!
|
||||
:input-opts {:class "!p-1 !text-sm"}
|
||||
@@ -68,4 +69,4 @@
|
||||
(select-pages library-page)])
|
||||
{:align :start}))}
|
||||
(ui/icon "plus" {:size 16})
|
||||
"Add existing pages to Library")])
|
||||
(t :library/add-existing-pages))])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"Provides table views for class objects and property related objects"
|
||||
(:require [frontend.components.filepicker :as filepicker]
|
||||
[frontend.components.views :as views]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.react :as react]
|
||||
@@ -28,7 +29,7 @@
|
||||
(defn- build-asset-file-column
|
||||
[config]
|
||||
{:id :file
|
||||
:name "File"
|
||||
:name (t :file/label)
|
||||
:type :string
|
||||
:header views/header-cp
|
||||
:cell (fn [_table row _column]
|
||||
@@ -84,7 +85,7 @@
|
||||
(shui/dialog-open!
|
||||
(fn []
|
||||
[:div.flex.flex-col.gap-2
|
||||
[:div.font-medium "Add assets"]
|
||||
[:div.font-medium (t :asset/add-assets)]
|
||||
(filepicker/picker
|
||||
{:on-change (fn [_e files]
|
||||
(p/let [entities (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true)]
|
||||
|
||||
@@ -10,31 +10,31 @@
|
||||
[:span.mr-1 (t :help/forum-community)]
|
||||
(ui/icon "message-circle" {:style {:font-size 20}})]
|
||||
list
|
||||
[{:title (t :help/title-usage)
|
||||
[{:title (t :help/usage-title)
|
||||
:children [[[:a
|
||||
{:on-click (fn [] (state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings))}
|
||||
[:div.flex-row.inline-flex.items-center
|
||||
[:span.mr-1 (t :help/shortcuts)]
|
||||
[:span.mr-1 (t :help.shortcuts/label)]
|
||||
(ui/icon "command" {:style {:font-size 20}})]]]
|
||||
[(t :help/docs) "https://docs.logseq.com/"]
|
||||
[(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
|
||||
["FAQ" "https://docs.logseq.com/#/page/faq"]]}
|
||||
|
||||
{:title (t :help/title-community)
|
||||
{:title (t :help/community-title)
|
||||
:children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
|
||||
[(t :help/blog) "https://blog.logseq.com"]
|
||||
[discourse-with-icon "https://discuss.logseq.com"]]}
|
||||
|
||||
{:title (t :help/title-development)
|
||||
{:title (t :help/development-title)
|
||||
:children [[(t :help/roadmap) "https://discuss.logseq.com/t/logseq-product-roadmap/34267"]
|
||||
[(t :help/bug) "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"]
|
||||
[(t :help/feature) "https://discuss.logseq.com/c/feedback/feature-requests/"]
|
||||
[(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
|
||||
|
||||
{:title (t :help/title-about)
|
||||
{:title (t :help/about-title)
|
||||
:children [[(t :help/about) "https://blog.logseq.com/about/"]]}
|
||||
|
||||
{:title (t :help/title-terms)
|
||||
{:title (t :help/terms-title)
|
||||
:children [[(t :help/privacy) "https://blog.logseq.com/privacy-policy/"]
|
||||
[(t :help/terms) "https://blog.logseq.com/terms/"]]}]]
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
|
||||
[:h1.text-xl
|
||||
(if picker?
|
||||
[:span (t :on-boarding/main-title)]
|
||||
[:span (t :on-boarding/importing-main-title)])]
|
||||
[:span (t :onboarding.setup/title)]
|
||||
[:span (t :onboarding.import-option/title)])]
|
||||
|
||||
[:h2
|
||||
(if picker?
|
||||
(t :on-boarding/main-desc)
|
||||
(t :on-boarding/importing-main-desc))]
|
||||
(t :onboarding.setup/desc)
|
||||
(t :onboarding.import-option/desc))]
|
||||
|
||||
content])])
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
(shui/button {:variant :ghost
|
||||
:class "text-muted-foreground w-full"
|
||||
:on-click (fn [] (route-handler/redirect-to-page! (:block/uuid block)))}
|
||||
"Load more"))
|
||||
(t :ui/load-more)))
|
||||
(when-not more?
|
||||
(when-not hide-add-button?
|
||||
(add-button block config)))])))))
|
||||
@@ -233,7 +233,7 @@
|
||||
(let [query' (assoc query :collapsed? true)]
|
||||
(rum/with-key
|
||||
(ui/catch-error
|
||||
(ui/component-error "Failed default query:" {:content (pr-str query')})
|
||||
(ui/component-error (t :page/default-query-error) {:content (pr-str query')})
|
||||
(query/custom-query (component-block/wrap-query-components
|
||||
{:editor-box editor/box
|
||||
:page page-cp
|
||||
@@ -255,7 +255,7 @@
|
||||
(state/pub-event! [:editor/new-property {:property-key "Icon"
|
||||
:block page
|
||||
:target (.-target e)}]))}
|
||||
"Add icon"))
|
||||
(t :command.editor/add-property-icon)))
|
||||
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
@@ -277,14 +277,14 @@
|
||||
(state/pub-event! [:editor/new-property opts]))))}
|
||||
(cond
|
||||
(ldb/class? page)
|
||||
"Add tag property"
|
||||
(t :class/add-property)
|
||||
(ldb/property? page)
|
||||
"Configure"
|
||||
(t :ui/configure)
|
||||
:else
|
||||
"Set property"))]])
|
||||
(t :property/set-property)))]])
|
||||
|
||||
(rum/defc db-page-title
|
||||
[page {:keys [sidebar? journals? container-id tag-dialog?]}]
|
||||
[page {:keys [sidebar? journals? container-id tag-dialog? display-title]}]
|
||||
(let [with-actions? (not config/publishing?)]
|
||||
[:div.ls-page-title.flex.flex-1.w-full.content.items-start.title
|
||||
{:class "title"
|
||||
@@ -305,6 +305,7 @@
|
||||
:hide-title? sidebar?
|
||||
:sidebar? sidebar?
|
||||
:tag-dialog? tag-dialog?
|
||||
:display-title display-title
|
||||
:hide-children? true
|
||||
:container-id container-id
|
||||
:show-tag-and-property-classes? true
|
||||
@@ -392,12 +393,12 @@
|
||||
(shui/tabs-trigger
|
||||
{:value "tag"
|
||||
:class "py-1 text-xs"}
|
||||
"Tagged nodes"))
|
||||
(t :class/tagged-nodes)))
|
||||
(when property?
|
||||
(shui/tabs-trigger
|
||||
{:value "property"
|
||||
:class "py-1 text-xs"}
|
||||
"Nodes with property"))
|
||||
(t :property/nodes-with-property)))
|
||||
(when property?
|
||||
(db-page/configure-property page)))])
|
||||
|
||||
@@ -422,7 +423,7 @@
|
||||
:size :sm
|
||||
:class "px-1 text-muted-foreground"
|
||||
:on-click #(set-collapsed! (not collapsed?))}
|
||||
[:span.text-xs (str (if collapsed? "Open" "Hide")) " properties"])]
|
||||
[:span.text-xs (t (if collapsed? :page/open-properties :page/hide-properties))])]
|
||||
|
||||
(when-not collapsed?
|
||||
[:<>
|
||||
@@ -451,11 +452,11 @@
|
||||
recycle-page? (and (ldb/page? page)
|
||||
(= title common-config/recycle-page-name))
|
||||
fmt-journal? (boolean (date/journal-title->int title))
|
||||
today? (and
|
||||
journal?
|
||||
(= title (date/journal-name)))
|
||||
today? (model/today-journal-page? page)
|
||||
home? (= :home (state/get-current-route))
|
||||
recycled? (ldb/recycled? page)
|
||||
page-display-title (when (ldb/page? page)
|
||||
(route-handler/built-in-page-title (:block/title page)))
|
||||
show-tabs? (and (or class-page? (ldb/property? page)) (not tag-dialog?))
|
||||
blocks-ready? (or journals?
|
||||
(= page-id @linked-refs-blocks-ready-page-id))
|
||||
@@ -469,7 +470,7 @@
|
||||
(if recycled?
|
||||
[:div.flex-1.page.relative.cp__page-inner-wrap
|
||||
[:div.relative.grid.gap-4.sm:gap-8.page-inner.mb-16
|
||||
[:div.opacity-75 "Node has been moved to Recycle"]]]
|
||||
[:div.opacity-75 (t :page/moved-to-recycle)]]]
|
||||
[:div.flex-1.page.relative.cp__page-inner-wrap
|
||||
(merge (if (seq (:block/tags page))
|
||||
(let [page-names (map :block/title (:block/tags page))]
|
||||
@@ -490,6 +491,7 @@
|
||||
{:sidebar? sidebar?
|
||||
:journals? journals?
|
||||
:container-id (:container-id state)
|
||||
:display-title page-display-title
|
||||
:tag-dialog? tag-dialog?}))
|
||||
(lsp-pagebar-slot)])
|
||||
|
||||
@@ -551,7 +553,7 @@
|
||||
class-page? property-page?)
|
||||
[:div.fade-in.delay {:key "page-unlinked-references"}
|
||||
(reference/unlinked-references page {:sidebar? sidebar?})])])]))
|
||||
[:div.opacity-75 "Page not found"])))
|
||||
[:div.opacity-75 (t :page/not-found)])))
|
||||
|
||||
(rum/defcs page-aux < rum/reactive
|
||||
{:init (fn [state]
|
||||
@@ -653,8 +655,8 @@
|
||||
[state]
|
||||
(let [*simulation-paused? pixi/*simulation-paused?]
|
||||
[:div.flex.flex-col.mb-2
|
||||
[:p {:title "Pause simulation"}
|
||||
"Pause simulation"]
|
||||
[:p {:title (t :graph/pause-simulation)}
|
||||
(t :graph/pause-simulation)]
|
||||
(ui/toggle
|
||||
(rum/react *simulation-paused?)
|
||||
(fn []
|
||||
@@ -697,19 +699,14 @@
|
||||
[:div.shadow-xl.rounded-sm
|
||||
[:ul
|
||||
(graph-filter-section
|
||||
[:span.font-medium "Nodes"]
|
||||
[:span.font-medium (t :graph/nodes)]
|
||||
(fn [open?]
|
||||
(filter-expand-area
|
||||
open?
|
||||
[:div
|
||||
[:p.text-sm.opacity-70.px-4
|
||||
(let [c1 (count (:nodes graph))
|
||||
s1 (if (> c1 1) "s" "")
|
||||
;; c2 (count (:links graph))
|
||||
;; s2 (if (> c2 1) "s" "")
|
||||
]
|
||||
;; (util/format "%d page%s, %d link%s" c1 s1 c2 s2)
|
||||
(util/format "%d page%s" c1 s1))]
|
||||
(let [c1 (count (:nodes graph))]
|
||||
(t :graph/page-count c1))]
|
||||
[:div.p-6
|
||||
;; [:div.flex.items-center.justify-between.mb-2
|
||||
;; [:span "Layout"]
|
||||
@@ -725,7 +722,7 @@
|
||||
;; (set-setting! :layout value))
|
||||
;; {:class "graph-layout"})]
|
||||
[:div.flex.items-center.justify-between.mb-2
|
||||
[:span (t :settings-page/enable-journals)]
|
||||
[:span (t :settings.features/enable-journals)]
|
||||
;; FIXME: why it's not aligned well?
|
||||
[:div.mt-1
|
||||
(ui/toggle journal?
|
||||
@@ -735,7 +732,7 @@
|
||||
(set-setting! :journal? value)))
|
||||
true)]]
|
||||
[:div.flex.items-center.justify-between.mb-2
|
||||
[:span "Orphan pages"]
|
||||
[:span (t :graph/orphan-pages)]
|
||||
[:div.mt-1
|
||||
(ui/toggle orphan-pages?
|
||||
(fn []
|
||||
@@ -744,7 +741,7 @@
|
||||
(set-setting! :orphan-pages? value)))
|
||||
true)]]
|
||||
[:div.flex.items-center.justify-between.mb-2
|
||||
[:span "Built-in pages"]
|
||||
[:span (t :graph/built-in-pages)]
|
||||
[:div.mt-1
|
||||
(ui/toggle builtin-pages?
|
||||
(fn []
|
||||
@@ -753,7 +750,7 @@
|
||||
(set-setting! :builtin-pages? value)))
|
||||
true)]]
|
||||
[:div.flex.items-center.justify-between.mb-2
|
||||
[:span "Excluded pages"]
|
||||
[:span (t :graph/excluded-pages)]
|
||||
[:div.mt-1
|
||||
(ui/toggle excluded-pages?
|
||||
(fn []
|
||||
@@ -763,7 +760,7 @@
|
||||
true)]]
|
||||
|
||||
[:div.flex.flex-col.mb-2
|
||||
[:p "Created before"]
|
||||
[:p (t :graph/created-before)]
|
||||
(when created-at-filter
|
||||
[:div (.toDateString (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))])
|
||||
|
||||
@@ -781,8 +778,8 @@
|
||||
|
||||
(when (seq focus-nodes)
|
||||
[:div.flex.flex-col.mb-2
|
||||
[:p {:title "N hops from selected nodes"}
|
||||
"N hops from selected nodes"]
|
||||
[:p {:title (t :graph/n-hops-from-selected-nodes)}
|
||||
(t :graph/n-hops-from-selected-nodes)]
|
||||
(ui/tooltip
|
||||
(ui/slider (or n-hops 10)
|
||||
{:min 1
|
||||
@@ -797,10 +794,10 @@
|
||||
(reset! *created-at-filter nil)
|
||||
(set-setting! :created-at-filter nil)
|
||||
(state/clear-search-filters!))}
|
||||
"Reset Graph"]]]))
|
||||
(t :graph/reset)]]]))
|
||||
{})
|
||||
(graph-filter-section
|
||||
[:span.font-medium "Search"]
|
||||
[:span.font-medium (t :graph/search)]
|
||||
(fn [open?]
|
||||
(filter-expand-area
|
||||
open?
|
||||
@@ -814,26 +811,25 @@
|
||||
svg/close]])
|
||||
|
||||
[:a.opacity-70.opacity-100 {:on-click state/clear-search-filters!}
|
||||
"Clear All"]]
|
||||
(t :notification/clear-all)]]
|
||||
[:a.opacity-70.opacity-100 {:on-click #(route-handler/go-to-search! :graph)}
|
||||
"Click to search"])]))
|
||||
(t :graph/click-to-search)])]))
|
||||
{:search-filters search-graph-filters})
|
||||
(graph-filter-section
|
||||
[:span.font-medium "Forces"]
|
||||
[:span.font-medium (t :graph/forces)]
|
||||
(fn [open?]
|
||||
(filter-expand-area
|
||||
open?
|
||||
[:div
|
||||
[:p.text-sm.opacity-70.px-4
|
||||
(let [c2 (count (:links graph))
|
||||
s2 (if (> c2 1) "s" "")]
|
||||
(util/format "%d link%s" c2 s2))]
|
||||
open?
|
||||
[:div
|
||||
[:p.text-sm.opacity-70.px-4
|
||||
(let [c2 (count (:links graph))]
|
||||
(t :graph/link-count c2))]
|
||||
[:div.p-6
|
||||
(simulation-switch)
|
||||
|
||||
[:div.flex.flex-col.mb-2
|
||||
[:p {:title "Link Distance"}
|
||||
"Link Distance"]
|
||||
[:p {:title (t :graph/link-distance)}
|
||||
(t :graph/link-distance)]
|
||||
(ui/tooltip
|
||||
(ui/slider (/ link-dist 10)
|
||||
{:min 1 ;; 10
|
||||
@@ -844,8 +840,8 @@
|
||||
[:div link-dist])]
|
||||
|
||||
[:div.flex.flex-col.mb-2
|
||||
[:p {:title "Charge Strength"}
|
||||
"Charge Strength"]
|
||||
[:p {:title (t :graph/charge-strength)}
|
||||
(t :graph/charge-strength)]
|
||||
(ui/tooltip
|
||||
(ui/slider (/ charge-strength 100)
|
||||
{:min -10 ;;-1000
|
||||
@@ -856,8 +852,8 @@
|
||||
[:div charge-strength])]
|
||||
|
||||
[:div.flex.flex-col.mb-2
|
||||
[:p {:title "Charge Range"}
|
||||
"Charge Range"]
|
||||
[:p {:title (t :graph/charge-range)}
|
||||
(t :graph/charge-range)]
|
||||
(ui/tooltip
|
||||
(ui/slider (/ charge-range 100)
|
||||
{:min 5 ;;500
|
||||
@@ -873,17 +869,17 @@
|
||||
(reset! *link-dist 70)
|
||||
(reset! *charge-strength -600)
|
||||
(reset! *charge-range 600))}
|
||||
"Reset Forces"]]]))
|
||||
(t :graph/reset-forces)]]]))
|
||||
{})
|
||||
(graph-filter-section
|
||||
[:span.font-medium "Export"]
|
||||
[:span.font-medium (t :ui/export)]
|
||||
(fn [open?]
|
||||
(filter-expand-area
|
||||
open?
|
||||
(when-let [canvas (js/document.querySelector "#global-graph canvas")]
|
||||
[:div.p-6
|
||||
;; We'll get an empty image if we don't wrap this in a requestAnimationFrame
|
||||
[:div [:a {:on-click #(.requestAnimationFrame js/window (fn [] (utils/canvasToImage canvas "graph" "png")))} "as PNG"]]])))
|
||||
[:div [:a {:on-click #(.requestAnimationFrame js/window (fn [] (utils/canvasToImage canvas "graph" "png")))} (t :graph/as-png)]]])))
|
||||
{:search-filters search-graph-filters})]]]]))
|
||||
|
||||
(defonce last-node-position (atom nil))
|
||||
@@ -984,7 +980,7 @@
|
||||
(let [show-journals-in-page-graph? (rum/react *show-journals-in-page-graph?)]
|
||||
[:div.sidebar-item.flex-col
|
||||
[:div.flex.items-center.justify-between.mb-0
|
||||
[:span (t :right-side-bar/show-journals)]
|
||||
[:span (t :graph.page/show-journals)]
|
||||
[:div.mt-1
|
||||
(ui/toggle show-journals-in-page-graph? ;my-val;
|
||||
(fn []
|
||||
@@ -1018,7 +1014,7 @@
|
||||
(let [current-page (or
|
||||
(and (= :page (state/sub [:route-match :data :name]))
|
||||
(state/sub [:route-match :path-params :name]))
|
||||
(date/today))
|
||||
(model/get-today-journal-title))
|
||||
theme (:ui/theme @state/state)
|
||||
show-journals-in-page-graph (rum/react *show-journals-in-page-graph?)
|
||||
page-entity (db/get-page current-page)]
|
||||
@@ -1038,7 +1034,7 @@
|
||||
(ui/icon "alert-triangle")]]
|
||||
[:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
|
||||
[:h3#modal-headline.text-lg.leading-6.font-medium
|
||||
(t :page/batch-delete-confirmation)]]]
|
||||
(t :page.delete/batch-confirm-title)]]]
|
||||
|
||||
[:ol.p-2.pt-4
|
||||
(for [page-item pages]
|
||||
@@ -1046,16 +1042,16 @@
|
||||
[:a {:href (rfe/href :page {:name (:block/uuid page-item)})}
|
||||
(component-block/page-cp {} page-item)]])]
|
||||
|
||||
[:p.px-2.opacity-50 [:small (str "Total: " (count pages))]]
|
||||
[:p.px-2.opacity-50 [:small (t :page.delete/total (count pages))]]
|
||||
|
||||
[:div.pt-6.flex.justify-end.gap-4
|
||||
(ui/button
|
||||
(t :cancel)
|
||||
(t :ui/cancel)
|
||||
:variant :outline
|
||||
:on-click close)
|
||||
|
||||
(ui/button
|
||||
(t :yes)
|
||||
(t :ui/yes)
|
||||
:on-click (fn []
|
||||
(close)
|
||||
(let [failed-pages (atom [])]
|
||||
@@ -1066,7 +1062,7 @@
|
||||
(swap! failed-pages conj (:block/name page)))}))
|
||||
pages))]
|
||||
(if (seq @failed-pages)
|
||||
(notification/show! (t :all-pages/failed-to-delete-pages (string/join ", " (map pr-str @failed-pages)))
|
||||
(notification/show! (t :page.delete/warning (string/join ", " (map pr-str @failed-pages)))
|
||||
:warning false)
|
||||
(notification/show! (t :tips/all-done) :success))))
|
||||
(notification/show! (t :ui/all-done) :success))))
|
||||
(js/setTimeout #(refresh-fn) 200)))]]))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[frontend.handler.page :as page-handler]
|
||||
[frontend.handler.publish :as publish-handler]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[frontend.modules.shortcut.data-helper :as shortcut-dh]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[logseq.db :as ldb]
|
||||
@@ -33,11 +34,11 @@
|
||||
{:on-submit (fn [e]
|
||||
(.preventDefault e)
|
||||
(submit!))}
|
||||
[:div.text-lg.font-medium "Publish page"]
|
||||
[:div.text-lg.font-medium (t :publish/dialog-title)]
|
||||
[:div.text-sm.opacity-70
|
||||
"Optionally protect this page with a password. Leave empty for public access."]
|
||||
(t :publish/dialog-desc)]
|
||||
(shui/toggle-password
|
||||
{:placeholder "Optional password"
|
||||
{:placeholder (t :publish/password-optional-placeholder)
|
||||
:value password
|
||||
:on-change (fn [e]
|
||||
(set-password! (util/evalue e)))})
|
||||
@@ -46,20 +47,20 @@
|
||||
{:variant "ghost"
|
||||
:type "button"
|
||||
:on-click #(shui/dialog-close!)}
|
||||
"Cancel")
|
||||
(t :ui/cancel))
|
||||
(shui/button
|
||||
{:type "submit"
|
||||
:auto-focus true
|
||||
:disabled publishing?}
|
||||
(if publishing?
|
||||
"Publishing..."
|
||||
"Publish"))]]))
|
||||
(t :publish/publishing)
|
||||
(t :publish/action)))]]))
|
||||
|
||||
(defn- delete-page!
|
||||
[page]
|
||||
(page-handler/<delete! (:block/uuid page)
|
||||
(fn []
|
||||
(notification/show! (str "Page " (:block/title page) " was deleted successfully!")
|
||||
(notification/show! (t :page.delete/success (:block/title page))
|
||||
:success))
|
||||
{:error-handler (fn [{:keys [msg]}]
|
||||
(notification/show! msg :warning))}))
|
||||
@@ -72,10 +73,12 @@
|
||||
[:span.top-1.relative
|
||||
(shui/tabler-icon "alert-triangle")]
|
||||
(if (or (ldb/class? page) (ldb/property? page))
|
||||
(t :page/permanently-delete-confirmation)
|
||||
(t :page/db-delete-confirmation))]
|
||||
(t :page.delete/permanent-confirm-title)
|
||||
(t :page.delete/confirm-title))]
|
||||
:content [:p.opacity-60 (str "- " (:block/title page))]
|
||||
:outside-cancel? true})
|
||||
:outside-cancel? true
|
||||
:cancel-label (t :ui/cancel)
|
||||
:ok-label (t :ui/confirm)})
|
||||
(p/then #(delete-page! page))
|
||||
(p/catch #()))))
|
||||
|
||||
@@ -104,7 +107,7 @@
|
||||
|
||||
(when (or (util/electron?)
|
||||
(mobile-util/native-platform?))
|
||||
{:title (t :page/copy-page-url)
|
||||
{:title (t :page/copy-url)
|
||||
:options {:on-click #(page-handler/copy-page-url (:block/uuid page))}})
|
||||
|
||||
(when-not (or contents?
|
||||
@@ -114,14 +117,14 @@
|
||||
:options {:on-click #(delete-page-confirm! page)}})
|
||||
|
||||
(when page
|
||||
{:title (t :export-page)
|
||||
{:title (t :export/page)
|
||||
:options {:on-click #(shui/dialog-open!
|
||||
(fn []
|
||||
(export/export-blocks [(:block/uuid page)] {:export-type :page}))
|
||||
{:class "w-auto md:max-w-4xl max-h-[80vh] overflow-y-auto"})}})
|
||||
|
||||
(when (and page (not config/publishing?))
|
||||
{:title "Publish page"
|
||||
{:title (t :publish/dialog-title)
|
||||
:options {:on-click #(shui/dialog-open! (fn [] (publish-page-dialog page))
|
||||
{:class "w-auto max-w-md"})}})
|
||||
|
||||
@@ -145,12 +148,12 @@
|
||||
(db-page-handler/convert-page-to-tag! page))}})
|
||||
|
||||
(when (and (ldb/class? page) (not (:logseq.property/built-in? page)))
|
||||
{:title (t :page/convert-tag-to-page)
|
||||
{:title (t :page.convert/tag-to-page-action)
|
||||
:options {:on-click (fn []
|
||||
(db-page-handler/convert-tag-to-page! page))}})
|
||||
|
||||
(when developer-mode?
|
||||
{:title (t :dev/show-page-data)
|
||||
{:title (shortcut-dh/shortcut-desc-by-id :dev/show-page-data)
|
||||
:options {:on-click (fn []
|
||||
(dev-common-handler/show-entity-data (:db/id page)))}})]
|
||||
(flatten)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[frontend.components.plugins-settings :as plugins-settings]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.context.i18n :refer [interpolate-rich-text interpolate-rich-text-node t]]
|
||||
[frontend.handler.common.plugin :as plugin-common-handler]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
@@ -55,10 +55,13 @@
|
||||
(rum/local 0 ::cursor)
|
||||
(rum/local 0 ::total)
|
||||
{:did-mount (fn [state]
|
||||
(let [*themes (::themes state)
|
||||
(let [*themes (::themes state)
|
||||
*cursor (::cursor state)
|
||||
*total (::total state)
|
||||
mode (state/sub :ui/theme)
|
||||
mode-title (t (case mode
|
||||
"dark" :settings.general/theme-dark
|
||||
:settings.general/theme-light))
|
||||
all-themes (state/sub :plugin/installed-themes)
|
||||
themes (->> all-themes
|
||||
(filter #(= (:mode %) mode))
|
||||
@@ -66,19 +69,19 @@
|
||||
no-mode-themes (->> all-themes
|
||||
(filter #(= (:mode %) nil))
|
||||
(sort-by #(:name %))
|
||||
(map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) "light & dark themes" nil)))))
|
||||
(map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) (t :plugin.themes/light-and-dark) nil)))))
|
||||
selected (state/sub :plugin/selected-theme)
|
||||
themes (map-indexed (fn [idx opt]
|
||||
(let [selected? (= (:url opt) selected)]
|
||||
(when selected? (reset! *cursor (+ idx 1)))
|
||||
(assoc opt :mode mode :selected selected?))) (concat themes no-mode-themes))
|
||||
themes (cons {:name (string/join " " ["Default" (string/capitalize mode) "Theme"])
|
||||
themes (cons {:name (t :plugin.themes/default-name (string/capitalize mode-title))
|
||||
:url nil
|
||||
:description (string/join " " ["Logseq default" mode "theme."])
|
||||
:description (t :plugin.themes/default-desc mode-title)
|
||||
:mode mode
|
||||
:selected (nil? selected)
|
||||
:group-first true
|
||||
:group-desc (str mode " themes")} themes)]
|
||||
:group-desc (t :plugin.themes/group mode-title)} themes)]
|
||||
(reset! *themes themes)
|
||||
(reset! *total (count themes))
|
||||
state))}
|
||||
@@ -108,7 +111,7 @@
|
||||
*themes (::themes state)]
|
||||
[:div.cp__themes-installed
|
||||
{:tab-index -1}
|
||||
[:h1.mb-4.text-2xl.p-1 (t :themes)]
|
||||
[:h1.mb-4.text-2xl.p-1 (t :nav/themes)]
|
||||
(map-indexed
|
||||
(fn [idx opt]
|
||||
(let [current-selected? (:selected opt)
|
||||
@@ -141,9 +144,9 @@
|
||||
(fn [^js e]
|
||||
(case (keyword (aget e "name"))
|
||||
:IllegalPluginPackageError
|
||||
(plugin-handler/show-illegal-plugin-package-notification! e)
|
||||
(plugin-handler/show-illegal-plugin-package-notification! e)
|
||||
:ExistedImportedPluginPackageError
|
||||
(notification/show! (str "Existed plugin package (" (.-message e) ").") :error)
|
||||
(notification/show! (t :plugin/existed-package (.-message e)) :error)
|
||||
:default)
|
||||
(plugin-handler/reset-unpacked-state))
|
||||
reg-handle #(plugin-handler/reset-unpacked-state)]
|
||||
@@ -158,7 +161,7 @@
|
||||
[unpacked-pkg-path])
|
||||
|
||||
(when unpacked-pkg-path
|
||||
[:strong.inline-flex.px-3 "Loading ..."]))
|
||||
[:strong.inline-flex.px-3 (t :ui/loading)]))
|
||||
|
||||
(rum/defc category-tabs
|
||||
[t total-nums category on-action]
|
||||
@@ -167,14 +170,14 @@
|
||||
(ui/button
|
||||
[:span.flex.items-center
|
||||
(ui/icon "puzzle")
|
||||
(t :plugins) (when (vector? total-nums) (str " (" (first total-nums) ")"))]
|
||||
(t :nav/plugins) (when (vector? total-nums) (str " (" (first total-nums) ")"))]
|
||||
:intent "link"
|
||||
:on-click #(on-action :plugins)
|
||||
:class (if (= category :plugins) "active" ""))
|
||||
(ui/button
|
||||
[:span.flex.items-center
|
||||
(ui/icon "palette")
|
||||
(t :themes) (when (vector? total-nums) (str " (" (last total-nums) ")"))]
|
||||
(t :nav/themes) (when (vector? total-nums) (str " (" (last total-nums) ")"))]
|
||||
:intent "link"
|
||||
:on-click #(on-action :themes)
|
||||
:class (if (= category :themes) "active" ""))])
|
||||
@@ -260,7 +263,9 @@
|
||||
[:li {:on-click #(plugin-handler/open-report-modal! id name)} (t :plugin/report-security)]
|
||||
[:li {:on-click
|
||||
#(-> (shui/dialog-confirm!
|
||||
[:b (t :plugin/delete-alert name)])
|
||||
[:b (t :plugin/delete-alert name)]
|
||||
{:cancel-label (t :ui/cancel)
|
||||
:ok-label (t :ui/confirm)})
|
||||
(p/then (fn []
|
||||
(plugin-common-handler/unregister-plugin id)
|
||||
|
||||
@@ -315,7 +320,7 @@
|
||||
disabled? market? *search-key has-other-pending?
|
||||
installing-or-updating? installed? stat coming-update]
|
||||
|
||||
(let [name (or title name "Untitled")
|
||||
(let [name (or title name (t :ui/untitled))
|
||||
web? (not (nil? webPkg))
|
||||
unpacked? (and (not web?) (not iir))
|
||||
new-version (state/coming-update-new-version? coming-update)]
|
||||
@@ -356,18 +361,18 @@
|
||||
(reset! *search-key (str "@" author))
|
||||
(.select el))} author]
|
||||
[:small {:on-click #(do
|
||||
(notification/show! "Copied!" :success)
|
||||
(notification/show! (t :notification/copied) :success)
|
||||
(util/copy-to-clipboard! id))}
|
||||
(str "ID: " id)]]]
|
||||
|
||||
;; Github repo
|
||||
;; GitHub repo
|
||||
[:div.flag.is-top.flex.items-center.space-x-2
|
||||
(cond
|
||||
(false? (:supportsDB item))
|
||||
[:a.flex.cursor-help {:title "Not supports DB graph"}
|
||||
[:a.flex.cursor-help {:title (t :plugin/does-not-support-db)}
|
||||
(shui/tabler-icon "database-off" {:size 17})]
|
||||
(true? (:supportsDB item))
|
||||
[:a.flex.cursor-help {:title "Supports DB graph"}
|
||||
[:a.flex.cursor-help {:title (t :plugin/supports-db)}
|
||||
(shui/tabler-icon "database-heart" {:size 17})])
|
||||
(when repo
|
||||
[:a.flex {:target "_blank"
|
||||
@@ -425,19 +430,19 @@
|
||||
*test-input (rum/create-ref)
|
||||
disabled? (or (= (:type opts) "system") (= (:type opts) "direct"))]
|
||||
[:div.cp__settings-network-proxy-cnt
|
||||
[:h1.mb-2.text-2xl.font-bold (t :settings-page/network-proxy)]
|
||||
[:h1.mb-2.text-2xl.font-bold (t :settings.advanced/network-proxy)]
|
||||
[:div.p-2
|
||||
[:p [:label [:strong (t :type)]
|
||||
(ui/select [{:label "System" :value "system" :selected (= type "system")}
|
||||
{:label "Direct" :value "direct" :selected (= type "direct")}
|
||||
{:label "HTTP" :value "http" :selected (= type "http")}
|
||||
{:label "SOCKS5" :value "socks5" :selected (= type "socks5")}]
|
||||
[:p [:label [:strong (t :ui/type)]
|
||||
(ui/select [{:label (t :plugin.proxy/system) :value "system" :selected (= type "system")}
|
||||
{:label (t :plugin.proxy/direct) :value "direct" :selected (= type "direct")}
|
||||
{:label "HTTP" :value "http" :selected (= type "http")}
|
||||
{:label "SOCKS5" :value "socks5" :selected (= type "socks5")}]
|
||||
(fn [_e value]
|
||||
(set-opts! (assoc opts :type value :protocol value))))]]
|
||||
[:p.flex
|
||||
[:label.pr-4
|
||||
{:class (if disabled? "opacity-50" nil)}
|
||||
[:strong (t :host)]
|
||||
[:strong (t :ui/host)]
|
||||
[:input.form-input.is-small
|
||||
{:value (:host opts)
|
||||
:disabled disabled?
|
||||
@@ -446,7 +451,7 @@
|
||||
|
||||
[:label
|
||||
{:class (if disabled? "opacity-50" nil)}
|
||||
[:strong (t :port)]
|
||||
[:strong (t :ui/port)]
|
||||
[:input.form-input.is-small
|
||||
{:value (:port opts) :type "number" :min 1 :max 65535
|
||||
:disabled disabled?
|
||||
@@ -471,7 +476,7 @@
|
||||
[:option "https://s3.amazonaws.com"]
|
||||
[:option "https://clients3.google.com/generate_204"]]]
|
||||
|
||||
(ui/button (if testing? (ui/loading "Testing") "Test URL")
|
||||
(ui/button (if testing? (ui/loading (t :plugin.proxy/testing)) (t :plugin.proxy/test-url))
|
||||
:intent "logseq"
|
||||
:on-click #(let [val (util/trim-safe (.-value (rum/deref *test-input)))]
|
||||
(when (and (not testing?) (not (string/blank? val)))
|
||||
@@ -480,13 +485,13 @@
|
||||
(js->clj result :keywordize-keys true))
|
||||
(p/then (fn [{:keys [code response-ms]}]
|
||||
(notification/clear! :proxy-net-check)
|
||||
(notification/show! (str "Success! Status " code " in " response-ms "ms.") :success)))
|
||||
(notification/show! (t :plugin/proxy-check-success code response-ms) :success)))
|
||||
(p/catch (fn [e]
|
||||
(notification/show! (str e) :error false :proxy-net-check)))
|
||||
(p/finally (fn [] (set-testing?! false)))))))]
|
||||
|
||||
[:p.pt-2
|
||||
(ui/button (t :save)
|
||||
(ui/button (t :ui/save)
|
||||
:on-click (fn []
|
||||
(p/let [_ (ipc/ipc :setProxy opts)]
|
||||
(state/set-state! [:electron/user-cfgs :settings/agent] opts))))]]]))
|
||||
@@ -498,7 +503,7 @@
|
||||
handle-submit! (fn []
|
||||
(set-pending? true)
|
||||
(-> (plugin-handler/load-plugin-from-web-url! url)
|
||||
(p/then #(do (notification/show! "New plugin registered!" :success)
|
||||
(p/then #(do (notification/show! (t :plugin/new-registered) :success)
|
||||
(shui/dialog-close!)))
|
||||
(p/catch #(notification/show! (str %) :error))
|
||||
(p/finally
|
||||
@@ -512,13 +517,13 @@
|
||||
:auto-focus true})
|
||||
[:span.text-gray-10
|
||||
(shui/tabler-icon "info-circle" {:size 13})
|
||||
[:span "URLs support both GitHub repositories and local development servers.
|
||||
(For examples: https://github.com/xyhp915/logseq-journals-calendar,
|
||||
http://localhost:8080/<plugin-dir-root>)"]]]
|
||||
[:span (t :plugin.install-from-web-url/supports-note
|
||||
"https://github.com/xyhp915/logseq-journals-calendar"
|
||||
"http://localhost:8080/<plugin-dir-root>")]]]
|
||||
[:div.flex.justify-end
|
||||
(shui/button {:disabled (or pending? (string/blank? url))
|
||||
:on-click handle-submit!}
|
||||
(if pending? (ui/loading) "Install"))]]))
|
||||
(if pending? (ui/loading) (t :plugin/install)))]]))
|
||||
|
||||
(rum/defc install-from-github-release-container
|
||||
[]
|
||||
@@ -527,7 +532,7 @@
|
||||
[pending set-pending!] (rum/use-state false)
|
||||
*input (rum/use-ref nil)]
|
||||
[:div.p-4.flex.flex-col.pb-0
|
||||
(shui/input {:placeholder "GitHub repo url"
|
||||
(shui/input {:placeholder (t :plugin.install-from-web-url/repo-url-placeholder)
|
||||
:value url
|
||||
:ref *input
|
||||
:on-change #(set-url! (util/evalue %))
|
||||
@@ -536,11 +541,11 @@
|
||||
[:label.flex.items-center.gap-2
|
||||
(shui/checkbox {:checked (:theme? opts)
|
||||
:on-checked-change #(set-opts! (assoc opts :theme? %))})
|
||||
[:span.opacity-60 "theme?"]]
|
||||
[:span.opacity-60 (t :plugin.install-from-web-url/theme-label)]]
|
||||
[:label.flex.items-center.gap-2
|
||||
(shui/checkbox {:checked (:effect? opts)
|
||||
:on-checked-change #(set-opts! (assoc opts :effect? %))})
|
||||
[:span.opacity-60 "effect?"]]]
|
||||
[:span.opacity-60 (t :plugin.install-from-web-url/effect-label)]]]
|
||||
[:div.flex.justify-end.pt-3
|
||||
(shui/button
|
||||
{:on-click (fn []
|
||||
@@ -559,23 +564,26 @@
|
||||
(p/then #(shui/dialog-close!))
|
||||
(p/catch #(notification/show! (str %) :error))
|
||||
(p/finally #(set-pending! false))))
|
||||
(notification/show! "Invalid GitHub repo url" :error)))))
|
||||
(notification/show! (t :plugin/invalid-github-repo-url) :error)))))
|
||||
:disabled pending}
|
||||
(if pending (ui/loading "Installing") "Install"))]]))
|
||||
(if pending (ui/loading (t :plugin/installing)) (t :plugin/install)))]]))
|
||||
|
||||
(rum/defc auto-check-for-updates-control
|
||||
[]
|
||||
(let [[enabled, set-enabled!] (rum/use-state (plugin-handler/get-enabled-auto-check-for-updates?))
|
||||
text (t :plugin/auto-check-for-updates)]
|
||||
text (t :plugin/auto-update-check)]
|
||||
|
||||
[:div.flex.items-center.justify-between.px-3.py-2
|
||||
{:on-click (fn []
|
||||
(let [t (not enabled)]
|
||||
(set-enabled! t)
|
||||
(plugin-handler/set-enabled-auto-check-for-updates t)
|
||||
(let [next-enabled (not enabled)]
|
||||
(set-enabled! next-enabled)
|
||||
(plugin-handler/set-enabled-auto-check-for-updates next-enabled)
|
||||
(notification/show!
|
||||
[:span text [:strong.pl-1 (if t "ON" "OFF")] "!"]
|
||||
(if t :success :info))))}
|
||||
(into [:span]
|
||||
(interpolate-rich-text
|
||||
(t :plugin/auto-update-check-feedback)
|
||||
[[:strong.pl-1 (t (if next-enabled :ui/on :ui/off))]]))
|
||||
(if next-enabled :success :info))))}
|
||||
[:span.pr-3.opacity-80 text]
|
||||
(ui/toggle enabled #() true)]))
|
||||
|
||||
@@ -703,7 +711,7 @@
|
||||
:options {:on-click #(plugin-handler/user-check-enabled-for-updates! (not= :plugins category))}}])
|
||||
|
||||
(when (util/electron?)
|
||||
[{:title [:span.flex.items-center.gap-1 (ui/icon "world") (t :settings-page/network-proxy)]
|
||||
[{:title [:span.flex.items-center.gap-1 (ui/icon "world") (t :settings.advanced/network-proxy)]
|
||||
:options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}
|
||||
|
||||
{:title [:span.flex.items-center.gap-1 (ui/icon "arrow-down-circle") (t :plugin.install-from-file/menu-title)]
|
||||
@@ -927,7 +935,7 @@
|
||||
[:p.flex.justify-center.py-20 svg/loading]
|
||||
|
||||
@*error
|
||||
[:p.flex.justify-center.pt-20.opacity-50 (t :plugin/remote-error) (.-message @*error)]
|
||||
[:p.flex.justify-center.pt-20.opacity-50 (t :plugin/remote-error (.-message @*error))]
|
||||
|
||||
:else
|
||||
[:div.cp__plugins-marketplace-cnt
|
||||
@@ -1051,7 +1059,7 @@
|
||||
(lazy-items-loader load-more-pages!)
|
||||
[:div.flex.items-center.justify-center.py-28.flex-col.gap-2.opacity-30
|
||||
(shui/tabler-icon "list-search" {:size 40})
|
||||
[:span.text-sm "Nothing Found."]])]]))
|
||||
[:span.text-sm (t :plugin/empty)]])]]))
|
||||
|
||||
(rum/defcs waiting-coming-updates
|
||||
< rum/reactive
|
||||
@@ -1093,7 +1101,7 @@
|
||||
(ui/tooltip [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")] [:p notes]))]])]
|
||||
|
||||
;; all done
|
||||
[:div.py-4 [:strong.text-4xl (str "\uD83C\uDF89 " (t :plugin/all-updated))]])
|
||||
[:div.py-4 [:strong.text-4xl (str "🎉 " (t :plugin/update-all-success))]])
|
||||
|
||||
;; actions
|
||||
(when (seq updates)
|
||||
@@ -1142,7 +1150,7 @@
|
||||
(plugin-config-handler/replace-plugins plugins)
|
||||
(shui/dialog-close! "ls-plugins-from-file-modal")))]]
|
||||
;; all done
|
||||
[:div.py-4 [:strong.text-xl (str "\uD83C\uDF89 " (t :plugin.install-from-file/success))]])])
|
||||
[:div.py-4 [:strong.text-xl (str "🎉 " (t :plugin.install-from-file/success))]])])
|
||||
|
||||
(defn open-select-theme!
|
||||
[]
|
||||
@@ -1223,17 +1231,17 @@
|
||||
(plugin-handler/op-pinned-toolbar-item! pkey (if pinned? :remove :add)))
|
||||
true))}})
|
||||
[{:hr true}
|
||||
{:title (t :plugins)
|
||||
{:title (t :nav/plugins)
|
||||
:options {:on-click #(plugin-handler/goto-plugins-dashboard!)
|
||||
:class "extra-item mt-2"}
|
||||
:icon (ui/icon "apps")}
|
||||
|
||||
{:title (t :themes)
|
||||
{:title (t :nav/themes)
|
||||
:options {:on-click #(plugin-handler/show-themes-modal!)
|
||||
:class "extra-item"}
|
||||
:icon (ui/icon "palette")}
|
||||
|
||||
{:title (t :settings)
|
||||
{:title (t :nav/settings)
|
||||
:options {:on-click #(plugin-handler/goto-plugins-settings!)
|
||||
:class "extra-item"}
|
||||
:icon (ui/icon "adjustments")}
|
||||
@@ -1381,7 +1389,7 @@
|
||||
:class (when-not (util/electron?) "web-platform")
|
||||
:tab-index "-1"}
|
||||
|
||||
[:h1 (t :plugins)]
|
||||
[:h1 (t :nav/plugins)]
|
||||
|
||||
(when (util/electron?)
|
||||
[:<>
|
||||
@@ -1488,7 +1496,7 @@
|
||||
(when nav?
|
||||
[:aside.md:w-64 {:style {:min-width "10rem"}}
|
||||
[:header.cp__settings-header
|
||||
[:h1.cp__settings-modal-title (or title (t :settings-of-plugins))]]
|
||||
[:h1.cp__settings-modal-title (or title (t :plugin.settings/title))]]
|
||||
(let [plugins (plugin-handler/get-enabled-plugins-if-setting-schema)]
|
||||
[:ul.settings-plugin-list
|
||||
(for [{:keys [id name title icon]} plugins]
|
||||
@@ -1508,7 +1516,7 @@
|
||||
(when-let [^js pl (and focused (= @*cache focused)
|
||||
(plugin-handler/get-plugin-inst focused))]
|
||||
(ui/catch-error
|
||||
[:p.warning.text-lg.mt-5 "Settings schema Error!"]
|
||||
[:p.warning.text-lg.mt-5 (t :plugin/settings-schema-error)]
|
||||
(plugins-settings/settings-container
|
||||
(bean/->clj (.-settingsSchema pl)) pl)))]]]]))
|
||||
|
||||
@@ -1526,16 +1534,15 @@
|
||||
[pid name url]
|
||||
[:div
|
||||
[:span.block.whitespace-normal
|
||||
"This plugin "
|
||||
[:strong.text-error "#" name]
|
||||
" takes too long to load, affecting the application startup time and
|
||||
potentially causing other plugins to fail to load."]
|
||||
(interpolate-rich-text-node
|
||||
(t :plugin/perf-tip)
|
||||
[[:strong.text-error (str "#" name)]])]
|
||||
|
||||
[:path.opacity-50
|
||||
[:small [:span.pr-1 (ui/icon "folder")] url]]
|
||||
|
||||
[:p
|
||||
(ui/button "Disable now"
|
||||
(ui/button (t :plugin/disable-now)
|
||||
:small? true
|
||||
:on-click
|
||||
(fn []
|
||||
@@ -1543,9 +1550,10 @@
|
||||
(p/then #(do
|
||||
(notification/clear! pid)
|
||||
(notification/show!
|
||||
[:span "The plugin "
|
||||
[:strong.text-error "#" name]
|
||||
" is disabled."] :success
|
||||
(interpolate-rich-text-node
|
||||
(t :plugin/disable-for-performance-feedback)
|
||||
[[:strong.text-error (str "#" name)]])
|
||||
:success
|
||||
true nil 3000 nil)))
|
||||
(p/catch #(js/console.error %)))))]])
|
||||
|
||||
@@ -1576,7 +1584,7 @@
|
||||
(fn []
|
||||
[:div.settings-modal.of-plugins
|
||||
(focused-settings-content title)])
|
||||
{:label "plugin-settings-modal"
|
||||
{:label :plugin-settings-modal
|
||||
:align :start
|
||||
:id "ls-focused-settings-modal"}))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.components.plugins-settings
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[frontend.components.lazy-editor :as lazy-editor]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.plugin :as plugin-handler]
|
||||
[frontend.security :as security]
|
||||
@@ -25,8 +26,8 @@
|
||||
(plugin-handler/open-settings-file-in-default-app! pid)
|
||||
(set-edit-mode! #(if % nil :code))))}
|
||||
(if (= edit-mode :code)
|
||||
"Exit code mode"
|
||||
"Edit settings.json")])
|
||||
(t :plugin.settings/exit-code-mode)
|
||||
(t :plugin.settings/edit-settings-json))])
|
||||
|
||||
(rum/defc render-item-input
|
||||
[val {:keys [key type title default description inputAs]} update-setting!]
|
||||
@@ -104,7 +105,7 @@
|
||||
|
||||
(rum/defc render-item-not-handled
|
||||
[s]
|
||||
[:p.text-red-500 (str "#Not Handled# " s)])
|
||||
[:p.text-red-500 (t :plugin/setting-not-handled s)])
|
||||
|
||||
(rum/defc settings-container
|
||||
[schema ^js pl]
|
||||
@@ -147,7 +148,7 @@
|
||||
(let [^js cm (util/get-cm-instance (-> (.-target e) (.closest ".code-mode-wrap")))
|
||||
content' (some-> (.toJSON plugin-settings) (js/JSON.stringify nil 2))]
|
||||
(.setValue cm content')))}
|
||||
"Reset")
|
||||
(t :ui/reset))
|
||||
(shui/button {:size :sm
|
||||
:on-click (fn [^js e]
|
||||
(try
|
||||
@@ -158,7 +159,7 @@
|
||||
(set-edit-mode! nil))
|
||||
(catch js/Error e
|
||||
(notification/show! (.-message e) :error))))}
|
||||
"Save")]]
|
||||
(t :ui/save))]]
|
||||
|
||||
;; render with gui items
|
||||
(for [desc schema
|
||||
@@ -179,4 +180,4 @@
|
||||
key)))]]
|
||||
|
||||
;; no settings
|
||||
[:h2.font-bold.text-lg.py-4.warning "No Settings Schema!"])))
|
||||
[:h2.font-bold.text-lg.py-4.warning (t :plugin/no-settings-schema)])))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"Profiler UI"
|
||||
(:require [clojure.set :as set]
|
||||
[fipp.edn :as fipp]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.handler.profiler :as profiler-handler]
|
||||
[frontend.util :as util]
|
||||
[logseq.shui.ui :as shui]
|
||||
@@ -17,31 +18,32 @@
|
||||
*mem-leak-reports (get state ::mem-leak-reports)
|
||||
*register-fn-name (get state ::register-fn-name)]
|
||||
[:div
|
||||
[:b "Profiling fns(Only support UI thread now):"]
|
||||
[:div.pb-4
|
||||
[:b "Profiling fns (Only support UI thread now):"]
|
||||
[:div.pb-1
|
||||
(for [f-name profiling-fns]
|
||||
[:div.flex.flex-row.items-center.gap-2
|
||||
[:pre.select-text (str f-name)]
|
||||
[:a.inline.close.flex.transition-opacity.duration-300.ease-in
|
||||
{:title "Unregister"
|
||||
{:title (t :profiler/unregister)
|
||||
:on-pointer-down
|
||||
(fn [e]
|
||||
(util/stop e)
|
||||
(profiler-handler/unregister-fn! f-name))}
|
||||
(shui/tabler-icon "x")]])]
|
||||
[:div.flex.flex-row.items-center.gap-2
|
||||
[:div.flex.flex-row.items-center.gap-2.mb-2
|
||||
(shui/button
|
||||
{:on-click (fn []
|
||||
{:size :sm
|
||||
:on-click (fn []
|
||||
(when-let [fn-sym (some-> @*register-fn-name symbol)]
|
||||
(profiler-handler/register-fn! fn-sym)))}
|
||||
"Register fn")
|
||||
[:input.form-input.my-2.py-1
|
||||
[:input.form-input.flex-1.h-8.leading-8.py-0.box-border
|
||||
{:on-change (fn [e] (reset! *register-fn-name (util/evalue e)))
|
||||
:on-focus (fn [e] (let [v (.-value (.-target e))]
|
||||
(when (= v "input fn name here")
|
||||
(when (= v (t :profiler/input-fn-placeholder))
|
||||
(set! (.-value (.-target e)) ""))))
|
||||
:placeholder "input fn name here"}]]
|
||||
[:div.flex.gap-2.flex-wrap.items-center.pb-3
|
||||
:placeholder (t :profiler/input-fn-placeholder)}]]
|
||||
[:div.flex.gap-2.flex-wrap.items-center.pb-1
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:on-click (fn [_] (reset! *reports (profiler-handler/profile-report)))}
|
||||
@@ -53,23 +55,25 @@
|
||||
(shui/tabler-icon "x") "Reset reports")]
|
||||
(let [update-time-sum
|
||||
(fn [m] (update-vals m (fn [m2] (update-vals m2 #(.toFixed % 6)))))]
|
||||
[:div.pb-4
|
||||
[:div.pb-0
|
||||
[:pre.select-text
|
||||
(when @*reports
|
||||
(-> @*reports
|
||||
(update :time-sum update-time-sum)
|
||||
(fipp/pprint {:width 20})
|
||||
with-out-str))]])
|
||||
[:hr]
|
||||
[:b "Atom/Volatile Mem Leak Detect(Only support UI thread now):"]
|
||||
[:pre "Only check atoms/volatiles with a value type of `coll`.
|
||||
[:hr.my-2]
|
||||
[:div.pb-1
|
||||
[:b "Atom/Volatile Mem Leak Detect (Only support UI thread now):"]
|
||||
[:pre.mb-2 "Only check atoms/volatiles with a value type of `coll`.
|
||||
The report shows refs with coll-size > 5k and atom's watches-count > 1k.
|
||||
`ref` means atom or volatile.
|
||||
`ref-hash` means `(hash ref)`."]
|
||||
[:div.flex.flex-row.items-center.gap-2
|
||||
`ref-hash` means `(hash ref)`."]]
|
||||
[:div.flex.flex-row.items-center.gap-2.pb-2
|
||||
(if (= 2 (count (set/difference #{'cljs.core/reset! 'cljs.core/vreset!} (set profiling-fns))))
|
||||
(shui/button
|
||||
{:on-click (fn []
|
||||
{:size :sm
|
||||
:on-click (fn []
|
||||
(profiler-handler/mem-leak-detect))}
|
||||
"Start to detect")
|
||||
(shui/button
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[frontend.components.select :as select]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.async :as db-async]
|
||||
@@ -50,7 +51,7 @@
|
||||
(do
|
||||
(when (and (not (ldb/public-built-in-property? property))
|
||||
(ldb/built-in? property))
|
||||
(notification/show! "This is a private built-in property that can't be used." :error))
|
||||
(notification/show! (t :property/private-built-in-not-usable) :error))
|
||||
property)
|
||||
;; new property entered or converting page to property
|
||||
(if (db-property/valid-property-name? property-title)
|
||||
@@ -62,7 +63,7 @@
|
||||
_ (when add-class-property?
|
||||
(pv/<add-property! entity (:db/ident property) "" {:class-schema? class-schema? :exit-edit? false}))]
|
||||
property)
|
||||
(notification/show! "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['." :error)))))
|
||||
(notification/show! (t :property.validation/invalid-name) :error)))))
|
||||
|
||||
;; TODO: This component should be cleaned up as it's only used for new properties and used to be used for existing properties
|
||||
(rum/defcs property-type-select <
|
||||
@@ -72,7 +73,7 @@
|
||||
*show-class-select?
|
||||
default-open? class-schema?]
|
||||
:as opts}]
|
||||
(let [property-name (or (and *property-name @*property-name) (:block/title property))
|
||||
(let [property-name (or (and *property-name @*property-name) (db-property/built-in-display-title property t))
|
||||
property-schema (or (and *property-schema @*property-schema)
|
||||
(select-keys property [:logseq.property/type]))
|
||||
schema-types (->> (concat db-property-type/user-built-in-property-types
|
||||
@@ -134,7 +135,7 @@
|
||||
(shui/select-trigger
|
||||
{:class "!px-2 !py-0 !h-8"}
|
||||
(shui/select-value
|
||||
{:placeholder "Select a property type"}))
|
||||
{:placeholder (t :property/select-type-placeholder)}))
|
||||
(shui/select-content
|
||||
(shui/select-group
|
||||
(for [{:keys [label value disabled]} schema-types]
|
||||
@@ -144,7 +145,7 @@
|
||||
(util/stop-propagation e)))} label)))))
|
||||
(when show-type-change-hints?
|
||||
(ui/tooltip (svg/info)
|
||||
[:span "Changing the property type clears some property configurations."]))]))
|
||||
[:span (t :property/type-change-warning)]))]))
|
||||
|
||||
(rum/defc property-select
|
||||
[select-opts]
|
||||
@@ -172,7 +173,7 @@
|
||||
(map (fn [x]
|
||||
(let [convert? (:convert-page-to-property? x)]
|
||||
{:label (if convert?
|
||||
(util/format "Convert \"%s\" to property" (:block/title x))
|
||||
(t :property/convert-page-to-property (:block/title x))
|
||||
(let [ident (:db/ident x)
|
||||
ns' (some-> ident (namespace))
|
||||
plugin? (some-> ident (api-block/plugin-property-key?))
|
||||
@@ -200,7 +201,7 @@
|
||||
:new-case-sensitive? true
|
||||
:show-new-when-not-exact-match? true
|
||||
;; :exact-match-exclude-items (fn [s] (contains? excluded-properties s))
|
||||
:input-default-placeholder "Add or change property"
|
||||
:input-default-placeholder (t :property/add-or-change)
|
||||
:on-input set-q!}
|
||||
select-opts))]])))
|
||||
|
||||
@@ -279,7 +280,7 @@
|
||||
:a
|
||||
{:tabIndex 0
|
||||
:title (or (:block/title (:logseq.property/description property))
|
||||
(:block/title property))
|
||||
(db-property/built-in-display-title property t))
|
||||
:class "property-k flex select-none jtrigger w-full"
|
||||
:on-pointer-down (fn [^js e]
|
||||
(when (util/meta-key? e)
|
||||
@@ -302,7 +303,7 @@
|
||||
:dropdown-menu? true
|
||||
:as-dropdown? true})))}
|
||||
|
||||
(:block/title property)))
|
||||
(db-property/built-in-display-title property t)))
|
||||
|
||||
(rum/defc property-key-cp < rum/static
|
||||
[block property {:keys [other-position? class-schema?]}]
|
||||
@@ -339,7 +340,7 @@
|
||||
(if config/publishing?
|
||||
[:a.property-k.flex.select-none.jtrigger
|
||||
{:on-click #(route-handler/redirect-to-page! (:block/uuid property))}
|
||||
(:block/title property)]
|
||||
(db-property/built-in-display-title property t)]
|
||||
(property-key-title block property class-schema?))]))
|
||||
|
||||
(defn- bidirectional-property-icon-cp
|
||||
@@ -362,7 +363,7 @@
|
||||
(if (and blocks-container (seq entities))
|
||||
[:div.property-block-container.content.w-full
|
||||
(blocks-container config entities)]
|
||||
[:span.opacity-60 "Empty"])))
|
||||
[:span.opacity-60 (t :view.filter/empty)])))
|
||||
|
||||
(rum/defc bidirectional-properties-section < rum/static
|
||||
[bidirectional-properties]
|
||||
@@ -493,7 +494,7 @@
|
||||
[:div.flex.flex-row.items-center.shrink-0
|
||||
(ui/icon "plus" {:size 15 :class "opacity-50"})
|
||||
[:div.ml-1 {:style {:margin-top 1}}
|
||||
"Add property"]]]])))
|
||||
(t :property/add-new)]]]])))
|
||||
|
||||
(defn- resolve-linked-block-if-exists
|
||||
"Properties will be updated for the linked page instead of the refed block.
|
||||
@@ -684,7 +685,7 @@
|
||||
[:details.my-1
|
||||
[:summary.text-sm.opacity-50.hover:opacity-90.cursor-pointer
|
||||
{:style {:margin-left 11}}
|
||||
[:span.ml-1 "Hidden properties"]]
|
||||
[:span.ml-1 (t :property/hidden-properties)]]
|
||||
[:div.mt-1
|
||||
(properties-section block hidden-properties opts)]]))
|
||||
|
||||
@@ -854,7 +855,7 @@
|
||||
[:div.property-key.text-sm
|
||||
(property-key-cp block (db/entity :logseq.property.class/properties) {})]]
|
||||
[:div.text-muted-foreground {:style {:margin-left 26}}
|
||||
"Tag properties are inherited by all nodes using the tag. For example, each #Task node inherits 'Status' and 'Priority'."]]
|
||||
(t :class/tag-properties-desc)]]
|
||||
[:div.ml-4
|
||||
(properties-section block properties opts')
|
||||
(hidden-properties-cp block hidden-properties
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[frontend.components.property.value :as pv]
|
||||
[frontend.components.select :as select]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.async :as db-async]
|
||||
@@ -100,12 +101,12 @@
|
||||
:value (:block/uuid class)})
|
||||
classes)
|
||||
options (if no-class?
|
||||
(cons {:label "Skip choosing tag"
|
||||
(cons {:label (t :property/skip-choosing-tag)
|
||||
:value :no-tag}
|
||||
options)
|
||||
options)
|
||||
opts {:items options
|
||||
:input-default-placeholder (if multiple-choices? "Choose tags" "Choose tag")
|
||||
:input-default-placeholder (if multiple-choices? (t :property/choose-tags) (t :property/choose-tag))
|
||||
:dropdown? false
|
||||
:close-modal? false
|
||||
:multiple-choices? multiple-choices?
|
||||
@@ -182,13 +183,13 @@
|
||||
(shui/input {:ref *input-ref
|
||||
:size "sm"
|
||||
:default-value title
|
||||
:placeholder "name"
|
||||
:placeholder (t :property/name-placeholder)
|
||||
:disabled disabled?
|
||||
:on-key-down (fn [e]
|
||||
(when (contains? #{"ArrowLeft" "ArrowRight"} (util/ekey e))
|
||||
(util/stop-propagation e)))
|
||||
:on-change (fn [^js e] (set-form-data! (assoc form-data :title (util/trim-safe (util/evalue e)))))})]
|
||||
[:div.pt-2 (shui/textarea {:placeholder "description" :default-value description
|
||||
[:div.pt-2 (shui/textarea {:placeholder (t :property/description-placeholder) :default-value description
|
||||
:disabled disabled? :on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
|
||||
|
||||
(let [dirty? (not= (rum/deref *form-data) form-data)]
|
||||
@@ -208,7 +209,7 @@
|
||||
(p/then #(set-sub-open! false))
|
||||
(p/catch #(shui/toast! (str %) :error))
|
||||
(p/finally #(set-saving! false))))}
|
||||
"Save")])]))
|
||||
(t :ui/save))])]))
|
||||
|
||||
(rum/defc choice-base-edit-form
|
||||
[own-property block owner-block]
|
||||
@@ -239,9 +240,9 @@
|
||||
(shui/input {:ref *input-ref :size "sm"
|
||||
:default-value (:value form-data)
|
||||
:on-change (fn [^js e] (set-form-data! (assoc form-data :value (util/trim-safe (util/evalue e)))))
|
||||
:placeholder "title"})]
|
||||
:placeholder (t :property/title-placeholder)})]
|
||||
[:div.pt-2 (shui/textarea
|
||||
{:placeholder "description" :default-value (:description form-data)
|
||||
{:placeholder (t :property/description-placeholder) :default-value (:description form-data)
|
||||
:on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
|
||||
[:div.pt-2.flex.justify-end
|
||||
(let [dirty? (not= (rum/deref *form-data) form-data)]
|
||||
@@ -257,7 +258,7 @@
|
||||
(p/then #(shui/popup-hide!))
|
||||
(p/catch #(shui/toast! (str %) :error))))
|
||||
:variant (if dirty? :default :secondary)}
|
||||
"Save"))]]))
|
||||
(t :ui/save)))]]))
|
||||
|
||||
(defn restore-root-highlight-item!
|
||||
[id]
|
||||
@@ -335,18 +336,18 @@
|
||||
excluded-ids (set (keep :db/id (:logseq.property/choice-exclusions owner-block')))
|
||||
global-choice? (empty? (:logseq.property/choice-classes block))]
|
||||
[:li
|
||||
(shui/button {:size :sm :variant :ghost :title "Drag && Drop to reorder"}
|
||||
(shui/button {:size :sm :variant :ghost :title (t :property/drag-to-reorder)}
|
||||
(shui/tabler-icon "grip-vertical" {:size 14}))
|
||||
(icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon))
|
||||
:popup-opts {:align "start"}
|
||||
:del-btn? (boolean icon)
|
||||
:empty-label "?"
|
||||
:button-opts {:title "Set Icon"}})
|
||||
:button-opts {:title (t :property/set-icon)}})
|
||||
[:strong {:on-click (fn [^js e]
|
||||
(shui/popup-show! (.-target e)
|
||||
(fn [] (choice-base-edit-form property block owner-block))
|
||||
{:id :ls-base-edit-form
|
||||
:align "start"}))
|
||||
(fn [] (choice-base-edit-form property block owner-block))
|
||||
{:id :ls-base-edit-form
|
||||
:align "start"}))
|
||||
:title value}
|
||||
value]
|
||||
(shui/dropdown-menu
|
||||
@@ -355,7 +356,7 @@
|
||||
:disabled disabled?}
|
||||
(shui/button
|
||||
{:size :sm :variant :ghost
|
||||
:title "More settings"}
|
||||
:title (t :property/more-settings)}
|
||||
(shui/tabler-icon "dots" {:size 16})))
|
||||
(shui/dropdown-menu-content
|
||||
;; default choice
|
||||
@@ -372,10 +373,10 @@
|
||||
value))}
|
||||
(shui/checkbox {:id "default value"
|
||||
:size :sm
|
||||
:title "Set as default choice"
|
||||
:title (t :property/set-default-choice)
|
||||
:class "mr-1 opacity-50 hover:opacity-100"
|
||||
:checked default-value?})
|
||||
"Set as default choice")))
|
||||
(t :property/set-default-choice))))
|
||||
|
||||
(when (and owner-class? owner-block' global-choice?)
|
||||
(let [excluded? (contains? excluded-ids (:db/id block))
|
||||
@@ -389,24 +390,24 @@
|
||||
:on-click toggle-exclusion!}
|
||||
(shui/checkbox {:id "exclude for tag"
|
||||
:size :sm
|
||||
:title "Hide choice for this tag"
|
||||
:title (t :property/hide-choice-for-tag)
|
||||
:class "mr-1 opacity-50 hover:opacity-100"
|
||||
:checked excluded?})
|
||||
(str "Hide for #" tag-title))))
|
||||
(t :property/hide-for-tag tag-title))))
|
||||
|
||||
(when-not (and owner-class? global-choice?)
|
||||
(shui/dropdown-menu-item
|
||||
{:key "delete"
|
||||
:class "del"
|
||||
:on-click delete-choice!}
|
||||
[:span.w-full.text-red-rx-09.opacity-90.flex.items-center.hover:opacity-100
|
||||
(ui/icon "x" {:class "scale-90 pr-1"}) "Delete"]))))]))
|
||||
[:span.w-full.text-red-rx-09.opacity-90.flex.items-center.hover:opacity-100
|
||||
(ui/icon "x" {:class "scale-90 pr-1"}) (t :ui/delete)]))))]))
|
||||
|
||||
(rum/defc add-existing-values
|
||||
[property values {:keys [toggle-fn]}]
|
||||
[:div.flex.flex-col.gap-1.w-64.p-4.overflow-y-auto
|
||||
{:class "max-h-[50dvh]"}
|
||||
[:div "Existing values:"]
|
||||
[:div (t :property/existing-values)]
|
||||
[:ol
|
||||
(for [value values]
|
||||
[:li (:label value)])]
|
||||
@@ -416,7 +417,7 @@
|
||||
(map (fn [{:keys [value]}]
|
||||
(:block/uuid value)) values))]
|
||||
(toggle-fn)))}
|
||||
"Add choices")])
|
||||
(t :property/add-choices))])
|
||||
|
||||
(rum/defcs choices-sub-pane < rum/reactive db-mixins/query
|
||||
(rum/local false ::show-hidden?)
|
||||
@@ -459,7 +460,7 @@
|
||||
:class "text-muted-foreground"
|
||||
:on-click (fn []
|
||||
(swap! *show-hidden? not))}
|
||||
(if @*show-hidden? "Hide hidden choices" "Show hidden choices")))
|
||||
(if @*show-hidden? (t :property/hide-hidden-choices) (t :property/show-hidden-choices))))
|
||||
[:ul.choices-list
|
||||
(dnd/items choice-items
|
||||
{:sort-by-inner-element? false
|
||||
@@ -485,7 +486,7 @@
|
||||
;; add choice
|
||||
(when-not disabled?
|
||||
(dropdown-editor-menuitem
|
||||
{:icon :plus :title "Add choice"
|
||||
{:icon :plus :title (t :property/add-choice)
|
||||
:item-props {:on-click
|
||||
(fn [^js e]
|
||||
(p/let [values (db-async/<get-property-values (:db/ident property) {})
|
||||
@@ -521,7 +522,7 @@
|
||||
opts
|
||||
(shui/select-trigger
|
||||
{:class "h-8"}
|
||||
(shui/select-value {:placeholder "Select a choice"}))
|
||||
(shui/select-value {:placeholder (t :property/select-choice)}))
|
||||
(shui/select-content
|
||||
(map (fn [choice]
|
||||
(shui/select-item {:key (str (:db/id choice))
|
||||
@@ -530,7 +531,7 @@
|
||||
unchecked-choice (some (fn [choice] (when (false? (:logseq.property/choice-checkbox-state choice)) choice)) choices)]
|
||||
[:div.flex.flex-col.gap-4.text-sm.p-2
|
||||
[:div.flex.flex-col.gap-2
|
||||
[:div "Map unchecked to"]
|
||||
[:div (t :property/map-unchecked-to)]
|
||||
(select-cp
|
||||
(cond->
|
||||
{:on-value-change
|
||||
@@ -542,7 +543,7 @@
|
||||
unchecked-choice
|
||||
(assoc :default-value (:db/id unchecked-choice))))
|
||||
|
||||
[:div.mt-2 "Map checked to"]
|
||||
[:div.mt-2 (t :property/map-checked-to)]
|
||||
(select-cp
|
||||
(cond->
|
||||
{:on-value-change
|
||||
@@ -554,11 +555,12 @@
|
||||
checked-choice
|
||||
(assoc :default-value (:db/id checked-choice))))]]))
|
||||
|
||||
(def position-labels
|
||||
{:properties {:icon :layout-distribute-horizontal :title "Block properties"}
|
||||
:block-left {:icon :layout-align-right :title "Beginning of the block"}
|
||||
:block-right {:icon :layout-align-left :title "End of the block"}
|
||||
:block-below {:icon :layout-align-top :title "Below the block"}})
|
||||
(defn position-labels
|
||||
[]
|
||||
{:properties {:icon :layout-distribute-horizontal :title (t :property/ui-position-properties)}
|
||||
:block-left {:icon :layout-align-right :title (t :property/ui-position-block-left)}
|
||||
:block-right {:icon :layout-align-left :title (t :property/ui-position-block-right)}
|
||||
:block-below {:icon :layout-align-top :title (t :property/ui-position-block-below)}})
|
||||
|
||||
(rum/defc ui-position-sub-pane
|
||||
[property {:keys [id set-sub-open! _ui-position]}]
|
||||
@@ -572,7 +574,7 @@
|
||||
(restore-root-highlight-item! id)))
|
||||
item-props {:on-select handle-select!}]
|
||||
[:div.ls-property-dropdown.ls-property-ui-position-sub-pane
|
||||
(for [[k v] position-labels]
|
||||
(for [[k v] (position-labels)]
|
||||
(let [item-props (assoc item-props :data-value k)]
|
||||
(dropdown-editor-menuitem
|
||||
(assoc v :item-props item-props))))]))
|
||||
@@ -580,10 +582,13 @@
|
||||
(defn property-type-label
|
||||
[property-type]
|
||||
(case property-type
|
||||
:default
|
||||
"Text"
|
||||
:datetime
|
||||
"DateTime"
|
||||
:default (t :property/type-text)
|
||||
:number (t :property/type-number)
|
||||
:date (t :property/type-date)
|
||||
:datetime (t :property/type-datetime)
|
||||
:checkbox (t :property/type-checkbox)
|
||||
:url (t :property/type-url)
|
||||
:node (t :property/type-node)
|
||||
((comp string/capitalize name) property-type)))
|
||||
|
||||
(defn- handle-delete-property!
|
||||
@@ -594,14 +599,20 @@
|
||||
(property-handler/remove-block-property! (:block/uuid block) (:db/ident property)))]
|
||||
(if (and class? class-schema?)
|
||||
(-> (shui/dialog-confirm!
|
||||
[:p "Are you sure you want to delete the property from this tag?"]
|
||||
[:p (t :property/delete-from-tag-confirm)]
|
||||
{:id :delete-property-from-class
|
||||
:data-reminder :ok})
|
||||
:data-reminder :ok
|
||||
:data-reminder-label (t :ui/dont-remind-me-again)
|
||||
:cancel-label (t :ui/cancel)
|
||||
:ok-label (t :ui/confirm)})
|
||||
(p/then remove!))
|
||||
(-> (shui/dialog-confirm!
|
||||
"Are you sure you want to delete the property from this node?"
|
||||
(t :property/delete-from-node-confirm)
|
||||
{:id :delete-property-from-node
|
||||
:data-reminder :ok})
|
||||
:data-reminder :ok
|
||||
:data-reminder-label (t :ui/dont-remind-me-again)
|
||||
:cancel-label (t :ui/cancel)
|
||||
:ok-label (t :ui/confirm)})
|
||||
(p/then remove!)))))
|
||||
|
||||
(rum/defc property-type-sub-pane
|
||||
@@ -634,14 +645,14 @@
|
||||
option (if (= :checkbox property-type)
|
||||
(let [default-value (:logseq.property/scalar-default-value property)]
|
||||
{:icon :settings-2
|
||||
:title "Default value"
|
||||
:title (t :property/default-value)
|
||||
:toggle-checked? (boolean default-value)
|
||||
:checkbox? true
|
||||
:on-toggle-checked-change (fn []
|
||||
(db-property-handler/set-block-property! (:block/uuid property) :logseq.property/scalar-default-value (not default-value)))})
|
||||
(let [default-value (:logseq.property/default-value property)]
|
||||
{:icon :settings-2 :title "Default value"
|
||||
:desc (if default-value (db-property/property-value-content default-value) "Set value")
|
||||
{:icon :settings-2 :title (t :property/default-value)
|
||||
:desc (if default-value (db-property/property-value-content default-value) (t :property/set-value))
|
||||
:submenu-content (fn [] (pdv/default-value-config property))}))]
|
||||
(dropdown-editor-menuitem (assoc option :disabled? config/publishing?))))
|
||||
|
||||
@@ -649,7 +660,7 @@
|
||||
"property: block entity"
|
||||
[property owner-block values {:keys [class-schema? debug? with-title? more-options]
|
||||
:or {with-title? true}}]
|
||||
(let [title (:block/title property)
|
||||
(let [title (db-property/built-in-display-title property t)
|
||||
property-type (:logseq.property/type property)
|
||||
property-type-label' (some-> property-type (property-type-label))
|
||||
enable-closed-values? (contains? db-property-type/closed-value-property-types
|
||||
@@ -664,18 +675,18 @@
|
||||
(->>
|
||||
[(when with-title?
|
||||
[:h3.font-medium.px-2.py-2.opacity-80.flex.items-center.gap-1
|
||||
"Configure property"])
|
||||
(t :property/configure)])
|
||||
(when-not special-built-in-prop?
|
||||
(dropdown-editor-menuitem {:icon :pencil :title "Property name" :desc [:span.flex.items-center.gap-1 icon title]
|
||||
(dropdown-editor-menuitem {:icon :pencil :title (t :property/name) :desc [:span.flex.items-center.gap-1 icon title]
|
||||
:submenu-content (fn [ops] (name-edit-pane property (assoc ops :disabled? disabled?)))}))
|
||||
(let [disabled?' (or disabled? (and property-type (seq values)))]
|
||||
(dropdown-editor-menuitem {:icon :letter-t
|
||||
:title "Property type"
|
||||
:title (t :property/type)
|
||||
:desc (if disabled?'
|
||||
(ui/tooltip
|
||||
[:span (str property-type-label')]
|
||||
[:div.w-96
|
||||
"The type of this property is locked once you start using it. This is to make sure all your existing information stays correct if the property type is changed later. To unlock, all uses of a property must be deleted."])
|
||||
(t :property/type-locked-help)])
|
||||
(str property-type-label'))
|
||||
:disabled? disabled?'
|
||||
:submenu-content (fn [ops]
|
||||
@@ -685,7 +696,7 @@
|
||||
(not (contains? #{:logseq.property.class/extends} (:db/ident property))))
|
||||
(dropdown-editor-menuitem {:icon :hash
|
||||
:disabled? disabled?
|
||||
:title "Specify node tags"
|
||||
:title (t :property/specify-node-tags)
|
||||
:desc ""
|
||||
:submenu-content (fn [_ops]
|
||||
[:div.px-4
|
||||
@@ -700,8 +711,8 @@
|
||||
|
||||
(when enable-closed-values?
|
||||
(let [values (:property/closed-values property)]
|
||||
(dropdown-editor-menuitem {:icon :list :title "Available choices"
|
||||
:desc (when (seq values) (str (count values) " choices"))
|
||||
(dropdown-editor-menuitem {:icon :list :title (t :property/available-choices)
|
||||
:desc (when (seq values) (t :property/choices-count (count values)))
|
||||
:submenu-content (fn []
|
||||
(choices-sub-pane property
|
||||
{:disabled? config/publishing?
|
||||
@@ -713,14 +724,14 @@
|
||||
(when (>= (count values) 2)
|
||||
(dropdown-editor-menuitem
|
||||
{:icon :checkbox
|
||||
:title "Checkbox state mapping"
|
||||
:title (t :property/checkbox-state-mapping)
|
||||
:disabled? config/publishing?
|
||||
:submenu-content (fn []
|
||||
(checkbox-state-mapping values))}))))
|
||||
|
||||
(when (and (contains? db-property-type/cardinality-property-types property-type) (not disabled?))
|
||||
(let [many? (db-property/many? property)]
|
||||
(dropdown-editor-menuitem {:icon :checks :title "Multiple values"
|
||||
(dropdown-editor-menuitem {:icon :checks :title (t :property/multiple-values)
|
||||
:toggle-checked? many?
|
||||
:on-toggle-checked-change
|
||||
(fn []
|
||||
@@ -730,7 +741,9 @@
|
||||
;; Only show dialog for existing values as it can be reversed for unused properties
|
||||
(if (and (seq values) (not many?))
|
||||
(-> (shui/dialog-confirm!
|
||||
"This action cannot be undone. Do you want to change this property to have multiple values?")
|
||||
(t :property/multiple-values-confirm)
|
||||
{:cancel-label (t :ui/cancel)
|
||||
:ok-label (t :ui/confirm)})
|
||||
(p/then update-cardinality-fn))
|
||||
(update-cardinality-fn))))})))
|
||||
|
||||
@@ -744,20 +757,20 @@
|
||||
(empty? (:property/closed-values property))
|
||||
(contains? #{nil :properties} (:logseq.property/ui-position property)))))
|
||||
(let [position (:logseq.property/ui-position property)]
|
||||
(dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
|
||||
(dropdown-editor-menuitem {:icon :float-left :title (t :property/ui-position) :desc (some-> (position-labels) (get position) :title)
|
||||
:item-props {:class "ui__position-trigger-item"}
|
||||
:disabled? config/publishing?
|
||||
:submenu-content (fn [ops] (ui-position-sub-pane property (assoc ops :ui-position position)))})))
|
||||
|
||||
(when (not (contains? #{:logseq.property.class/extends :logseq.property.class/properties} (:db/ident property)))
|
||||
(dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:logseq.property/hide? property))
|
||||
(dropdown-editor-menuitem {:icon :eye-off :title (t :property/hide-by-default) :toggle-checked? (boolean (:logseq.property/hide? property))
|
||||
:disabled? config/publishing?
|
||||
:on-toggle-checked-change #(db-property-handler/set-block-property! (:db/id property)
|
||||
:logseq.property/hide?
|
||||
%)}))
|
||||
(when (not (contains? #{:logseq.property.class/extends :logseq.property.class/properties} (:db/ident property)))
|
||||
(dropdown-editor-menuitem
|
||||
{:icon :eye-off :title "Hide empty value"
|
||||
{:icon :eye-off :title (t :property/hide-empty-value)
|
||||
:toggle-checked? (boolean (:logseq.property/hide-empty-value property))
|
||||
:disabled? config/publishing?
|
||||
:on-toggle-checked-change (fn []
|
||||
@@ -772,7 +785,7 @@
|
||||
[:<>
|
||||
(shui/dropdown-menu-separator)
|
||||
(dropdown-editor-menuitem
|
||||
{:icon :share-3 :title "Go to this property" :desc ""
|
||||
{:icon :share-3 :title (t :property/go-to-this-property) :desc ""
|
||||
:item-props {:class "opacity-90 focus:opacity-100"
|
||||
:on-select (fn []
|
||||
(shui/popup-hide-all!)
|
||||
@@ -784,19 +797,19 @@
|
||||
(set (map :db/id (:logseq.property/checkbox-display-properties owner-block)))
|
||||
(:db/id property))]
|
||||
(dropdown-editor-menuitem
|
||||
{:icon :checkbox
|
||||
:title (if class-schema? "Show as checkbox on tagged nodes" "Show as checkbox on node")
|
||||
:disabled? config/publishing?
|
||||
:desc (when owner-block
|
||||
(shui/switch
|
||||
{:id "show as checkbox" :size "sm"
|
||||
:checked checked?
|
||||
:on-click util/stop-propagation
|
||||
:on-checked-change
|
||||
(fn [value]
|
||||
(if value
|
||||
(db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
|
||||
(db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))})))))
|
||||
{:icon :checkbox})
|
||||
:title (if class-schema? (t :property/show-as-checkbox-on-tagged-nodes) (t :property/show-as-checkbox-on-node))
|
||||
:disabled? config/publishing?
|
||||
:desc (when owner-block
|
||||
(shui/switch
|
||||
{:id "show as checkbox" :size "sm"
|
||||
:checked checked?
|
||||
:on-click util/stop-propagation
|
||||
:on-checked-change
|
||||
(fn [value]
|
||||
(if value
|
||||
(db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
|
||||
(db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))))))
|
||||
|
||||
(when (and owner-block
|
||||
;; Any property should be removable from Tag Properties
|
||||
@@ -806,7 +819,7 @@
|
||||
|
||||
(dropdown-editor-menuitem
|
||||
{:id :delete-property :icon :x
|
||||
:title (if class-schema? "Delete property from tag" "Delete property from node")
|
||||
:title (if class-schema? (t :property/delete-from-tag) (t :property/delete-from-node))
|
||||
:desc "" :disabled? false
|
||||
:item-props {:class "opacity-60 focus:!text-red-rx-09 focus:opacity-100"
|
||||
:on-select (fn [^js e]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[frontend.components.icon :as icon-component]
|
||||
[frontend.components.select :as select]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.date :as date]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
@@ -54,16 +55,11 @@
|
||||
|
||||
(rum/defc property-empty-btn-value
|
||||
[property & opts]
|
||||
(let [text (cond
|
||||
(= (:db/ident property) :logseq.property/description)
|
||||
"Add description"
|
||||
:else
|
||||
"Empty")]
|
||||
(if (= text "Empty")
|
||||
(shui/button (merge {:class "empty-btn" :variant :text} opts)
|
||||
text)
|
||||
(shui/button (merge {:class "empty-btn" :variant :text} opts)
|
||||
text))))
|
||||
(let [text (if (= (:db/ident property) :logseq.property/description)
|
||||
(t :property/add-description)
|
||||
(t :ui/empty))]
|
||||
(shui/button (merge {:class "empty-btn" :variant :text} opts)
|
||||
text)))
|
||||
|
||||
(rum/defc property-empty-text-value
|
||||
[property {:keys [property-position table-view?]}]
|
||||
@@ -74,7 +70,7 @@
|
||||
(if-let [icon (:logseq.property/icon property)]
|
||||
(icon-component/icon icon {:color? true})
|
||||
(ui/icon "line-dashed"))
|
||||
"Empty"))])
|
||||
(t :ui/empty)))])
|
||||
|
||||
(defn- get-selected-blocks
|
||||
[]
|
||||
@@ -204,7 +200,7 @@
|
||||
(:db/id (db/entity :block/page))
|
||||
{:entity-id? entity-id?})))))
|
||||
(when (seq (:view/selected-blocks @state/state))
|
||||
(notification/show! "Property updated!" :success))
|
||||
(notification/show! (t :property/update-success) :success))
|
||||
(when-not many?
|
||||
(cond
|
||||
exit-edit?
|
||||
@@ -264,11 +260,13 @@
|
||||
(db-property-handler/remove-block-property! (:db/id block)
|
||||
:logseq.property.repeat/temporal-property)))))]
|
||||
(if (#{:logseq.property/deadline :logseq.property/scheduled} (:db/ident property))
|
||||
[:div "Repeat task"]
|
||||
[:div "Repeat " (if (= :date (:logseq.property/type property)) "date" "datetime")])]]
|
||||
[:div (t :property.repeat/task)]
|
||||
[:div (t (if (= :date (:logseq.property/type property))
|
||||
:property.repeat/date
|
||||
:property.repeat/datetime))])]]
|
||||
[:div.flex.flex-row.gap-2.ls-repeat-task-frequency
|
||||
[:div.flex.text-muted-foreground
|
||||
"Every"]
|
||||
(t :property.repeat/every)]
|
||||
|
||||
;; recur frequency
|
||||
[:div.w-10.mr-2
|
||||
@@ -292,7 +290,7 @@
|
||||
(db/entity :logseq.property/status.done))]
|
||||
[:div.flex.flex-col.gap-2
|
||||
[:div.text-muted-foreground
|
||||
"When"]
|
||||
(t :property.repeat/when)]
|
||||
(shui/select
|
||||
(cond->
|
||||
{:on-value-change (fn [v]
|
||||
@@ -302,16 +300,16 @@
|
||||
property-id
|
||||
(assoc :default-value property-id))
|
||||
(shui/select-trigger
|
||||
(shui/select-value {:placeholder "Select a property"}))
|
||||
(shui/select-value {:placeholder (t :property/select-property-placeholder)}))
|
||||
(shui/select-content
|
||||
(map (fn [choice]
|
||||
(shui/select-item {:key (str (:db/id choice))
|
||||
:value (:db/id choice)} (:block/title choice))) properties)))
|
||||
:value (:db/id choice)} (db-property/built-in-display-title choice t))) properties)))
|
||||
[:div.flex.flex-row.gap-1
|
||||
[:div.text-muted-foreground
|
||||
"is:"]
|
||||
(t :property.repeat/is-label)]
|
||||
(when done-choice
|
||||
(db-property/property-value-content done-choice))]])]))
|
||||
(db-property/built-in-display-title done-choice t))]])]))
|
||||
|
||||
(defn- <resolve-journal-page-for-date
|
||||
([^js d]
|
||||
@@ -329,6 +327,15 @@
|
||||
journal-page
|
||||
(create-page-f journal {:redirect? false})))))
|
||||
|
||||
(defn- focus-selected-day!
|
||||
[id remaining]
|
||||
(when (pos? remaining)
|
||||
(if-let [selected-day (some-> id
|
||||
(js/document.getElementById)
|
||||
(.querySelector "[aria-selected=true]"))]
|
||||
(.focus selected-day)
|
||||
(js/setTimeout #(focus-selected-day! id (dec remaining)) 16))))
|
||||
|
||||
(rum/defcs calendar-inner < rum/reactive db-mixins/query
|
||||
(rum/local (str "calendar-inner-" (js/Date.now)) ::identity)
|
||||
{:init (fn [state]
|
||||
@@ -336,10 +343,7 @@
|
||||
state)
|
||||
:will-mount (fn [state]
|
||||
(js/setTimeout
|
||||
#(some-> @(::identity state)
|
||||
(js/document.getElementById)
|
||||
(.querySelector "[aria-selected=true]")
|
||||
(.focus)) 16)
|
||||
#(focus-selected-day! @(::identity state) 10) 16)
|
||||
state)
|
||||
:will-unmount (fn [state]
|
||||
(shui/popup-hide!)
|
||||
@@ -405,7 +409,7 @@
|
||||
(let [overdue? (when date (t/after? current-time (t/plus date (t/seconds 59))))]
|
||||
[:div
|
||||
(cond-> {} overdue? (assoc :class "overdue"
|
||||
:title "Overdue"))
|
||||
:title (t :property/overdue)))
|
||||
content])))
|
||||
|
||||
(defn- start-of-local-day [^js d]
|
||||
@@ -421,9 +425,9 @@
|
||||
tomorrow (js/Date. (+ (.getTime today) ms-in-day))
|
||||
yesterday (js/Date. (- (.getTime today) ms-in-day))]
|
||||
(cond
|
||||
(= (.getTime given-date) (.getTime yesterday)) "Yesterday"
|
||||
(= (.getTime given-date) (.getTime today)) "Today"
|
||||
(= (.getTime given-date) (.getTime tomorrow)) "Tomorrow"
|
||||
(= (.getTime given-date) (.getTime yesterday)) (t :date.nlp/yesterday)
|
||||
(= (.getTime given-date) (.getTime today)) (t :date.nlp/today)
|
||||
(= (.getTime given-date) (.getTime tomorrow)) (t :date.nlp/tomorrow)
|
||||
:else nil)))
|
||||
|
||||
(rum/defc datetime-value
|
||||
@@ -623,7 +627,7 @@
|
||||
(remove nil?)
|
||||
(remove #(= :logseq.property/empty-placeholder %))
|
||||
set)
|
||||
clear-value (str "No " (:block/title property))
|
||||
clear-value (t :property/clear-value)
|
||||
clear-value-label [:div.flex.flex-row.items-center.gap-1.text-sm
|
||||
(ui/icon "x" {:size 14})
|
||||
[:div clear-value]]
|
||||
@@ -795,13 +799,7 @@
|
||||
:items options
|
||||
:selected-choices selected-choices
|
||||
:dropdown? dropdown?
|
||||
:input-default-placeholder (cond
|
||||
tags?
|
||||
"Set tags"
|
||||
alias?
|
||||
"Set alias"
|
||||
:else
|
||||
(str "Set " (:block/title property)))
|
||||
:input-default-placeholder (t :property/set-placeholder (db-property/built-in-display-title property t))
|
||||
:show-new-when-not-exact-match? (not
|
||||
(or (and extends-property?
|
||||
(or (contains? (set children-pages) (:db/id block))
|
||||
@@ -953,7 +951,8 @@
|
||||
(remove (fn [b] (contains? #{:logseq.property.repeat/recur-unit.minute :logseq.property.repeat/recur-unit.hour} (:db/ident b)))))]
|
||||
(keep (fn [block]
|
||||
(let [icon (pu/get-block-property-value block :logseq.property/icon)
|
||||
value (db-property/closed-value-content block)]
|
||||
value (or (db-property/built-in-display-title block t)
|
||||
(db-property/closed-value-content block))]
|
||||
{:label (if icon
|
||||
[:div.flex.flex-row.gap-1.items-center
|
||||
(icon-component/icon icon {:color? true})
|
||||
@@ -969,9 +968,9 @@
|
||||
(distinct)))
|
||||
items (->> (cond
|
||||
(= :checkbox type)
|
||||
[{:label "True"
|
||||
[{:label (t :ui/true)
|
||||
:value true}
|
||||
{:label "False"
|
||||
{:label (t :ui/false)
|
||||
:value false}]
|
||||
(= :date type)
|
||||
(map (fn [m] (let [label (:block/title (db/entity (:value m)))]
|
||||
@@ -997,7 +996,7 @@
|
||||
:selected-choices selected-choices
|
||||
:dropdown? dropdown?
|
||||
:show-new-when-not-exact-match? (not (or closed-values? (= :date type)))
|
||||
:input-default-placeholder (str "Set " (:block/title property))
|
||||
:input-default-placeholder (t :property/set-placeholder (db-property/built-in-display-title property t))
|
||||
:extract-chosen-fn :value
|
||||
:extract-fn (fn [x] (or (:label-value x) (:label x)))
|
||||
:content-props content-props
|
||||
@@ -1070,7 +1069,7 @@
|
||||
:style {:min-height 20 :margin-left 3}
|
||||
:on-click #(<create-new-block! block property "")}
|
||||
(when (:class-schema? opts)
|
||||
"Add description")]))))
|
||||
(t :property/add-description))]))))
|
||||
|
||||
(rum/defc property-block-value
|
||||
[value block property page-cp opts]
|
||||
@@ -1156,7 +1155,8 @@
|
||||
(let [eid (if (entity-map? value) (:db/id value) [:block/uuid value])
|
||||
block (or (db/sub-block (:db/id (db/entity eid))) value)
|
||||
property-block? (db-property/property-created-block? block)
|
||||
value' (db-property/closed-value-content block)
|
||||
value' (or (db-property/built-in-display-title block t)
|
||||
(db-property/closed-value-content block))
|
||||
icon (pu/get-block-property-value block :logseq.property/icon)]
|
||||
(cond
|
||||
icon
|
||||
@@ -1177,7 +1177,7 @@
|
||||
[:span.inline-flex.w-full
|
||||
(let [value' (str value')
|
||||
value' (if (string/blank? value')
|
||||
"Empty"
|
||||
(t :ui/empty)
|
||||
value')]
|
||||
(inline-text {} :markdown value'))]))))
|
||||
|
||||
@@ -1215,12 +1215,12 @@
|
||||
(shui/dropdown-menu-item
|
||||
{:key "open"
|
||||
:on-click #(route-handler/redirect-to-page! (:block/uuid value))}
|
||||
(str "Open " (:block/title value)))
|
||||
(t :ui/open-named (:block/title value)))
|
||||
|
||||
(shui/dropdown-menu-item
|
||||
{:key "open sidebar"
|
||||
:on-click #(state/sidebar-add-block! (state/get-current-repo) (:db/id value) :page)}
|
||||
"Open in sidebar")])
|
||||
(t :sidebar.right/open))])
|
||||
{:as-dropdown? true
|
||||
:content-props {:on-click (fn [] (shui/popup-hide!))}
|
||||
:align "start"}))}]
|
||||
@@ -1312,7 +1312,7 @@
|
||||
(and (= :logseq.property/default-value (:db/ident property)) (nil? (:block/title value)))
|
||||
[:div.jtrigger.cursor-pointer.text-sm.px-2
|
||||
{:on-click #(<create-new-block! block property "")}
|
||||
"Set default value"]
|
||||
(t :property/set-default-value)]
|
||||
|
||||
(= (:db/ident property) :logseq.property.publish/published-url)
|
||||
[:div.flex.items-center.gap-2.w-full
|
||||
@@ -1328,7 +1328,7 @@
|
||||
:on-click (fn [e]
|
||||
(util/stop e)
|
||||
(publish-handler/unpublish-page! block))}
|
||||
"Unpublish"))]
|
||||
(t :publish/unpublish)))]
|
||||
|
||||
text-ref-type?
|
||||
(property-block-value value block property page-cp opts)
|
||||
@@ -1600,7 +1600,7 @@
|
||||
[state block property {:keys [show-tooltip? p-block p-property editing?]
|
||||
:as opts}]
|
||||
(ui/catch-error
|
||||
(ui/block-error "Something wrong" {})
|
||||
(ui/block-error (t :sync/something-wrong) {})
|
||||
(let [block-cp (state/get-component :block/blocks-container)
|
||||
opts (merge opts
|
||||
{:page-cp (state/get-component :block/page-cp)
|
||||
@@ -1627,7 +1627,7 @@
|
||||
(not= :logseq.class/Tag
|
||||
(:db/ident (db/entity (:db/id block)))))
|
||||
[:div.flex.flex-row.items-center.gap-1
|
||||
[:div.warning "Self reference"]
|
||||
[:div.warning (t :property/self-reference)]
|
||||
(shui/button {:variant :outline
|
||||
:size :sm
|
||||
:class "h-5"
|
||||
@@ -1635,7 +1635,7 @@
|
||||
(db-property-handler/remove-block-property!
|
||||
(:db/id block)
|
||||
(:db/ident property)))}
|
||||
"Fix it!")]
|
||||
(t :ui/fix))]
|
||||
(let [empty-value? (when (coll? v) (= :logseq.property/empty-placeholder (:db/ident (first v))))
|
||||
closed-values? (seq (:property/closed-values property))
|
||||
value-cp [:div.property-value-inner
|
||||
@@ -1667,5 +1667,5 @@
|
||||
:as-child true}
|
||||
value-cp)
|
||||
(shui/tooltip-content
|
||||
(str "Change " (:block/title property)))))
|
||||
(t :property/change-tooltip (db-property/built-in-display-title property t)))))
|
||||
value-cp))))))
|
||||
|
||||
@@ -12,13 +12,24 @@
|
||||
[frontend.util :as util]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.shui.ui :as shui]
|
||||
[rum.core :as rum]))
|
||||
|
||||
(defn- built-in-custom-query?
|
||||
[title]
|
||||
[{:keys [title-key]}]
|
||||
(let [queries (get-in (state/sub-config) [:default-queries :journals])]
|
||||
(when (seq queries)
|
||||
(boolean (some #(= % title) (map :title queries))))))
|
||||
(boolean
|
||||
(some (fn [built-in-query]
|
||||
(and title-key
|
||||
(= (:title-key built-in-query) title-key)))
|
||||
queries)))))
|
||||
|
||||
(defn- resolve-built-in-query?
|
||||
[built-in-query? q]
|
||||
(boolean
|
||||
(or built-in-query?
|
||||
(built-in-custom-query? q))))
|
||||
|
||||
(defn- grouped-by-page-result?
|
||||
[result group-by-page?]
|
||||
@@ -47,7 +58,7 @@
|
||||
(if @*query-error
|
||||
(do
|
||||
(log/error :exception @*query-error)
|
||||
[:div.warning.my-1 "Query failed: "
|
||||
[:div.warning.my-1 (t :query/error)
|
||||
[:p (.-message @*query-error)]])
|
||||
[:div.custom-query-results
|
||||
(cond
|
||||
@@ -57,8 +68,7 @@
|
||||
(catch :default error
|
||||
(log/error :custom-view-failed {:error error
|
||||
:result result})
|
||||
[:div "Custom view failed: "
|
||||
(str error)]))]
|
||||
[:div (t :query/custom-view-error (str error))]))]
|
||||
(util/hiccup-keywordize result))
|
||||
|
||||
(not (:built-in-query? config))
|
||||
@@ -102,21 +112,33 @@
|
||||
nil
|
||||
|
||||
:else
|
||||
[:div.text-sm.mt-2.opacity-90 (t :search-item/no-result)])])))
|
||||
[:div.text-sm.mt-2.opacity-90 (t :search/no-result)])])))
|
||||
|
||||
(rum/defc query-title
|
||||
[config title {:keys [result-count]}]
|
||||
[config {:keys [title title-key title-icon]} {:keys [result-count]}]
|
||||
(let [inline-text (:inline-text config)]
|
||||
[:div.custom-query-title.flex.justify-between.w-full
|
||||
[:span.title-text (cond
|
||||
(vector? title) title
|
||||
(string? title) (inline-text config
|
||||
(get-in config [:block :block/format] :markdown)
|
||||
title)
|
||||
:else title)]
|
||||
[:span.title-text
|
||||
(cond
|
||||
title-key
|
||||
[:span
|
||||
(when title-icon
|
||||
(shui/tabler-icon title-icon {:class "align-middle pr-1"}))
|
||||
[:span.align-middle (t title-key)]]
|
||||
|
||||
(vector? title)
|
||||
title
|
||||
|
||||
(string? title)
|
||||
(inline-text config
|
||||
(get-in config [:block :block/format] :markdown)
|
||||
title)
|
||||
|
||||
:else
|
||||
title)]
|
||||
(when result-count
|
||||
[:span.opacity-60.text-sm.ml-2.results-count
|
||||
(str result-count (if (> result-count 1) " results" " result"))])]))
|
||||
(t :search/result-count result-count)])]))
|
||||
|
||||
(defn- calculate-collapsed?
|
||||
[current-block current-block-uuid {:keys [collapsed? container-id]}]
|
||||
@@ -167,7 +189,9 @@
|
||||
:group-by-page? (query-result/get-group-by-page q {:table? table?})}]
|
||||
(if (:custom-query? config)
|
||||
;; Don't display recursive results when query blocks are a query result
|
||||
[:code (if dsl-query? (str "Results for " (pr-str query)) "Advanced query results")]
|
||||
[:code (if dsl-query?
|
||||
(t :query/results-for (pr-str query))
|
||||
(t :query/advanced-results))]
|
||||
(when-not (and built-in-query? (empty? result))
|
||||
[:div.custom-query (get config :attr {})
|
||||
(when (and dsl-query? builder) builder)
|
||||
@@ -175,7 +199,7 @@
|
||||
(if built-in-query?
|
||||
[:div {:style {:margin-left 2}}
|
||||
(ui/foldable
|
||||
(query-title config (:title q) {:result-count (count result)})
|
||||
(query-title config q {:result-count (count result)})
|
||||
(fn []
|
||||
(custom-query-inner config q opts))
|
||||
{:default-collapsed? collapsed?
|
||||
@@ -191,7 +215,7 @@
|
||||
[state {:keys [built-in-query?] :as config}
|
||||
{:keys [collapsed?] :as q}]
|
||||
(ui/catch-error
|
||||
(ui/block-error "Query Error:" {:content (:query q)})
|
||||
(ui/block-error (t :query/error) {:content (:query q)})
|
||||
(let [*query-error (:query-error state)
|
||||
current-block-uuid (or (:block/uuid (:block config))
|
||||
(:block/uuid config))
|
||||
@@ -205,7 +229,7 @@
|
||||
:current-block current-block
|
||||
:current-block-uuid current-block-uuid
|
||||
:collapsed? collapsed?'
|
||||
:built-in-query? (built-in-custom-query? (:title q))
|
||||
:built-in-query? (resolve-built-in-query? built-in-query? q)
|
||||
:*query-error *query-error)]
|
||||
(when (or built-in-collapsed? (not collapsed?'))
|
||||
(custom-query* config' q)))))
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[frontend.db.async :as db-async]
|
||||
[frontend.db.model :as db-model]
|
||||
[frontend.db.query-dsl :as query-dsl]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.query.builder :as query-builder]
|
||||
[frontend.mixins :as mixins]
|
||||
@@ -42,6 +43,23 @@
|
||||
(swap! *tree #(query-builder/append-element % loc x))
|
||||
(when toggle? (toggle-fn)))
|
||||
|
||||
(defn- filter-label
|
||||
[value]
|
||||
(case value
|
||||
"tags" (t :property.built-in/tags)
|
||||
"page reference" (t :query.builder/filter-page-reference-label)
|
||||
"property" (t :class.built-in/property)
|
||||
"task" (t :class.built-in/task)
|
||||
"priority" (t :property.built-in/priority)
|
||||
"page" (t :query.builder/filter-page-label)
|
||||
"full text search" (t :query.builder/filter-full-text-search-label)
|
||||
"between" (t :view.filter/operator-between)
|
||||
"sample" (t :query.builder/filter-sample-label)
|
||||
"and" (t :query.builder/operator-and-label)
|
||||
"or" (t :view.filter/or)
|
||||
"not" (t :query.builder/operator-not-label)
|
||||
value))
|
||||
|
||||
(rum/defcs search < (rum/local nil ::input-value)
|
||||
(mixins/event-mixin
|
||||
(fn [state]
|
||||
@@ -63,8 +81,8 @@
|
||||
(let [*input-value (::input-value state)]
|
||||
[:input#query-builder-search.form-input.block.sm:text-sm.sm:leading-5
|
||||
{:auto-focus true
|
||||
:placeholder "Full text search"
|
||||
:aria-label "Full text search"
|
||||
:placeholder (t :search/full-text-placeholder)
|
||||
:aria-label (t :search/full-text-placeholder)
|
||||
:on-change #(reset! *input-value (util/evalue %))}]))
|
||||
|
||||
(defonce *between-dates (atom {}))
|
||||
@@ -102,15 +120,15 @@
|
||||
[state {:keys [tree loc] :as opts}]
|
||||
[:div.between-date.p-4 {:on-pointer-down (fn [e] (util/stop-propagation e))}
|
||||
[:div.flex.flex-row.items-center.gap-2
|
||||
[:div.font-medium "Between: "]
|
||||
(datepicker :start "Start date"
|
||||
(datepicker :start (t :query.builder/between-start-label)
|
||||
(merge opts {:on-select (fn []
|
||||
(when-let [^js end-input (js/document.querySelector ".query-builder-datepicker[data-key=end]")]
|
||||
(when (string/blank? (.-value end-input))
|
||||
(.focus end-input))))}))
|
||||
(datepicker :end "End date" opts)]
|
||||
"~"
|
||||
(datepicker :end (t :query.builder/between-end-label) opts)]
|
||||
[:p.pt-2
|
||||
(ui/button "Submit"
|
||||
(ui/button (t :ui/submit)
|
||||
:on-click (fn []
|
||||
(let [{:keys [start end]} @*between-dates]
|
||||
(when (and start end)
|
||||
@@ -134,7 +152,7 @@
|
||||
[:div.flex.flex-row.justify-between.gap-1.items-center.px-1.pb-1.border-b
|
||||
[:label.opacity-50.cursor.select-none.text-sm
|
||||
{:for "built-in"}
|
||||
"Show built-in properties"]
|
||||
(t :query.builder/show-built-in-properties)]
|
||||
(shui/checkbox
|
||||
{:id "built-in"
|
||||
:value @*private-property?
|
||||
@@ -149,8 +167,9 @@
|
||||
(rum/defc property-value-select-inner
|
||||
< rum/reactive db-mixins/query
|
||||
[*property *private-property? *tree opts loc values]
|
||||
(let [values' (cons {:label "Select all"
|
||||
:value "Select all"}
|
||||
(let [select-all-label (t :view.table/select-all)
|
||||
values' (cons {:label select-all-label
|
||||
:value select-all-label}
|
||||
(map #(hash-map :value (str (:value %))
|
||||
;; Preserve original-value as non-string values like boolean do not display in select
|
||||
:original-value (:value %))
|
||||
@@ -158,7 +177,7 @@
|
||||
(select values'
|
||||
(fn [{:keys [value original-value]}]
|
||||
(let [k (if @*private-property? :private-property :property)
|
||||
x (if (= value "Select all")
|
||||
x (if (= value select-all-label)
|
||||
[k @*property]
|
||||
[k @*property original-value])]
|
||||
(reset! *property nil)
|
||||
@@ -292,14 +311,18 @@
|
||||
(let [*mode (::mode state)
|
||||
filters query-builder/db-based-block-filters
|
||||
filters-and-ops (concat filters query-builder/operators)
|
||||
operator? #(contains? query-builder/operators-set (keyword %))]
|
||||
operator? #(contains? query-builder/operators-set (keyword %))
|
||||
select-items (mapv (fn [value]
|
||||
{:value value
|
||||
:label (filter-label value)})
|
||||
(map name filters-and-ops))]
|
||||
[:div.query-builder-picker
|
||||
(if @*mode
|
||||
(when-not (operator? @*mode)
|
||||
(db-based-query-filter-picker state *tree loc clause opts))
|
||||
[:div
|
||||
(select
|
||||
(map name filters-and-ops)
|
||||
select-items
|
||||
(fn [{:keys [value]}]
|
||||
(cond
|
||||
(operator? value)
|
||||
@@ -307,7 +330,11 @@
|
||||
|
||||
:else
|
||||
(reset! *mode value)))
|
||||
{:input-default-placeholder "Add filter/operator"})])]))
|
||||
{:extract-fn (fn [{:keys [label value]}]
|
||||
(if label
|
||||
(str label " " value)
|
||||
value))
|
||||
:input-default-placeholder (t :query.builder/add-filter-or-operator-placeholder)})])]))
|
||||
|
||||
(rum/defc add-filter
|
||||
[*tree loc clause]
|
||||
@@ -322,7 +349,7 @@
|
||||
(picker *tree loc clause {:toggle-fn #(shui/popup-hide! id)}))
|
||||
{:align :start}))}
|
||||
(ui/icon "plus" {:size 14})
|
||||
(when (= [0] loc) "Filter")))
|
||||
(when (= [0] loc) (t :query.builder/filter))))
|
||||
|
||||
(declare clauses-group)
|
||||
|
||||
@@ -340,7 +367,7 @@
|
||||
(str clause)
|
||||
|
||||
(string? clause)
|
||||
(str "Search: " clause)
|
||||
(t :query.builder/search-label clause)
|
||||
|
||||
(= (keyword f) :page-ref)
|
||||
(ref/->page-ref (uuid->page-title (second clause)))
|
||||
@@ -384,9 +411,9 @@
|
||||
(second end))]
|
||||
(str (cond
|
||||
(= k :block/created-at)
|
||||
"Created"
|
||||
(t :query.builder/created-label)
|
||||
(= k :block/updated-at)
|
||||
"Updated"
|
||||
(t :query.builder/updated-label)
|
||||
:else
|
||||
(or (:block/title (db/entity k)) (name k)))
|
||||
" " start
|
||||
@@ -403,7 +430,7 @@
|
||||
(symbol? (last clause)))
|
||||
(name (last clause))
|
||||
(second (last clause)))]
|
||||
(str "between: " (uuid->page-title start) " ~ " (uuid->page-title end)))
|
||||
(t :query.builder/between-journal-label (uuid->page-title start) (uuid->page-title end)))
|
||||
|
||||
(contains? #{:task :priority} (keyword f))
|
||||
(str (name f) ": "
|
||||
@@ -423,24 +450,24 @@
|
||||
(rum/defc clause-inner
|
||||
[*tree loc clause & {:keys [operator?]}]
|
||||
(let [popup [:div.p-4.flex.flex-col.gap-2
|
||||
[:a {:title "Delete"
|
||||
[:a {:title (t :ui/delete)
|
||||
:on-click (fn []
|
||||
(swap! *tree (fn [q]
|
||||
(let [loc' (if operator? (vec (butlast loc)) loc)]
|
||||
(query-builder/remove-element q loc'))))
|
||||
(shui/popup-hide!))}
|
||||
"Delete"]
|
||||
(t :ui/delete)]
|
||||
|
||||
(when operator?
|
||||
[:a {:title "Unwrap this operator"
|
||||
[:a {:title (t :query.builder/unwrap-operator)
|
||||
:on-click (fn []
|
||||
(swap! *tree (fn [q]
|
||||
(let [loc' (vec (butlast loc))]
|
||||
(query-builder/unwrap-operator q loc'))))
|
||||
(shui/popup-hide!))}
|
||||
"Unwrap"])
|
||||
(t :query.builder/unwrap-operator)])
|
||||
|
||||
[:div.font-medium.text-sm "Wrap this filter with: "]
|
||||
[:div.font-medium.text-sm (t :query.builder/wrap-filter-with-label)]
|
||||
[:div.flex.flex-row.gap-2
|
||||
(for [op query-builder/operators]
|
||||
(ui/button (string/upper-case (name op))
|
||||
@@ -454,7 +481,7 @@
|
||||
|
||||
(when operator?
|
||||
[:div
|
||||
[:div.font-medium.text-sm "Replace with: "]
|
||||
[:div.font-medium.text-sm (t :query.builder/replace-with-label)]
|
||||
[:div.flex.flex-row.gap-2
|
||||
(for [op (remove #{(keyword (string/lower-case clause))} query-builder/operators)]
|
||||
(ui/button (string/upper-case (name op))
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
[:div.query-result.w-full
|
||||
(views/view
|
||||
{:config (assoc {:custom-query? true} :sidebar? (:sidebar? config))
|
||||
:title-key :views.table/live-query-title
|
||||
:title-key :view.table/live-query-title
|
||||
:view-entity view-entity
|
||||
:view-feature-type :query-result
|
||||
:data ids
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user