Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
11
deps/outliner/.carve/config.edn
vendored
Normal file
11
deps/outliner/.carve/config.edn
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{:paths ["src"]
|
||||
:api-namespaces [logseq.outliner.datascript-report
|
||||
logseq.outliner.pipeline
|
||||
logseq.outliner.cli
|
||||
logseq.outliner.core
|
||||
logseq.outliner.db-pipeline
|
||||
logseq.outliner.property
|
||||
logseq.outliner.op.construct
|
||||
logseq.outliner.recycle
|
||||
logseq.outliner.tree]
|
||||
:report {:format :ignore}}
|
||||
14
deps/outliner/.carve/ignore
vendored
Normal file
14
deps/outliner/.carve/ignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
;; API fn
|
||||
logseq.outliner.op/apply-ops!
|
||||
;; API fn
|
||||
logseq.outliner.op/register-op-handlers!
|
||||
;; API fn
|
||||
logseq.outliner.page/delete!
|
||||
;; API fn
|
||||
logseq.outliner.page/create!
|
||||
;; Symbol
|
||||
logseq.outliner.recycle/gc-interval-ms
|
||||
;; API fn
|
||||
logseq.outliner.recycle/restore!
|
||||
;; API fn
|
||||
logseq.outliner.recycle/gc!
|
||||
16
deps/outliner/.clj-kondo/config.edn
vendored
Normal file
16
deps/outliner/.clj-kondo/config.edn
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{:linters
|
||||
{:aliased-namespace-symbol {:level :warning}
|
||||
:namespace-name-mismatch {:level :warning}
|
||||
:used-underscored-binding {:level :warning}
|
||||
:shadowed-var {:level :warning}
|
||||
|
||||
:consistent-alias
|
||||
{:aliases {clojure.string string
|
||||
logseq.outliner.core outliner-core
|
||||
logseq.outliner.page outliner-page
|
||||
logseq.outliner.op outliner-op
|
||||
logseq.outliner.validate outliner-validate
|
||||
logseq.outliner.pipeline outliner-pipeline
|
||||
logseq.outliner.datascript-report ds-report}}}
|
||||
:skip-comments true
|
||||
:output {:progress true}}
|
||||
1
deps/outliner/.gitignore
vendored
Normal file
1
deps/outliner/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.clj-kondo/.cache
|
||||
50
deps/outliner/README.md
vendored
Normal file
50
deps/outliner/README.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
## Description
|
||||
|
||||
This library provides outliner operation related functionality. This library is
|
||||
compatible with ClojureScript and with
|
||||
node/[nbb-logseq](https://github.com/logseq/nbb-logseq) to respectively provide
|
||||
frontend and commandline functionality.
|
||||
|
||||
## API
|
||||
|
||||
This library is under the parent namespace `logseq.outliner`. This library
|
||||
provides two main namespaces: `logseq.outliner.datascript-report` and `logseq.outliner.pipeline`.
|
||||
|
||||
## Usage
|
||||
|
||||
See the frontend for cljs usage.
|
||||
|
||||
## Dev
|
||||
|
||||
This follows the practices that [the Logseq frontend
|
||||
follows](/docs/dev-practices.md). Most of the same linters are used, with
|
||||
configurations that are specific to this library. See [this library's CI
|
||||
file](/.github/workflows/deps-outliner.yml) for linting examples.
|
||||
|
||||
### Setup
|
||||
|
||||
To run linters and tests, you'll want to install pnpm dependencies once:
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
This step is not needed if you're just running the frontend application.
|
||||
|
||||
### Testing
|
||||
|
||||
Testing is done with nbb-logseq and
|
||||
[nbb-test-runner](https://github.com/nextjournal/nbb-test-runner). Some basic
|
||||
usage:
|
||||
|
||||
```
|
||||
# Run all tests
|
||||
$ pnpm test
|
||||
# List available options
|
||||
$ pnpm test -H
|
||||
# Run tests with :focus metadata flag
|
||||
$ pnpm test -i focus
|
||||
```
|
||||
|
||||
### Managing dependencies
|
||||
|
||||
See [standard nbb/cljs library advice in graph-parser](/deps/graph-parser/README.md#managing-dependencies).
|
||||
32
deps/outliner/bb.edn
vendored
Normal file
32
deps/outliner/bb.edn
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{:min-bb-version "1.0.168"
|
||||
:deps
|
||||
{logseq/bb-tasks
|
||||
#_{:local/root "../../../bb-tasks"}
|
||||
{:git/url "https://github.com/logseq/bb-tasks"
|
||||
:git/sha "7cc218286c03f093996539b8195de56a0ec5df6a"}}
|
||||
|
||||
:pods
|
||||
{clj-kondo/clj-kondo {:version "2026.04.15"}}
|
||||
|
||||
:tasks
|
||||
{test:load-all-namespaces-with-nbb
|
||||
logseq.bb-tasks.nbb.test/load-all-namespaces
|
||||
|
||||
lint:large-vars
|
||||
logseq.bb-tasks.lint.large-vars/-main
|
||||
|
||||
lint:carve
|
||||
logseq.bb-tasks.lint.carve/-main
|
||||
|
||||
lint:ns-docstrings
|
||||
logseq.bb-tasks.lint.ns-docstrings/-main
|
||||
|
||||
lint:minimize-public-vars
|
||||
logseq.bb-tasks.lint.minimize-public-vars/-main}
|
||||
|
||||
:tasks/config
|
||||
{:large-vars
|
||||
{:max-lines-count 55
|
||||
;; TODO: Remove this once outliner.core-test is in dep and these
|
||||
;; fns can be easily refactored
|
||||
:metadata-exceptions #{:large-vars/cleanup-todo}}}}
|
||||
19
deps/outliner/deps.edn
vendored
Normal file
19
deps/outliner/deps.edn
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{:deps
|
||||
;; These nbb-logseq deps are kept in sync with https://github.com/logseq/nbb-logseq/blob/main/bb.edn
|
||||
{datascript/datascript {:git/url "https://github.com/logseq/datascript" ;; fork
|
||||
:sha "3f141af97b70e1f14c65eaa119acd822ebece37e"}
|
||||
;; datascript/datascript {:local/root "../../../../datascript"}
|
||||
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
|
||||
|
||||
;; Any other deps should be added here and to nbb.edn
|
||||
logseq/db {:local/root "../db"}
|
||||
logseq/graph-parser {:local/root "../graph-parser"}
|
||||
metosin/malli {:git/url "https://github.com/metosin/malli"
|
||||
:sha "52ea58a36ff5172b38dfc526ca638afa7226a4a0"}
|
||||
;; stubbed via logseq.common.log
|
||||
com.lambdaisland/glogi {:git/url "https://github.com/lambdaisland/glogi"
|
||||
:git/sha "30328a045141717aadbbb693465aed55f0904976"}}
|
||||
:aliases
|
||||
{:clj-kondo
|
||||
{:replace-deps {clj-kondo/clj-kondo {:mvn/version "2026.04.15"}}
|
||||
:main-opts ["-m" "clj-kondo.main"]}}}
|
||||
9
deps/outliner/nbb.edn
vendored
Normal file
9
deps/outliner/nbb.edn
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{:paths ["src"]
|
||||
:deps
|
||||
{logseq/db
|
||||
{:local/root "../db"}
|
||||
logseq/graph-parser
|
||||
{:local/root "../graph-parser"}
|
||||
metosin/malli {:git/url "https://github.com/metosin/malli"
|
||||
:sha "52ea58a36ff5172b38dfc526ca638afa7226a4a0"}
|
||||
io.github.nextjournal/nbb-test-runner {:git/sha "b379325cfa5a3306180649da5de3bf5166414e71"}}}
|
||||
16
deps/outliner/package.json
vendored
Normal file
16
deps/outliner/package.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@logseq/outliner",
|
||||
"version": "1.0.0",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@logseq/nbb-logseq": "git+https://github.com/logseq/nbb-logseq.git#feat-db-v34"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"mldoc": "^1.5.9"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "pnpm exec nbb-logseq -cp test -m nextjournal.test-runner"
|
||||
}
|
||||
}
|
||||
691
deps/outliner/pnpm-lock.yaml
generated
vendored
Normal file
691
deps/outliner/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,691 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
better-sqlite3:
|
||||
specifier: ^12.8.0
|
||||
version: 12.8.0
|
||||
mldoc:
|
||||
specifier: ^1.5.9
|
||||
version: 1.5.9
|
||||
devDependencies:
|
||||
'@logseq/nbb-logseq':
|
||||
specifier: git+https://github.com/logseq/nbb-logseq.git#feat-db-v34
|
||||
version: https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb
|
||||
|
||||
packages:
|
||||
|
||||
'@logseq/nbb-logseq@https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb':
|
||||
resolution: {tarball: https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb}
|
||||
version: 1.2.173-feat-db-v34
|
||||
engines: {node: '>=20.10.0'}
|
||||
hasBin: true
|
||||
|
||||
ansi-regex@2.1.1:
|
||||
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ansi-regex@3.0.1:
|
||||
resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
better-sqlite3@12.8.0:
|
||||
resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==}
|
||||
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
|
||||
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
cliui@4.1.0:
|
||||
resolution: {integrity: sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==}
|
||||
|
||||
code-point-at@1.1.0:
|
||||
resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
cross-spawn@6.0.6:
|
||||
resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==}
|
||||
engines: {node: '>=4.8'}
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
detect-libc@2.0.3:
|
||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
end-of-stream@1.4.4:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
|
||||
execa@1.0.0:
|
||||
resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
find-up@3.0.0:
|
||||
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
get-caller-file@1.0.3:
|
||||
resolution: {integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==}
|
||||
|
||||
get-stream@4.1.0:
|
||||
resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
import-meta-resolve@4.1.0:
|
||||
resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
invert-kv@2.0.0:
|
||||
resolution: {integrity: sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
is-fullwidth-code-point@1.0.0:
|
||||
resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-fullwidth-code-point@2.0.0:
|
||||
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
is-stream@1.1.0:
|
||||
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
lcid@2.0.0:
|
||||
resolution: {integrity: sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
locate-path@3.0.0:
|
||||
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
map-age-cleaner@0.1.3:
|
||||
resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
mem@4.3.0:
|
||||
resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
mimic-fn@2.1.0:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
mldoc@1.5.9:
|
||||
resolution: {integrity: sha512-87FQ7hseS87tsk+VdpIigpu8LH+GwmbbFgpxgFwvnbH5oOjmIrc47laH4Dyggzqiy8/vMjDHkl7vsId0eXhCDQ==}
|
||||
hasBin: true
|
||||
|
||||
napi-build-utils@1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
|
||||
nice-try@1.0.5:
|
||||
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
|
||||
|
||||
node-abi@3.71.0:
|
||||
resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
npm-run-path@2.0.2:
|
||||
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
number-is-nan@1.0.1:
|
||||
resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
os-locale@3.1.0:
|
||||
resolution: {integrity: sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-defer@1.0.0:
|
||||
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-finally@1.0.0:
|
||||
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-is-promise@2.1.0:
|
||||
resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-locate@3.0.0:
|
||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
path-exists@3.0.0:
|
||||
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
path-key@2.0.1:
|
||||
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
prebuild-install@7.1.2:
|
||||
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||
hasBin: true
|
||||
|
||||
pump@3.0.2:
|
||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||
|
||||
rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@1.0.1:
|
||||
resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
semver@5.7.2:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.6.3:
|
||||
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
shebang-command@1.2.0:
|
||||
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
shebang-regex@1.0.0:
|
||||
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
signal-exit@3.0.7:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
|
||||
simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
|
||||
simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
||||
string-width@1.0.2:
|
||||
resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
string-width@2.1.1:
|
||||
resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-ansi@3.0.1:
|
||||
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
strip-ansi@4.0.0:
|
||||
resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
strip-eof@1.0.0:
|
||||
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
tar-fs@2.1.1:
|
||||
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
|
||||
|
||||
tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which@1.3.1:
|
||||
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@2.1.0:
|
||||
resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
yargs-parser@11.1.1:
|
||||
resolution: {integrity: sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==}
|
||||
|
||||
yargs@12.0.5:
|
||||
resolution: {integrity: sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@logseq/nbb-logseq@https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb':
|
||||
dependencies:
|
||||
import-meta-resolve: 4.1.0
|
||||
|
||||
ansi-regex@2.1.1: {}
|
||||
|
||||
ansi-regex@3.0.1: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-sqlite3@12.8.0:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.2
|
||||
|
||||
bindings@1.5.0:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
buffer@5.7.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
cliui@4.1.0:
|
||||
dependencies:
|
||||
string-width: 2.1.1
|
||||
strip-ansi: 4.0.0
|
||||
wrap-ansi: 2.1.0
|
||||
|
||||
code-point-at@1.1.0: {}
|
||||
|
||||
cross-spawn@6.0.6:
|
||||
dependencies:
|
||||
nice-try: 1.0.5
|
||||
path-key: 2.0.1
|
||||
semver: 5.7.2
|
||||
shebang-command: 1.2.0
|
||||
which: 1.3.1
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
||||
deep-extend@0.6.0: {}
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
|
||||
end-of-stream@1.4.4:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
execa@1.0.0:
|
||||
dependencies:
|
||||
cross-spawn: 6.0.6
|
||||
get-stream: 4.1.0
|
||||
is-stream: 1.1.0
|
||||
npm-run-path: 2.0.2
|
||||
p-finally: 1.0.0
|
||||
signal-exit: 3.0.7
|
||||
strip-eof: 1.0.0
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
file-uri-to-path@1.0.0: {}
|
||||
|
||||
find-up@3.0.0:
|
||||
dependencies:
|
||||
locate-path: 3.0.0
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
get-caller-file@1.0.3: {}
|
||||
|
||||
get-stream@4.1.0:
|
||||
dependencies:
|
||||
pump: 3.0.2
|
||||
|
||||
github-from-package@0.0.0: {}
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
import-meta-resolve@4.1.0: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ini@1.3.8: {}
|
||||
|
||||
invert-kv@2.0.0: {}
|
||||
|
||||
is-fullwidth-code-point@1.0.0:
|
||||
dependencies:
|
||||
number-is-nan: 1.0.1
|
||||
|
||||
is-fullwidth-code-point@2.0.0: {}
|
||||
|
||||
is-stream@1.1.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
lcid@2.0.0:
|
||||
dependencies:
|
||||
invert-kv: 2.0.0
|
||||
|
||||
locate-path@3.0.0:
|
||||
dependencies:
|
||||
p-locate: 3.0.0
|
||||
path-exists: 3.0.0
|
||||
|
||||
map-age-cleaner@0.1.3:
|
||||
dependencies:
|
||||
p-defer: 1.0.0
|
||||
|
||||
mem@4.3.0:
|
||||
dependencies:
|
||||
map-age-cleaner: 0.1.3
|
||||
mimic-fn: 2.1.0
|
||||
p-is-promise: 2.1.0
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
mldoc@1.5.9:
|
||||
dependencies:
|
||||
yargs: 12.0.5
|
||||
|
||||
napi-build-utils@1.0.2: {}
|
||||
|
||||
nice-try@1.0.5: {}
|
||||
|
||||
node-abi@3.71.0:
|
||||
dependencies:
|
||||
semver: 7.6.3
|
||||
|
||||
npm-run-path@2.0.2:
|
||||
dependencies:
|
||||
path-key: 2.0.1
|
||||
|
||||
number-is-nan@1.0.1: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
os-locale@3.1.0:
|
||||
dependencies:
|
||||
execa: 1.0.0
|
||||
lcid: 2.0.0
|
||||
mem: 4.3.0
|
||||
|
||||
p-defer@1.0.0: {}
|
||||
|
||||
p-finally@1.0.0: {}
|
||||
|
||||
p-is-promise@2.1.0: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
p-locate@3.0.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
path-exists@3.0.0: {}
|
||||
|
||||
path-key@2.0.1: {}
|
||||
|
||||
prebuild-install@7.1.2:
|
||||
dependencies:
|
||||
detect-libc: 2.0.3
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 1.0.2
|
||||
node-abi: 3.71.0
|
||||
pump: 3.0.2
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
|
||||
pump@3.0.2:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
|
||||
rc@1.2.8:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-main-filename@1.0.1: {}
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
semver@5.7.2: {}
|
||||
|
||||
semver@7.6.3: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
shebang-command@1.2.0:
|
||||
dependencies:
|
||||
shebang-regex: 1.0.0
|
||||
|
||||
shebang-regex@1.0.0: {}
|
||||
|
||||
signal-exit@3.0.7: {}
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
|
||||
simple-get@4.0.1:
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
|
||||
string-width@1.0.2:
|
||||
dependencies:
|
||||
code-point-at: 1.1.0
|
||||
is-fullwidth-code-point: 1.0.0
|
||||
strip-ansi: 3.0.1
|
||||
|
||||
string-width@2.1.1:
|
||||
dependencies:
|
||||
is-fullwidth-code-point: 2.0.0
|
||||
strip-ansi: 4.0.0
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-ansi@3.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 2.1.1
|
||||
|
||||
strip-ansi@4.0.0:
|
||||
dependencies:
|
||||
ansi-regex: 3.0.1
|
||||
|
||||
strip-eof@1.0.0: {}
|
||||
|
||||
strip-json-comments@2.0.1: {}
|
||||
|
||||
tar-fs@2.1.1:
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.2
|
||||
tar-stream: 2.2.0
|
||||
|
||||
tar-stream@2.2.0:
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.4
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which@1.3.1:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrap-ansi@2.1.0:
|
||||
dependencies:
|
||||
string-width: 1.0.2
|
||||
strip-ansi: 3.0.1
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
yargs-parser@11.1.1:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs@12.0.5:
|
||||
dependencies:
|
||||
cliui: 4.1.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 3.0.0
|
||||
get-caller-file: 1.0.3
|
||||
os-locale: 3.1.0
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 1.0.1
|
||||
set-blocking: 2.0.0
|
||||
string-width: 2.1.1
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 11.1.1
|
||||
38
deps/outliner/script/transact.cljs
vendored
Normal file
38
deps/outliner/script/transact.cljs
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
(ns transact
|
||||
"This script generically runs transactions against the queried blocks"
|
||||
(:require [clojure.edn :as edn]
|
||||
[datascript.core :as d]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.sqlite-cli :as sqlite-cli]
|
||||
[logseq.db.frontend.rules :as rules]
|
||||
[logseq.outliner.db-pipeline :as db-pipeline]
|
||||
[nbb.core :as nbb]))
|
||||
|
||||
(defn -main [args]
|
||||
(when (< (count args) 3)
|
||||
(println "Usage: $0 GRAPH-DIR QUERY TRANSACT-FN")
|
||||
(js/process.exit 1))
|
||||
(let [[graph-dir query* transact-fn*] args
|
||||
dry-run? (contains? (set args) "-n")
|
||||
open-db-args (sqlite-cli/->open-db-args graph-dir)
|
||||
db-name (if (= 1 (count open-db-args)) (first open-db-args) (second open-db-args))
|
||||
conn (apply sqlite-cli/open-db! open-db-args)
|
||||
;; find blocks to update
|
||||
query (into (edn/read-string query*) [:in '$ '%]) ;; assumes no :in are in queries
|
||||
transact-fn (edn/read-string transact-fn*)
|
||||
blocks-to-update (mapv first (d/q query @conn (rules/extract-rules rules/db-query-dsl-rules)))
|
||||
;; TODO: Use sci eval when it's available in nbb-logseq
|
||||
update-tx (mapv (fn [id] (eval (list transact-fn id)))
|
||||
blocks-to-update)]
|
||||
(if dry-run?
|
||||
(do (println "Would update" (count blocks-to-update) "blocks with the following tx:")
|
||||
(prn update-tx)
|
||||
(println "With the following blocks updated:")
|
||||
(prn (map #(select-keys (d/entity @conn %) [:block/name :block/title]) blocks-to-update)))
|
||||
(do
|
||||
(db-pipeline/add-listener conn)
|
||||
(ldb/transact! conn update-tx)
|
||||
(println "Updated" (count update-tx) "block(s) for graph" (str db-name "!"))))))
|
||||
|
||||
(when (= nbb/*file* (nbb/invoked-file))
|
||||
(-main *command-line-args*))
|
||||
73
deps/outliner/src/logseq/outliner/cli.cljs
vendored
Normal file
73
deps/outliner/src/logseq/outliner/cli.cljs
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
(ns ^:node-only logseq.outliner.cli
|
||||
"Primary ns for outliner CLI fns"
|
||||
(:require ["child_process" :as child-process]
|
||||
["fs" :as fs]
|
||||
["path" :as node-path]
|
||||
[borkdude.rewrite-edn :as rewrite]
|
||||
[clojure.string :as string]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.sqlite-cli :as sqlite-cli]
|
||||
[logseq.db.sqlite.build :as sqlite-build]
|
||||
[logseq.db.sqlite.create-graph :as sqlite-create-graph]
|
||||
[logseq.outliner.db-pipeline :as db-pipeline]))
|
||||
|
||||
(defn- find-on-classpath [classpath rel-path]
|
||||
(some (fn [dir]
|
||||
(let [f (node-path/join dir rel-path)]
|
||||
(when (fs/existsSync f) f)))
|
||||
(string/split classpath #":")))
|
||||
|
||||
(defn- pretty-print-merge
|
||||
"Merge map into string while preversing whitespace"
|
||||
[s m]
|
||||
(-> (reduce (fn [acc [k v]]
|
||||
(rewrite/assoc acc k v))
|
||||
(rewrite/parse-string s)
|
||||
m)
|
||||
str))
|
||||
|
||||
(defn- get-git-sha
|
||||
[]
|
||||
(let [res (child-process/spawnSync "git"
|
||||
#js ["rev-parse" "--short" "HEAD"]
|
||||
#js {})]
|
||||
(when (zero? (.-status res))
|
||||
(string/trim (str (.-stdout res))))))
|
||||
|
||||
(defn- setup-init-data
|
||||
"Setup initial data same as frontend.handler.repo/create-db"
|
||||
[conn {:keys [additional-config classpath import-type]
|
||||
:or {import-type :cli/default}}]
|
||||
(let [config-content
|
||||
(cond-> (or (some-> (find-on-classpath classpath "templates/config.edn") fs/readFileSync str)
|
||||
(do (println "Setting graph's config to empty since no templates/config.edn was found.")
|
||||
"{}"))
|
||||
additional-config
|
||||
(pretty-print-merge additional-config))
|
||||
git-sha (get-git-sha)]
|
||||
(ldb/transact! conn (sqlite-create-graph/build-db-initial-data config-content
|
||||
(merge {:import-type import-type}
|
||||
(when git-sha {:graph-git-sha git-sha}))))))
|
||||
|
||||
(defn init-conn
|
||||
"Create sqlite DB, initialize datascript connection and sync listener and then
|
||||
transacts initial data. Takes the following options:
|
||||
* :additional-config - Additional config map to merge into repo config.edn
|
||||
* :classpath - A java classpath string i.e. paths delimited by ':'. Used to find default config.edn
|
||||
that comes with Logseq"
|
||||
[& args*]
|
||||
(let [[args opts] (if (map? (last args*))
|
||||
[(butlast args*) (last args*)]
|
||||
[args* {}])
|
||||
;; Only mkdir when a dir and db-name are passed
|
||||
_ (when (= 2 (count args))
|
||||
(fs/mkdirSync (apply node-path/join args) #js {:recursive true}))
|
||||
;; Same order as frontend.db.conn/start!
|
||||
conn (apply sqlite-cli/open-db! args)]
|
||||
(db-pipeline/add-listener conn)
|
||||
(setup-init-data conn opts)
|
||||
conn))
|
||||
|
||||
(def build-blocks-tx
|
||||
"An alias for build-blocks-tx to specify default options for this ns"
|
||||
sqlite-build/build-blocks-tx)
|
||||
1321
deps/outliner/src/logseq/outliner/core.cljs
vendored
Normal file
1321
deps/outliner/src/logseq/outliner/core.cljs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10
deps/outliner/src/logseq/outliner/datascript.cljs
vendored
Normal file
10
deps/outliner/src/logseq/outliner/datascript.cljs
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
(ns logseq.outliner.datascript
|
||||
"Provides fns related to batch txs state")
|
||||
|
||||
(defn new-outliner-txs-state [] (atom []))
|
||||
|
||||
(defn outliner-txs-state?
|
||||
[state]
|
||||
(and
|
||||
(instance? cljs.core/Atom state)
|
||||
(coll? @state)))
|
||||
45
deps/outliner/src/logseq/outliner/datascript_report.cljs
vendored
Normal file
45
deps/outliner/src/logseq/outliner/datascript_report.cljs
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
(ns logseq.outliner.datascript-report
|
||||
"Datascript fns related to getting data from a connection listener's tx-report"
|
||||
(:require [clojure.set :as set]
|
||||
[datascript.core :as d]))
|
||||
|
||||
(def ^:private keys-of-deleted-entity 1)
|
||||
|
||||
(defn- get-entity-from-db-after-or-before
|
||||
"Get the entity from db after if possible; otherwise get entity from db before
|
||||
Useful for fetching deleted elements"
|
||||
[db-before db-after db-id]
|
||||
(let [r (d/pull db-after '[*] db-id)]
|
||||
(if (= keys-of-deleted-entity (count r))
|
||||
;; block has been deleted
|
||||
(d/pull db-before '[*] db-id)
|
||||
r)))
|
||||
|
||||
(defn get-blocks-and-pages
|
||||
"Calculate updated blocks and pages based on the db-before and db-after from tx-report"
|
||||
[{:keys [db-before db-after tx-data tx-meta]}]
|
||||
(let [updated-db-ids (-> (mapv first tx-data) (set))
|
||||
result (reduce
|
||||
(fn [acc x]
|
||||
(let [block-entity
|
||||
(get-entity-from-db-after-or-before db-before db-after x)
|
||||
page-entity
|
||||
(when-let [page-id (-> block-entity :block/page :db/id)]
|
||||
(get-entity-from-db-after-or-before db-before db-after page-id))]
|
||||
(cond-> acc
|
||||
(some? block-entity)
|
||||
(update :blocks conj block-entity)
|
||||
|
||||
(some? page-entity)
|
||||
(update :pages conj page-entity))))
|
||||
{:blocks #{}
|
||||
:pages #{}}
|
||||
updated-db-ids)
|
||||
;; updated pages logged in tx-meta (usually from move op)
|
||||
tx-meta-pages (->> [(:from-page tx-meta) (:target-page tx-meta)]
|
||||
(remove nil?)
|
||||
(map #(get-entity-from-db-after-or-before db-before db-after %))
|
||||
(set))]
|
||||
(if (seq tx-meta-pages)
|
||||
(update result :pages set/union tx-meta-pages)
|
||||
result)))
|
||||
23
deps/outliner/src/logseq/outliner/db_pipeline.cljs
vendored
Normal file
23
deps/outliner/src/logseq/outliner/db_pipeline.cljs
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
(ns logseq.outliner.db-pipeline
|
||||
"This ns provides a datascript listener for DB graphs that is useful for CLIs
|
||||
and testing (since it doesn't assume a frontend worker exists). The listener adds
|
||||
additional changes that the frontend also adds per transact. Missing features
|
||||
from frontend.worker.pipeline including:
|
||||
* Deleted blocks don't update effected :block/tx-id
|
||||
* Delete empty property parent"
|
||||
(:require [datascript.core :as d]
|
||||
[logseq.outliner.pipeline :as outliner-pipeline]))
|
||||
|
||||
(defn- invoke-hooks
|
||||
"Modified copy of frontend.worker.pipeline/invoke-hooks that handles new DB graphs but
|
||||
doesn't handle updating DB graphs well yet e.g. doesn't handle :block/tx-id"
|
||||
[conn tx-report]
|
||||
(when-not (:transact-new-graph-refs? (:tx-meta tx-report))
|
||||
;; TODO: Handle block edits with separate :block/refs rebuild as deleting property values is buggy
|
||||
(outliner-pipeline/transact-new-db-graph-refs conn tx-report)))
|
||||
|
||||
(defn ^:api add-listener
|
||||
"Adds a listener to the datascript connection to add additional changes from outliner.pipeline"
|
||||
[conn]
|
||||
(d/listen! conn :pipeline-updates (fn pipeline-updates [tx-report]
|
||||
(invoke-hooks conn tx-report))))
|
||||
450
deps/outliner/src/logseq/outliner/op.cljs
vendored
Normal file
450
deps/outliner/src/logseq/outliner/op.cljs
vendored
Normal file
@@ -0,0 +1,450 @@
|
||||
(ns logseq.outliner.op
|
||||
"Transact outliner ops"
|
||||
(:require [clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.defkeywords :refer [defkeywords]]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.sqlite.export :as sqlite-export]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.template :as outliner-template]
|
||||
[logseq.outliner.page :as outliner-page]
|
||||
[logseq.outliner.property :as outliner-property]
|
||||
[logseq.outliner.recycle :as outliner-recycle]
|
||||
[logseq.outliner.transaction :as outliner-tx]
|
||||
[malli.core :as m]
|
||||
[logseq.outliner.op.construct :as op-construct]))
|
||||
|
||||
(defkeywords
|
||||
::missing-parent-original {:doc "The linked-reference parent supplied to an outdent no longer exists."})
|
||||
|
||||
(def ^:private ^:large-vars/data-var op-schema
|
||||
[:multi {:dispatch first}
|
||||
;; blocks
|
||||
[:save-block
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block ::option]]]]
|
||||
[:insert-blocks
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::blocks ::block-id ::option]]]]
|
||||
[:apply-template
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-id ::block-id ::option]]]]
|
||||
[:delete-blocks
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-ids ::option]]]]
|
||||
[:move-blocks
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-ids ::block-id ::option]]]]
|
||||
[:move-blocks-up-down
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-ids :boolean]]]]
|
||||
[:indent-outdent-blocks
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-ids :boolean ::option]]]]
|
||||
[:collapse-expand-blocks
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::blocks ::option]]]]
|
||||
|
||||
;; properties
|
||||
[:upsert-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::maybe-property-id ::schema ::option]]]]
|
||||
[:set-block-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-id ::property-id ::value]]]]
|
||||
[:remove-block-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-id ::property-id]]]]
|
||||
[:delete-property-value
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-id ::property-id ::value]]]]
|
||||
[:batch-delete-property-value
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-ids ::property-id ::value]]]]
|
||||
[:create-property-text-block
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::maybe-block-id ::property-id ::value ::option]]]]
|
||||
[:collapse-expand-block-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-id ::property-id :boolean]]]]
|
||||
[:batch-set-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-ids ::property-id ::value ::option]]]]
|
||||
[:batch-remove-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::block-ids ::property-id]]]]
|
||||
[:class-add-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::class-id ::property-id]]]]
|
||||
[:class-remove-property
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::class-id ::property-id]]]]
|
||||
[:upsert-closed-value
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::property-id ::option]]]]
|
||||
[:delete-closed-value
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::property-id ::block-id]]]]
|
||||
[:add-existing-values-to-closed-values
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::property-id ::values]]]]
|
||||
|
||||
[:batch-import-edn
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::import-edn ::option]]]]
|
||||
|
||||
;; transact
|
||||
[:transact
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::tx-data ::tx-meta]]]]
|
||||
|
||||
;; page ops
|
||||
[:create-page
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::title ::option]]]]
|
||||
|
||||
[:rename-page
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::uuid ::title]]]]
|
||||
|
||||
[:delete-page
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::uuid ::option]]]]
|
||||
|
||||
[:restore-recycled
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::uuid]]]]
|
||||
|
||||
[:recycle-delete-permanently
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::uuid]]]]
|
||||
|
||||
[:toggle-reaction
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::uuid ::emoji-id ::maybe-uuid]]]]])
|
||||
|
||||
(def ^:private ops-schema
|
||||
[:schema {:registry {::block map?
|
||||
::schema map?
|
||||
::block-id uuid?
|
||||
::maybe-block-id [:maybe ::block-id]
|
||||
::block-ids [:sequential ::block-id]
|
||||
::class-id ::block-id
|
||||
::emoji-id string?
|
||||
::property-id qualified-keyword?
|
||||
::maybe-property-id [:maybe ::property-id]
|
||||
::maybe-uuid [:maybe :uuid]
|
||||
::value :any
|
||||
::values [:sequential ::value]
|
||||
::option [:maybe map?]
|
||||
::import-edn map?
|
||||
::blocks [:sequential ::block]
|
||||
::uuid uuid?
|
||||
::title string?
|
||||
::tx-data [:sequential :any]
|
||||
::tx-meta [:maybe map?]}}
|
||||
[:sequential op-schema]])
|
||||
|
||||
(def ^:private ops-validator (m/validator ops-schema))
|
||||
|
||||
(defn- reaction-user-id
|
||||
[reaction]
|
||||
(:db/id (:logseq.property/created-by-ref reaction)))
|
||||
|
||||
(defn- toggle-reaction!
|
||||
[conn target-uuid emoji-id user-uuid]
|
||||
(when-let [target (d/entity @conn [:block/uuid target-uuid])]
|
||||
(let [user-id (when user-uuid
|
||||
(:db/id (d/entity @conn [:block/uuid user-uuid])))
|
||||
reactions (:logseq.property.reaction/_target target)
|
||||
match? (fn [reaction]
|
||||
(and (= emoji-id (:logseq.property.reaction/emoji-id reaction))
|
||||
(if user-id
|
||||
(= user-id (reaction-user-id reaction))
|
||||
(nil? (reaction-user-id reaction)))))
|
||||
existing (some (fn [reaction] (when (match? reaction) reaction)) reactions)]
|
||||
(if existing
|
||||
(do
|
||||
(ldb/transact! conn [[:db/retractEntity (:db/id existing)]]
|
||||
{:outliner-op :toggle-reaction})
|
||||
true)
|
||||
(let [now (common-util/time-ms)
|
||||
reaction-tx (cond-> {:block/uuid (d/squuid)
|
||||
:block/created-at now
|
||||
:logseq.property.reaction/emoji-id emoji-id
|
||||
:logseq.property.reaction/target (:db/id target)}
|
||||
user-id
|
||||
(assoc :logseq.property/created-by-ref user-id))]
|
||||
(ldb/transact! conn [reaction-tx]
|
||||
{:outliner-op :toggle-reaction})
|
||||
true)))))
|
||||
|
||||
(defn- import-edn-data
|
||||
[conn *result export-map {:keys [tx-meta] :as import-options}]
|
||||
(let [{:keys [error] :as txs}
|
||||
(try (sqlite-export/build-import export-map @conn (dissoc import-options :tx-meta))
|
||||
(catch :default e
|
||||
(js/console.error "Import EDN error: " e)
|
||||
{:error "An unexpected error occurred building the import. See the javascript console for details."}))
|
||||
validation (when-not error
|
||||
(sqlite-export/validate-import-txs txs @conn))]
|
||||
;; (cljs.pprint/pprint txs)
|
||||
(if (or error (:error validation))
|
||||
(reset! *result {:error (or error (:error validation))})
|
||||
(try
|
||||
(ldb/transact! conn (:tx-data validation)
|
||||
(merge {::sqlite-export/imported-data? true} tx-meta))
|
||||
(catch :default e
|
||||
(js/console.error "Unexpected Import EDN error:" e)
|
||||
(reset! *result {:error (str "Unexpected Import EDN error: " (pr-str (ex-message e)))}))))))
|
||||
|
||||
(defn- apply-insert-blocks-op!
|
||||
[conn *result [blocks target-block-id opts]]
|
||||
(when-let [target-block (d/entity @conn [:block/uuid target-block-id])]
|
||||
(let [result (outliner-core/insert-blocks! conn blocks target-block opts)]
|
||||
(reset! *result result))))
|
||||
|
||||
(defn- template-children-blocks
|
||||
[db template-id]
|
||||
(when-let [template (d/entity db template-id)]
|
||||
(let [template-blocks (some->> (ldb/get-block-and-children db (:block/uuid template)
|
||||
{:include-property-block? true})
|
||||
rest)]
|
||||
(when (seq template-blocks)
|
||||
(cons (assoc (into {} (first template-blocks))
|
||||
:db/id (:db/id (first template-blocks))
|
||||
:logseq.property/used-template (:db/id template))
|
||||
(map (fn [block]
|
||||
(assoc (into {} block) :db/id (:db/id block)))
|
||||
(rest template-blocks)))))))
|
||||
|
||||
(defn- journal-title
|
||||
[db journal-day]
|
||||
(date-time-util/int->journal-title
|
||||
journal-day
|
||||
(:logseq.property.journal/title-format (d/entity db :logseq.class/Journal))))
|
||||
|
||||
(defn- ensure-template-journal-pages!
|
||||
[conn blocks]
|
||||
(doseq [journal-day (outliner-template/dynamic-template-journal-days blocks)]
|
||||
(when-not (ldb/get-journal-page-by-day @conn journal-day)
|
||||
(outliner-page/create! conn (journal-title @conn journal-day) {:journal? true}))))
|
||||
|
||||
(defn- apply-template-op!
|
||||
[conn *result [template-id target-block-id opts]]
|
||||
(when-let [target (d/entity @conn [:block/uuid target-block-id])]
|
||||
(let [blocks (or (some-> (:template-blocks opts) seq vec)
|
||||
(template-children-blocks @conn [:block/uuid template-id]))
|
||||
_ (ensure-template-journal-pages! conn blocks)
|
||||
blocks (outliner-template/resolve-dynamic-template-blocks @conn target blocks)]
|
||||
(when (seq blocks)
|
||||
(let [sibling? (:sibling? opts)
|
||||
sibling?' (cond
|
||||
(some? sibling?)
|
||||
sibling?
|
||||
|
||||
(seq (:block/_parent target))
|
||||
false
|
||||
|
||||
:else
|
||||
true)
|
||||
result (outliner-core/insert-blocks! conn blocks target
|
||||
(assoc opts
|
||||
:sibling? sibling?'
|
||||
:insert-template? true
|
||||
:outliner-op :insert-template-blocks))]
|
||||
(reset! *result result))))))
|
||||
|
||||
(defn- resolve-indent-outdent-opts
|
||||
[db opts]
|
||||
(if-let [parent-original-uuid (get-in opts [:parent-original :block/uuid])]
|
||||
(let [parent-original (or (d/entity db [:block/uuid parent-original-uuid])
|
||||
(throw (ex-info "Missing outdent parent original"
|
||||
{:type ::missing-parent-original
|
||||
:block/uuid parent-original-uuid})))]
|
||||
(assoc opts :parent-original parent-original))
|
||||
opts))
|
||||
|
||||
(defn- ^:large-vars/cleanup-todo apply-op!
|
||||
[conn opts' *result [op args]]
|
||||
(case op
|
||||
;; blocks
|
||||
:save-block
|
||||
(apply outliner-core/save-block! conn args)
|
||||
|
||||
:insert-blocks
|
||||
(apply-insert-blocks-op! conn *result args)
|
||||
|
||||
:apply-template
|
||||
(apply-template-op! conn *result args)
|
||||
|
||||
:delete-blocks
|
||||
(let [[block-ids opts] args
|
||||
blocks (keep #(d/entity @conn [:block/uuid %]) block-ids)]
|
||||
(outliner-core/delete-blocks! conn blocks (merge opts opts')))
|
||||
|
||||
:move-blocks
|
||||
(let [[block-ids target-block-id opts] args
|
||||
blocks (keep #(d/entity @conn [:block/uuid %]) block-ids)
|
||||
target-block (d/entity @conn [:block/uuid target-block-id])]
|
||||
(when (and target-block (seq blocks))
|
||||
(outliner-core/move-blocks! conn blocks target-block opts)))
|
||||
|
||||
:move-blocks-up-down
|
||||
(let [[block-ids up?] args
|
||||
blocks (keep #(d/entity @conn [:block/uuid %]) block-ids)]
|
||||
(when (seq blocks)
|
||||
(outliner-core/move-blocks-up-down! conn blocks up?)))
|
||||
|
||||
:indent-outdent-blocks
|
||||
(let [[block-ids indent? opts] args
|
||||
blocks (keep #(d/entity @conn [:block/uuid %]) block-ids)
|
||||
opts (resolve-indent-outdent-opts @conn opts)]
|
||||
(when (seq blocks)
|
||||
(outliner-core/indent-outdent-blocks! conn blocks indent? opts)))
|
||||
|
||||
:collapse-expand-blocks
|
||||
(let [[blocks opts] args]
|
||||
(ldb/transact! conn blocks opts))
|
||||
|
||||
;; properties
|
||||
:upsert-property
|
||||
(reset! *result (apply outliner-property/upsert-property! conn args))
|
||||
|
||||
:set-block-property
|
||||
(apply outliner-property/set-block-property! conn args)
|
||||
|
||||
:remove-block-property
|
||||
(apply outliner-property/remove-block-property! conn args)
|
||||
|
||||
:delete-property-value
|
||||
(apply outliner-property/delete-property-value! conn args)
|
||||
|
||||
:create-property-text-block
|
||||
(let [[block-id property-id v opts] args
|
||||
block-id' (when block-id [:block/uuid block-id])]
|
||||
(outliner-property/create-property-text-block! conn block-id' property-id v opts))
|
||||
|
||||
:batch-set-property
|
||||
(apply outliner-property/batch-set-property! conn args)
|
||||
|
||||
:batch-remove-property
|
||||
(apply outliner-property/batch-remove-property! conn args)
|
||||
|
||||
:batch-delete-property-value
|
||||
(apply outliner-property/batch-delete-property-value! conn args)
|
||||
|
||||
:class-add-property
|
||||
(let [[class-id property-id] args]
|
||||
(outliner-property/class-add-property! conn [:block/uuid class-id] property-id))
|
||||
|
||||
:class-remove-property
|
||||
(let [[class-id property-id] args]
|
||||
(outliner-property/class-remove-property! conn [:block/uuid class-id] property-id))
|
||||
|
||||
:upsert-closed-value
|
||||
(apply outliner-property/upsert-closed-value! conn args)
|
||||
|
||||
:delete-closed-value
|
||||
(let [[property-id value-block-id] args]
|
||||
(outliner-property/delete-closed-value! conn property-id [:block/uuid value-block-id]))
|
||||
|
||||
:add-existing-values-to-closed-values
|
||||
(apply outliner-property/add-existing-values-to-closed-values! conn args)
|
||||
|
||||
:batch-import-edn
|
||||
(apply import-edn-data conn *result args)
|
||||
|
||||
:transact
|
||||
(apply ldb/transact! conn args)
|
||||
|
||||
:create-page
|
||||
(let [[title options] args]
|
||||
(reset! *result (outliner-page/create! conn title (or options {}))))
|
||||
|
||||
:rename-page
|
||||
(let [[page-uuid new-title] args]
|
||||
(if (string/blank? new-title)
|
||||
(throw (ex-info "Page name shouldn't be blank" {:block/uuid page-uuid
|
||||
:block/title new-title}))
|
||||
(outliner-core/save-block! conn
|
||||
{:block/uuid page-uuid
|
||||
:block/title new-title})))
|
||||
|
||||
:delete-page
|
||||
(let [[page-uuid opts] args]
|
||||
(reset! *result (outliner-page/delete! conn page-uuid (merge opts opts'))))
|
||||
|
||||
:restore-recycled
|
||||
(let [[root-uuid] args]
|
||||
(reset! *result (outliner-recycle/restore! conn root-uuid)))
|
||||
|
||||
:recycle-delete-permanently
|
||||
(let [[root-uuid] args]
|
||||
(reset! *result (outliner-recycle/permanently-delete! conn root-uuid)))
|
||||
|
||||
:toggle-reaction
|
||||
(reset! *result (apply toggle-reaction! conn args))
|
||||
nil))
|
||||
|
||||
(defn- import-edn-op?
|
||||
[[op _args]]
|
||||
(= :batch-import-edn op))
|
||||
|
||||
(defn apply-ops!
|
||||
[conn ops opts]
|
||||
(assert (ops-validator ops) ops)
|
||||
(let [semantic-ops (filter (fn [op] (get op-construct/semantic-outliner-ops (first op))) ops)
|
||||
single-op-outliner-op (when (= 1 (count ops))
|
||||
(first (first ops)))
|
||||
opts' (cond-> (assoc opts
|
||||
:transact-opts {:conn conn}
|
||||
:local-tx? true
|
||||
:outliner-ops semantic-ops
|
||||
:db-sync/tx-id (or (:db-sync/tx-id opts) (random-uuid)))
|
||||
(and single-op-outliner-op
|
||||
(nil? (:outliner-op opts)))
|
||||
(assoc :outliner-op single-op-outliner-op)
|
||||
|
||||
(some import-edn-op? ops)
|
||||
(assoc ::sqlite-export/imported-data? true))
|
||||
*result (atom nil)]
|
||||
|
||||
(outliner-tx/transact!
|
||||
opts'
|
||||
(doseq [op-entry ops]
|
||||
(apply-op! conn opts' *result op-entry)))
|
||||
|
||||
@*result))
|
||||
1309
deps/outliner/src/logseq/outliner/op/construct.cljc
vendored
Normal file
1309
deps/outliner/src/logseq/outliner/op/construct.cljc
vendored
Normal file
File diff suppressed because it is too large
Load Diff
427
deps/outliner/src/logseq/outliner/page.cljs
vendored
Normal file
427
deps/outliner/src/logseq/outliner/page.cljs
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
(ns logseq.outliner.page
|
||||
"Page-related fns for DB graphs"
|
||||
(:require [clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[datascript.impl.entity :as de]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.util.namespace :as ns-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.entity-plus :as entity-plus]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.frontend.class :as db-class]
|
||||
[logseq.db.frontend.content :as db-content]
|
||||
[logseq.db.frontend.entity-util :as entity-util]
|
||||
[logseq.db.frontend.malli-schema :as db-malli-schema]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.frontend.property.build :as db-property-build]
|
||||
[logseq.graph-parser.block :as gp-block]
|
||||
[logseq.graph-parser.text :as text]
|
||||
[logseq.outliner.recycle :as outliner-recycle]
|
||||
[logseq.outliner.tx-meta :as outliner-tx-meta]
|
||||
[logseq.outliner.validate :as outliner-validate]))
|
||||
|
||||
(defn- page-ref-rewrite-targets
|
||||
"Collect entities that reference `page-entity` via node refs and need title rewrite."
|
||||
[page-entity]
|
||||
(let [refs (->> (:block/_refs page-entity)
|
||||
;; remove child or self that refed this page
|
||||
(remove (fn [ref]
|
||||
(or (= (:db/id ref) (:db/id page-entity))
|
||||
(= (:db/id (:block/page ref)) (:db/id page-entity))))))
|
||||
id-ref->page #(db-content/content-id-ref->page % [page-entity])]
|
||||
(->> refs
|
||||
(keep (fn [ref]
|
||||
(let [raw-title (:block/raw-title ref)
|
||||
block-uuid (:block/uuid ref)]
|
||||
(when raw-title
|
||||
(let [content' (id-ref->page raw-title)]
|
||||
(when (not= raw-title content')
|
||||
(let [remaining-refs (->> (:block/refs ref)
|
||||
(remove (fn [ref']
|
||||
(= (:db/id ref') (:db/id page-entity))))
|
||||
vec)]
|
||||
{:ref-id (:db/id ref)
|
||||
:ref-uuid block-uuid
|
||||
:title content'
|
||||
:refs remaining-refs})))))))
|
||||
seq)))
|
||||
|
||||
(defn- db-refs->page
|
||||
"Replace [[page name]] with page name."
|
||||
[page-entity]
|
||||
(let [page-id (:db/id page-entity)]
|
||||
(some->> (page-ref-rewrite-targets page-entity)
|
||||
(mapcat (fn [{:keys [ref-id title]}]
|
||||
[[:db/retract ref-id :block/refs page-id]
|
||||
{:db/id ref-id
|
||||
:block/title title}])))))
|
||||
|
||||
(defn- db-refs->page-save-ops
|
||||
[page-entity]
|
||||
(some->> (page-ref-rewrite-targets page-entity)
|
||||
(keep (fn [{:keys [ref-uuid title refs]}]
|
||||
(when ref-uuid
|
||||
[:save-block [{:block/uuid ref-uuid
|
||||
:block/title title
|
||||
:block/refs refs}
|
||||
{}]])))
|
||||
seq
|
||||
vec))
|
||||
|
||||
(defn ^:api build-page-retract-tx
|
||||
"Build cleanup tx-data for deleting a schema page.
|
||||
This is pure and can be reused by sync repair."
|
||||
[db page & [{:keys [include-page-retract? today-page?]
|
||||
:or {include-page-retract? true}}]]
|
||||
(let [page-id (:db/id page)
|
||||
page-blocks-tx-data (->> (:block/_page page)
|
||||
(keep (fn [block]
|
||||
(when (d/entity db [:block/uuid (:block/uuid block)])
|
||||
[:db/retractEntity [:block/uuid (:block/uuid block)]]))))
|
||||
property-pair-tx-data (when (ldb/property? page)
|
||||
(->> (d/datoms db :avet (:db/ident page))
|
||||
(map (fn [d] [:db/retract (:e d) (:a d) (:v d)]))))
|
||||
restore-class-parent-tx (when (ldb/class? page)
|
||||
(->> (filter ldb/class? (:logseq.property.class/_extends page))
|
||||
(map (fn [p]
|
||||
{:db/id (:db/id p)
|
||||
:logseq.property.class/extends :logseq.class/Root}))))
|
||||
page-tx (when (and include-page-retract?
|
||||
(d/entity db page-id))
|
||||
[[:db/retractEntity page-id]])]
|
||||
(if today-page?
|
||||
page-blocks-tx-data
|
||||
(concat page-blocks-tx-data
|
||||
property-pair-tx-data
|
||||
restore-class-parent-tx
|
||||
(db-refs->page page)
|
||||
page-tx))))
|
||||
|
||||
(defn delete!
|
||||
"Deletes a page. Returns true if able to delete page. If unable to delete,
|
||||
calls error-handler fn and returns false.
|
||||
Rules:
|
||||
1. today page content is truncated but the page itself can't be deleted
|
||||
2. properties and tags will be hard retracted
|
||||
3. other pages will be moved to Recycle"
|
||||
[conn page-uuid & {:keys [persist-op? rename? error-handler deleted-by-uuid now-ms]
|
||||
:or {persist-op? true
|
||||
error-handler (fn [{:keys [msg]}] (js/console.error msg))}}]
|
||||
(assert (uuid? page-uuid) (str ::delete! " wrong page-uuid: " (if page-uuid page-uuid "nil")))
|
||||
(when page-uuid
|
||||
(when-let [page (d/entity @conn [:block/uuid page-uuid])]
|
||||
(let [today-page? (when-let [day (:block/journal-day page)]
|
||||
(= (date-time-util/ms->journal-day (js/Date.)) day))
|
||||
tx-meta (cond-> (outliner-tx-meta/ensure-outliner-ops
|
||||
{:outliner-op :delete-page
|
||||
:deleted-page (:block/title page)
|
||||
:persist-op? persist-op?}
|
||||
[:delete-page [page-uuid {:deleted-by-uuid deleted-by-uuid
|
||||
:now-ms now-ms}]])
|
||||
rename?
|
||||
(assoc :source-outliner-op :rename-page))]
|
||||
;; TODO: maybe we should add $$$favorites to built-in pages?
|
||||
(cond
|
||||
(or (ldb/built-in? page) (ldb/hidden? page))
|
||||
(do
|
||||
(error-handler {:msg "Built-in page cannot be deleted"})
|
||||
false)
|
||||
|
||||
today-page?
|
||||
(let [tx-data (build-page-retract-tx @conn page {:today-page? true})]
|
||||
(when (seq tx-data)
|
||||
(ldb/transact! conn tx-data tx-meta))
|
||||
{:truncated? true})
|
||||
|
||||
(or (ldb/class? page) (ldb/property? page))
|
||||
(let [tx-data (build-page-retract-tx @conn page {})]
|
||||
(ldb/transact! conn tx-data tx-meta)
|
||||
true)
|
||||
|
||||
:else
|
||||
(let [ref-rewrite-tx-data (db-refs->page page)
|
||||
ref-rewrite-save-ops (db-refs->page-save-ops page)
|
||||
tx-data (concat ref-rewrite-tx-data
|
||||
(outliner-recycle/recycle-page-tx-data @conn page {:deleted-by-uuid deleted-by-uuid
|
||||
:now-ms now-ms}))
|
||||
tx-meta' (cond-> tx-meta
|
||||
(seq ref-rewrite-save-ops)
|
||||
(update :outliner-ops (fnil into []) ref-rewrite-save-ops))]
|
||||
(when (seq tx-data)
|
||||
(ldb/transact! conn tx-data tx-meta'))
|
||||
true))))))
|
||||
|
||||
(defn- build-page-tx [db properties page {:keys [class? tags class-ident-namespace]}]
|
||||
(when (:block/uuid page)
|
||||
(let [type-tag (if class? :logseq.class/Tag :logseq.class/Page)
|
||||
tags' (if (:block/journal-day page) tags (conj tags type-tag))
|
||||
page' (update page :block/tags
|
||||
(fnil into [])
|
||||
(mapv (fn [tag]
|
||||
(let [v (if (uuid? tag)
|
||||
(d/entity db [:block/uuid tag])
|
||||
tag)]
|
||||
(cond (de/entity? v)
|
||||
(:db/id v)
|
||||
;; tx map
|
||||
(map? v)
|
||||
;; Handle adding :db/ident if a new tag
|
||||
(if (d/entity db [:block/uuid (:block/uuid v)])
|
||||
v
|
||||
(db-class/build-new-class db v))
|
||||
:else
|
||||
v)))
|
||||
tags'))
|
||||
property-vals-tx-m
|
||||
;; Builds property values for built-in properties like logseq.property.pdf/file
|
||||
(db-property-build/build-property-values-tx-m
|
||||
page'
|
||||
(->> properties
|
||||
(keep (fn [[k v]]
|
||||
;; TODO: Pass in property type in order to support property
|
||||
;; types other than :default
|
||||
(when (db-property/built-in-has-ref-value? k)
|
||||
[k v])))
|
||||
(into {})))]
|
||||
(cond-> (if class?
|
||||
[(merge (db-class/build-new-class db page' {:ident-namespace class-ident-namespace})
|
||||
;; FIXME: new pages shouldn't have db/ident but converting property to tag still relies on this
|
||||
(select-keys page' [:db/ident]))
|
||||
[:db/retract [:block/uuid (:block/uuid page)] :block/tags :logseq.class/Page]]
|
||||
[page'])
|
||||
(seq property-vals-tx-m)
|
||||
(into (vals property-vals-tx-m))
|
||||
true
|
||||
(conj (merge {:block/uuid (:block/uuid page)}
|
||||
properties
|
||||
(db-property-build/build-properties-with-ref-values property-vals-tx-m)))))))
|
||||
|
||||
;; TODO: Revisit title cleanup as this was copied from file implementation
|
||||
(defn ^:api sanitize-title
|
||||
[title]
|
||||
(let [title (-> (string/trim title)
|
||||
(text/page-ref-un-brackets!))
|
||||
title (common-util/remove-boundary-slashes title)]
|
||||
title))
|
||||
|
||||
(defn- get-page-by-parent-name
|
||||
[db parent-title child-title class?]
|
||||
(some->>
|
||||
(d/q
|
||||
'[:find [?b ...]
|
||||
:in $ ?attribute ?parent-name ?child-name
|
||||
:where
|
||||
[?b ?attribute ?p]
|
||||
[?b :block/name ?child-name]
|
||||
[?p :block/name ?parent-name]]
|
||||
db
|
||||
(if class? :logseq.property.class/extends :block/parent)
|
||||
(common-util/page-name-sanity-lc parent-title)
|
||||
(common-util/page-name-sanity-lc child-title))
|
||||
first
|
||||
(d/entity db)))
|
||||
|
||||
(defn- page-with-parent-and-order
|
||||
"Apply to namespace pages"
|
||||
[db page & {:keys [parent]}]
|
||||
(let [library (ldb/get-built-in-page db common-config/library-page-name)]
|
||||
(when (nil? library)
|
||||
(throw (ex-info "Library page doesn't exist" {})))
|
||||
(assoc page
|
||||
:block/parent (or parent (:db/id library))
|
||||
:block/order (db-order/gen-key))))
|
||||
|
||||
(defn- ^:large-vars/cleanup-todo split-namespace-pages
|
||||
[db page date-formatter create-class?]
|
||||
(let [{:block/keys [title] block-uuid :block/uuid} page]
|
||||
(->>
|
||||
(if (and (or (entity-util/class? page)
|
||||
(entity-util/page? page))
|
||||
(ns-util/namespace-page? title))
|
||||
(let [class? (entity-util/class? page)
|
||||
parts (->> (string/split title ns-util/parent-re)
|
||||
(map string/trim)
|
||||
(remove string/blank?))
|
||||
pages (map-indexed
|
||||
(fn [idx part]
|
||||
(let [last-part? (= idx (dec (count parts)))
|
||||
page (if (zero? idx)
|
||||
(ldb/get-page db part)
|
||||
(get-page-by-parent-name db (nth parts (dec idx)) part create-class?))
|
||||
result (or page
|
||||
(gp-block/page-name->map part db true date-formatter
|
||||
{:page-uuid (when last-part? block-uuid)
|
||||
:skip-existing-page-check? true
|
||||
:class? class?}))]
|
||||
result))
|
||||
parts)]
|
||||
(cond
|
||||
(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."
|
||||
: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."
|
||||
:i18n-key :class.validation/parents-must-be-tags
|
||||
:type :warning}}))
|
||||
|
||||
:else
|
||||
(map-indexed
|
||||
(fn [idx page]
|
||||
(let [parent-eid (when (> idx 0)
|
||||
(when-let [id (:block/uuid (nth pages (dec idx)))]
|
||||
[:block/uuid id]))]
|
||||
(if class?
|
||||
(cond
|
||||
(and (de/entity? page) (ldb/class? page))
|
||||
(assoc page :logseq.property.class/extends parent-eid)
|
||||
|
||||
(de/entity? page) ; page exists but not a class, avoid converting here because this could be troublesome.
|
||||
nil
|
||||
|
||||
(zero? idx)
|
||||
(db-class/build-new-class db page)
|
||||
|
||||
:else
|
||||
(db-class/build-new-class db (assoc page :logseq.property.class/extends parent-eid)))
|
||||
(if (de/entity? page)
|
||||
page
|
||||
(page-with-parent-and-order db page {:parent parent-eid})))))
|
||||
pages)))
|
||||
[page])
|
||||
(remove nil?))))
|
||||
|
||||
(defn ^:large-vars/cleanup-todo ^:api create
|
||||
"Pure function without side effects"
|
||||
[db title*
|
||||
{uuid' :uuid
|
||||
:keys [tags properties persist-op?
|
||||
class? journal? today-journal? split-namespace? class-ident-namespace]
|
||||
:or {properties nil
|
||||
persist-op? true}
|
||||
:as options}]
|
||||
(let [date-formatter (:logseq.property.journal/title-format (entity-plus/entity-memoized db :logseq.class/Journal))
|
||||
tags (if (every? uuid? tags)
|
||||
(map (fn [id] (d/entity db [:block/uuid id])) tags)
|
||||
tags)
|
||||
class? (or class? (some (fn [t] (= :logseq.class/Tag (:db/ident t))) tags))
|
||||
class-ident-namespace? (and class? class-ident-namespace (string? class-ident-namespace))
|
||||
title (sanitize-title title*)
|
||||
_ (outliner-validate/validate-page-title-no-hashtag title {:node {:block/title title}})
|
||||
types (cond class?
|
||||
#{:logseq.class/Tag}
|
||||
(or journal? today-journal?)
|
||||
#{:logseq.class/Journal}
|
||||
(seq tags)
|
||||
(set (map :db/ident tags))
|
||||
:else
|
||||
#{:logseq.class/Page})
|
||||
existing-names-page (ldb/page-exists? db title types)
|
||||
journal-page-uuid (some-> (gp-block/page-name->map title db false date-formatter
|
||||
{:class? class?
|
||||
:skip-existing-page-check? true})
|
||||
:block/uuid)
|
||||
existing-page-by-journal-uuid (when (uuid? journal-page-uuid)
|
||||
(d/entity db [:block/uuid journal-page-uuid]))
|
||||
existing-page-id (if class-ident-namespace?
|
||||
(some->> existing-names-page
|
||||
(filter #(try (when-let [e (d/entity db %)]
|
||||
(let [ns' (namespace (:db/ident e))]
|
||||
(= (str ns') class-ident-namespace)))
|
||||
(catch :default _ false)))
|
||||
(first))
|
||||
(first existing-names-page))
|
||||
existing-page (or (some->> existing-page-id (d/entity db))
|
||||
existing-page-by-journal-uuid)]
|
||||
(if (and existing-page
|
||||
(or (:block/journal-day existing-page)
|
||||
(not (:block/parent existing-page))
|
||||
(ldb/recycled? existing-page)))
|
||||
(let [tx-meta {:persist-op? persist-op?
|
||||
:outliner-op :save-block}]
|
||||
(cond
|
||||
(and class?
|
||||
(not (ldb/class? existing-page))
|
||||
(ldb/internal-page? existing-page))
|
||||
;; Convert existing page to class
|
||||
(let [tx-data [(merge (db-class/build-new-class db
|
||||
(select-keys existing-page [:block/title :block/uuid :block/created-at])
|
||||
(when class-ident-namespace?
|
||||
{:ident-namespace class-ident-namespace}))
|
||||
(select-keys existing-page [:db/ident]))
|
||||
[:db/retract [:block/uuid (:block/uuid existing-page)] :block/tags :logseq.class/Page]]]
|
||||
{:tx-meta tx-meta
|
||||
:tx-data tx-data
|
||||
:page-uuid (:block/uuid existing-page)
|
||||
:title (:block/title existing-page)})
|
||||
|
||||
(ldb/recycled? existing-page)
|
||||
(let [options' (assoc options :uuid (:block/uuid existing-page))
|
||||
tx-meta' (outliner-tx-meta/ensure-outliner-ops
|
||||
{:persist-op? persist-op?
|
||||
:outliner-op :create-page}
|
||||
[:create-page [title options']])]
|
||||
{:tx-meta tx-meta'
|
||||
:tx-data (outliner-recycle/restore-tx-data db existing-page)
|
||||
:page-uuid (:block/uuid existing-page)
|
||||
:title (:block/title existing-page)})
|
||||
|
||||
;; Just return existing page info
|
||||
:else
|
||||
{:page-uuid (:block/uuid existing-page)
|
||||
:title (:block/title existing-page)}))
|
||||
(let [page (gp-block/page-name->map title db true date-formatter
|
||||
{:class? class?
|
||||
:page-uuid (when (uuid? uuid') uuid')
|
||||
:skip-existing-page-check? true})
|
||||
[page parents'] (if (and (not (:block/journal-day page))
|
||||
(text/namespace-page? title)
|
||||
split-namespace?)
|
||||
(let [pages (split-namespace-pages db page date-formatter class?)]
|
||||
[(last pages) (butlast pages)])
|
||||
[page nil])]
|
||||
(when (and page (or (nil? (:db/ident page))
|
||||
;; New page creation must not override built-in entities
|
||||
(not (db-malli-schema/internal-ident? (:db/ident page)))))
|
||||
;; Don't validate journal names because they can have '/'
|
||||
(when-not (or (contains? types :logseq.class/Journal)
|
||||
(contains? (set (:block/tags page)) :logseq.class/Journal))
|
||||
(outliner-validate/validate-page-title-characters (str (:block/title page)) {:node page})
|
||||
(doseq [parent parents']
|
||||
(outliner-validate/validate-page-title-characters (str (:block/title parent)) {:node parent})))
|
||||
|
||||
(let [page-uuid (if-let [journal-day (:block/journal-day page)]
|
||||
(common-uuid/gen-uuid :journal-page-uuid journal-day)
|
||||
(:block/uuid page))
|
||||
page (assoc page :block/uuid page-uuid)
|
||||
page-txs (build-page-tx db properties page (select-keys options [:class? :tags :class-ident-namespace]))
|
||||
txs (concat
|
||||
;; transact doesn't support entities
|
||||
(remove de/entity? parents')
|
||||
page-txs)
|
||||
tx-meta (cond-> (outliner-tx-meta/ensure-outliner-ops
|
||||
{:persist-op? persist-op?
|
||||
:outliner-op :create-page}
|
||||
[:create-page [title options]])
|
||||
today-journal?
|
||||
(assoc :create-today-journal? true
|
||||
:today-journal-name title))]
|
||||
{:tx-meta tx-meta
|
||||
:tx-data txs
|
||||
:title title
|
||||
:page-uuid page-uuid}))))))
|
||||
|
||||
(defn create!
|
||||
[conn title opts]
|
||||
(let [{:keys [tx-meta tx-data title page-uuid]} (create @conn title opts)]
|
||||
(when (seq tx-data)
|
||||
(ldb/transact! conn tx-data tx-meta))
|
||||
[title page-uuid]))
|
||||
156
deps/outliner/src/logseq/outliner/pipeline.cljs
vendored
Normal file
156
deps/outliner/src/logseq/outliner/pipeline.cljs
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
(ns logseq.outliner.pipeline
|
||||
"Core fns for use with frontend worker and node"
|
||||
(:require [datascript.core :as d]
|
||||
[datascript.impl.entity :as de]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.entity-plus :as entity-plus]
|
||||
[logseq.db.frontend.content :as db-content]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.outliner.datascript-report :as ds-report]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(defn filter-deleted-blocks
|
||||
[datoms]
|
||||
(keep
|
||||
(fn [d]
|
||||
(when (and (= :block/uuid (:a d)) (false? (:added d)))
|
||||
{:db/id (:e d)
|
||||
:block/uuid (:v d)}))
|
||||
datoms))
|
||||
|
||||
(defn- ref->eid
|
||||
"ref: entity, map, int, eid"
|
||||
[ref]
|
||||
(cond
|
||||
(:db/id ref)
|
||||
(:db/id ref)
|
||||
|
||||
(:block/uuid ref)
|
||||
[:block/uuid (:block/uuid ref)]
|
||||
|
||||
(and (vector? ref)
|
||||
(= (count ref) 2)
|
||||
(= :block/uuid (first ref)))
|
||||
[:block/uuid (second ref)]
|
||||
|
||||
(int? ref)
|
||||
ref
|
||||
|
||||
:else (throw (js/Error. (str "invalid ref " ref)))))
|
||||
|
||||
(defn block-content-refs
|
||||
"Return ref block ids for given block"
|
||||
[db block]
|
||||
(let [content (or (:block/raw-title block)
|
||||
(:block/title block))]
|
||||
(when (string? content)
|
||||
(->> (db-content/get-matched-ids content)
|
||||
(map (fn [id]
|
||||
(when-let [e (d/entity db [:block/uuid id])]
|
||||
(:db/id e))))))))
|
||||
|
||||
(defn ^:api get-journal-day-from-long
|
||||
[db v]
|
||||
(when v
|
||||
(let [day (date-time-util/ms->journal-day v)]
|
||||
(:e (first (d/datoms db :avet :block/journal-day day))))))
|
||||
|
||||
(def ^:private private-built-in-props (set (keep (fn [[k v]] (when-not (get-in v [:schema :public?]) k))
|
||||
db-property/built-in-properties)))
|
||||
|
||||
(defn- build-journal-refs-for-datetime-properties
|
||||
"For a given property pair, builds a coll of journal refs for select built-in
|
||||
:datetime properties and all user :datetime properties. Otherwise returns nil"
|
||||
[db property v]
|
||||
(let [property-ent (d/entity db property)
|
||||
allowed-datetime? (and (= :datetime (:logseq.property/type property-ent))
|
||||
;; Only allow a few built-in properties as some built-in properties
|
||||
;; can create undesirable refs
|
||||
(if (db-property/internal-property? (:db/ident property-ent))
|
||||
(contains? #{:logseq.property/scheduled :logseq.property/deadline} (:db/ident property-ent))
|
||||
;; All user properties are allowed to create refs but not plugin properties
|
||||
(not (db-property/plugin-property? (:db/ident property-ent)))))]
|
||||
(cond
|
||||
(and allowed-datetime? (coll? v))
|
||||
(keep #(get-journal-day-from-long db %) v)
|
||||
|
||||
allowed-datetime?
|
||||
(when-let [journal-day (get-journal-day-from-long db v)]
|
||||
[journal-day])
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(defn ^:api page-or-object?-helper
|
||||
[block]
|
||||
(and (de/entity? block)
|
||||
(or (ldb/page? block)
|
||||
(ldb/object? block))
|
||||
;; Don't allow :default property value objects to reference their
|
||||
;; parent block as they are dependent on their block for display
|
||||
;; and look weirdly recursive - https://github.com/logseq/db-test/issues/36
|
||||
(not (:logseq.property/created-from-property block))))
|
||||
|
||||
(defonce ^:private non-ref-properties
|
||||
(set/union private-built-in-props
|
||||
#{:logseq.property/query :logseq.property.publish/published-url :logseq.property/exclude-from-graph-view}))
|
||||
|
||||
(defn db-rebuild-block-refs
|
||||
"Rebuild block refs for DB graphs, should returns ids"
|
||||
[db block & {:keys [page-or-object?-memoized]}]
|
||||
(let [block-db-id (:db/id block)
|
||||
;; explicit lookup in order to be nbb compatible
|
||||
properties (->>
|
||||
(entity-plus/lookup-kv-then-entity (d/entity db block-db-id) :block/properties)
|
||||
(remove (fn [[k _v]]
|
||||
(non-ref-properties k)))
|
||||
(into {}))
|
||||
property-key-refs (->> (keys properties)
|
||||
(keep (fn [ident]
|
||||
(:db/id (d/entity db ident)))))
|
||||
page-or-object? (or page-or-object?-memoized page-or-object?-helper)
|
||||
property-value-refs (->> properties
|
||||
(mapcat (fn [[property v]]
|
||||
(cond
|
||||
(and (coll? v) (every? page-or-object? v))
|
||||
(map :db/id v)
|
||||
|
||||
(page-or-object? v)
|
||||
[(:db/id v)]
|
||||
|
||||
:else
|
||||
(build-journal-refs-for-datetime-properties db property v)))))
|
||||
property-refs (concat property-key-refs property-value-refs)
|
||||
content-refs (block-content-refs db block)]
|
||||
(->> (concat (map ref->eid (:block/tags block))
|
||||
(when-let [id (:db/id (:block/link block))]
|
||||
[id])
|
||||
property-refs content-refs)
|
||||
distinct
|
||||
;; Remove self-ref to avoid recursive bugs
|
||||
(remove #(or (identical? block-db-id %)
|
||||
(identical? block-db-id (:db/id (d/entity db %)))))
|
||||
;; Remove alias ref to avoid recursive display bugs
|
||||
(remove #(some (fn [alias-id] (identical? alias-id %)) (map :db/id (:block/alias block))))
|
||||
(remove nil?))))
|
||||
|
||||
(defn- rebuild-block-refs-tx
|
||||
[{:keys [db-after]} blocks]
|
||||
(mapcat (fn [block]
|
||||
(when (d/entity db-after (:db/id block))
|
||||
(let [refs (db-rebuild-block-refs db-after block)]
|
||||
(when (seq refs)
|
||||
[[:db/retract (:db/id block) :block/refs]
|
||||
{:db/id (:db/id block)
|
||||
:block/refs refs}]))))
|
||||
blocks))
|
||||
|
||||
(defn transact-new-db-graph-refs
|
||||
"Transacts :block/refs for a new or imported DB graph"
|
||||
[conn tx-report]
|
||||
(let [{:keys [blocks]} (ds-report/get-blocks-and-pages tx-report)
|
||||
refs-tx-report (when-let [refs-tx (and (seq blocks) (rebuild-block-refs-tx tx-report blocks))]
|
||||
(ldb/transact! conn refs-tx (-> (:tx-meta tx-report)
|
||||
(assoc :transact-new-graph-refs? true))))]
|
||||
refs-tx-report))
|
||||
1274
deps/outliner/src/logseq/outliner/property.cljs
vendored
Normal file
1274
deps/outliner/src/logseq/outliner/property.cljs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
292
deps/outliner/src/logseq/outliner/recycle.cljs
vendored
Normal file
292
deps/outliner/src/logseq/outliner/recycle.cljs
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
(ns logseq.outliner.recycle
|
||||
"Recycle-based soft delete helpers for DB graphs"
|
||||
(:require [datascript.core :as d]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.delete-blocks :as delete-blocks]
|
||||
[logseq.db.common.entity-plus :as entity-plus]
|
||||
[logseq.db.common.initial-data :as common-initial-data]
|
||||
[logseq.db.common.order :as db-order]))
|
||||
|
||||
(def ^:private recycle-page-title "Recycle")
|
||||
(def ^:private retention-ms (* 30 24 3600 1000))
|
||||
(def gc-interval-ms (* 24 3600 1000))
|
||||
|
||||
(defn- recycled?
|
||||
[entity]
|
||||
(some? (:logseq.property/deleted-at entity)))
|
||||
|
||||
(defn- build-recycle-page-tx
|
||||
[db-id]
|
||||
(let [now (common-util/time-ms)]
|
||||
{:db/id db-id
|
||||
:block/uuid (common-uuid/gen-uuid :builtin-block-uuid recycle-page-title)
|
||||
:block/name (common-util/page-name-sanity-lc recycle-page-title)
|
||||
:block/title recycle-page-title
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:logseq.property/hide? true
|
||||
:logseq.property/built-in? true}))
|
||||
|
||||
(defn- recycle-page
|
||||
[db]
|
||||
(ldb/get-built-in-page db recycle-page-title))
|
||||
|
||||
(defn- ensure-recycle-page
|
||||
[db]
|
||||
(if-let [page (recycle-page db)]
|
||||
{:page page
|
||||
:page-id (:db/id page)
|
||||
:tx-data []}
|
||||
{:page nil
|
||||
:page-id "recycle-page"
|
||||
:tx-data [(build-recycle-page-tx "recycle-page")]}))
|
||||
|
||||
(defn- next-child-order
|
||||
[parent]
|
||||
(let [last-child (last (ldb/sort-by-order (:block/_parent parent)))]
|
||||
(db-order/gen-key (:block/order last-child) nil)))
|
||||
|
||||
(defn- maybe-assoc-ref
|
||||
[m k entity]
|
||||
(if (and entity (:db/id entity))
|
||||
(assoc m k (:db/id entity))
|
||||
m))
|
||||
|
||||
(defn- maybe-assoc
|
||||
[m k v]
|
||||
(if (some? v)
|
||||
(assoc m k v)
|
||||
m))
|
||||
|
||||
(defn- resolve-entity
|
||||
[db value]
|
||||
(cond
|
||||
(and value (:db/id value)) value
|
||||
(int? value) (d/entity db value)
|
||||
(vector? value) (d/entity db value)
|
||||
:else nil))
|
||||
|
||||
(defn- block-subtree
|
||||
[db block]
|
||||
(let [ids (cons (:db/id block)
|
||||
(common-initial-data/get-block-full-children-ids db (:db/id block)))]
|
||||
(keep #(d/entity db %) ids)))
|
||||
|
||||
(defn- block-children
|
||||
[block]
|
||||
(entity-plus/lookup-kv-then-entity block :block/_raw-parent))
|
||||
|
||||
(defn- page-descendants
|
||||
[page]
|
||||
(loop [pages [page]
|
||||
result []]
|
||||
(if-let [page' (first pages)]
|
||||
(let [children (->> (block-children page')
|
||||
(filter ldb/page?)
|
||||
ldb/sort-by-order)]
|
||||
(recur (concat (rest pages) children)
|
||||
(conj result page')))
|
||||
result)))
|
||||
|
||||
(defn- page-block-subtree-ids
|
||||
[db page]
|
||||
(let [root-blocks (->> (concat (:block/_page page)
|
||||
(remove ldb/page? (block-children page)))
|
||||
(common-util/distinct-by :db/id)
|
||||
ldb/sort-by-order)]
|
||||
(->> root-blocks
|
||||
(mapcat (fn [block]
|
||||
(map :db/id (block-subtree db block)))))))
|
||||
|
||||
(defn- page-tree-ids
|
||||
[db page]
|
||||
(->> (page-descendants page)
|
||||
(mapcat (fn [page']
|
||||
(cons (:db/id page')
|
||||
(page-block-subtree-ids db page'))))
|
||||
distinct))
|
||||
|
||||
(defn- deleted-by-id
|
||||
[db deleted-by-uuid]
|
||||
(some-> deleted-by-uuid
|
||||
(#(d/entity db [:block/uuid %]))
|
||||
:db/id))
|
||||
|
||||
(defn- with-delete-cleanup-tx
|
||||
[db tx-data]
|
||||
(distinct (concat tx-data (delete-blocks/update-refs-history db tx-data {}))))
|
||||
|
||||
(defn recycle-blocks-tx-data
|
||||
[db blocks {:keys [deleted-by-uuid now-ms]}]
|
||||
(let [{:keys [page page-id tx-data]} (ensure-recycle-page db)
|
||||
deleted-by-id' (deleted-by-id db deleted-by-uuid)
|
||||
now-ms (or now-ms (common-util/time-ms))
|
||||
[recycle-tx _previous-order]
|
||||
(reduce
|
||||
(fn [[txs previous-order] block]
|
||||
(let [subtree (block-subtree db block)
|
||||
order (db-order/gen-key previous-order nil)
|
||||
root-tx (cond-> {:db/id (:db/id block)
|
||||
:block/parent page-id
|
||||
:block/page page-id
|
||||
:block/order order
|
||||
:logseq.property/deleted-at now-ms}
|
||||
true
|
||||
(maybe-assoc-ref :logseq.property/deleted-by-ref (d/entity db deleted-by-id'))
|
||||
true
|
||||
(maybe-assoc-ref :logseq.property.recycle/original-parent (:block/parent block))
|
||||
true
|
||||
(maybe-assoc-ref :logseq.property.recycle/original-page (:block/page block))
|
||||
true
|
||||
(maybe-assoc :logseq.property.recycle/original-order (:block/order block)))
|
||||
subtree-page-tx (map (fn [node]
|
||||
{:db/id (:db/id node)
|
||||
:block/page page-id})
|
||||
subtree)]
|
||||
[(into txs (cons root-tx (rest subtree-page-tx))) order]))
|
||||
[[] (some->> page :block/_parent ldb/sort-by-order last :block/order)]
|
||||
blocks)]
|
||||
(concat tx-data recycle-tx)))
|
||||
|
||||
(defn recycle-page-tx-data
|
||||
[db page {:keys [deleted-by-uuid now-ms]}]
|
||||
(let [{recycle-page-id :page-id
|
||||
recycle-page-init-tx-data :tx-data
|
||||
recycle-page-existing :page} (ensure-recycle-page db)
|
||||
deleted-by-db-id (deleted-by-id db deleted-by-uuid)
|
||||
now-ms (or now-ms (common-util/time-ms))]
|
||||
(concat recycle-page-init-tx-data
|
||||
[(cond-> {:db/id (:db/id page)
|
||||
:block/parent recycle-page-id
|
||||
:block/order (if recycle-page-existing
|
||||
(next-child-order recycle-page-existing)
|
||||
(db-order/gen-key nil nil))
|
||||
:logseq.property/deleted-at now-ms}
|
||||
true
|
||||
(maybe-assoc-ref :logseq.property/deleted-by-ref (d/entity db deleted-by-db-id))
|
||||
true
|
||||
(maybe-assoc-ref :logseq.property.recycle/original-parent (:block/parent page))
|
||||
true
|
||||
(maybe-assoc-ref :logseq.property.recycle/original-page page)
|
||||
true
|
||||
(maybe-assoc :logseq.property.recycle/original-order (:block/order page)))])))
|
||||
|
||||
(defn- restore-order
|
||||
[target-parent]
|
||||
(next-child-order target-parent))
|
||||
|
||||
(defn- restore-target
|
||||
[db root]
|
||||
(let [original-parent (resolve-entity db (:logseq.property.recycle/original-parent root))
|
||||
original-page (resolve-entity db (:logseq.property.recycle/original-page root))
|
||||
parent-valid? (and original-parent
|
||||
(not (recycled? original-parent))
|
||||
(d/entity db (:db/id original-parent)))]
|
||||
(cond
|
||||
(ldb/page? root)
|
||||
{:parent (when parent-valid? original-parent)
|
||||
:page root
|
||||
:order (or (:logseq.property.recycle/original-order root)
|
||||
(when parent-valid? (restore-order original-parent)))}
|
||||
|
||||
parent-valid?
|
||||
{:parent original-parent
|
||||
:page original-page
|
||||
:order (or (:logseq.property.recycle/original-order root)
|
||||
(restore-order original-parent))}
|
||||
|
||||
(and original-page
|
||||
(d/entity db (:db/id original-page))
|
||||
(not (recycled? original-page)))
|
||||
{:parent original-page
|
||||
:page original-page
|
||||
:order (restore-order original-page)}
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(defn ^:api restore-tx-data
|
||||
[db root]
|
||||
(when-let [{:keys [parent page order]} (restore-target db root)]
|
||||
(let [subtree (when-not (ldb/page? root)
|
||||
(block-subtree db root))
|
||||
clear-structure [[:db/retract (:db/id root) :block/parent]
|
||||
[:db/retract (:db/id root) :block/order]
|
||||
(when-not (ldb/page? root)
|
||||
[:db/retract (:db/id root) :block/page])]
|
||||
clear-meta [[:db/retract (:db/id root) :logseq.property/deleted-at]
|
||||
[:db/retract (:db/id root) :logseq.property/deleted-by-ref]
|
||||
[:db/retract (:db/id root) :logseq.property.recycle/original-parent]
|
||||
[:db/retract (:db/id root) :logseq.property.recycle/original-page]
|
||||
[:db/retract (:db/id root) :logseq.property.recycle/original-order]]
|
||||
root-tx (cond-> {:db/id (:db/id root)}
|
||||
parent
|
||||
(assoc :block/parent (:db/id parent))
|
||||
order
|
||||
(assoc :block/order order)
|
||||
(not (ldb/page? root))
|
||||
(assoc :block/page (:db/id page)))
|
||||
subtree-page-tx (when (seq subtree)
|
||||
(map (fn [node]
|
||||
{:db/id (:db/id node)
|
||||
:block/page (:db/id page)})
|
||||
subtree))]
|
||||
(concat clear-structure [root-tx] subtree-page-tx (remove nil? clear-meta)))))
|
||||
|
||||
(defn ^:api restore!
|
||||
[conn root-uuid]
|
||||
(when-let [root (d/entity @conn [:block/uuid root-uuid])]
|
||||
(when-let [tx-data (seq (restore-tx-data @conn root))]
|
||||
(ldb/transact! conn tx-data {:outliner-op :restore-recycled})
|
||||
true)))
|
||||
|
||||
(defn ^:api permanently-delete-tx-data
|
||||
[db root]
|
||||
(when (and root (recycled? root))
|
||||
(some->> (if (ldb/page? root)
|
||||
(keep (fn [id]
|
||||
(some-> (d/entity db id) :block/uuid))
|
||||
(page-tree-ids db root))
|
||||
(keep :block/uuid (block-subtree db root)))
|
||||
(map (fn [block-uuid]
|
||||
[:db/retractEntity [:block/uuid block-uuid]]))
|
||||
distinct
|
||||
seq
|
||||
(with-delete-cleanup-tx db)
|
||||
seq)))
|
||||
|
||||
(defn ^:api permanently-delete!
|
||||
[conn root-uuid]
|
||||
(when-let [root (d/entity @conn [:block/uuid root-uuid])]
|
||||
(when-let [tx-data (permanently-delete-tx-data @conn root)]
|
||||
(ldb/transact! conn tx-data {:outliner-op :recycle-delete-permanently})
|
||||
true)))
|
||||
|
||||
(defn- gc-tx-data
|
||||
[db {:keys [now-ms] :or {now-ms (common-util/time-ms)}}]
|
||||
(let [cutoff (- now-ms retention-ms)]
|
||||
(->>
|
||||
(d/q '[:find [?e ...]
|
||||
:in $ ?cutoff
|
||||
:where
|
||||
[?e :logseq.property/deleted-at ?deleted-at]
|
||||
[(<= ?deleted-at ?cutoff)]]
|
||||
db cutoff)
|
||||
(map #(d/entity db %))
|
||||
(filter recycled?)
|
||||
(mapcat (fn [entity]
|
||||
(if (ldb/page? entity)
|
||||
(map (fn [id] [:db/retractEntity id]) (page-tree-ids db entity))
|
||||
(map (fn [node] [:db/retractEntity (:db/id node)]) (block-subtree db entity)))))
|
||||
distinct
|
||||
seq
|
||||
(with-delete-cleanup-tx db))))
|
||||
|
||||
(defn ^:api gc!
|
||||
[conn opts]
|
||||
(when-let [tx-data (seq (gc-tx-data @conn opts))]
|
||||
(ldb/transact! conn tx-data {:outliner-op :recycle-gc
|
||||
:persist-op? false})
|
||||
true))
|
||||
170
deps/outliner/src/logseq/outliner/template.cljs
vendored
Normal file
170
deps/outliner/src/logseq/outliner/template.cljs
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
(ns logseq.outliner.template
|
||||
"Template with variables"
|
||||
(:require [clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.db :as ldb]))
|
||||
|
||||
(def ^:private template-re #"<%([^%].*?)%>")
|
||||
|
||||
(defn- current-time
|
||||
[]
|
||||
(let [d (js/Date.)
|
||||
locale (some-> js/globalThis (aget "navigator") (aget "language"))]
|
||||
(.toLocaleTimeString d locale (clj->js {:hour "2-digit"
|
||||
:minute "2-digit"
|
||||
:hourCycle "h23"}))))
|
||||
|
||||
(defn- date-with-day-offset
|
||||
[offset-days]
|
||||
(let [d (js/Date.)]
|
||||
(.setHours d 0 0 0 0)
|
||||
(.setDate d (+ (.getDate d) offset-days))
|
||||
d))
|
||||
|
||||
(defn- date->journal-day
|
||||
[^js/Date date]
|
||||
(let [year (.getFullYear date)
|
||||
month (inc (.getMonth date))
|
||||
day (.getDate date)
|
||||
month' (if (< month 10) (str "0" month) (str month))
|
||||
day' (if (< day 10) (str "0" day) (str day))]
|
||||
(js/parseInt (str year month' day') 10)))
|
||||
|
||||
(def ^:private journal-variable-offsets
|
||||
{"today" 0
|
||||
"yesterday" -1
|
||||
"tomorrow" 1})
|
||||
|
||||
(defn- journal-title
|
||||
[db offset-days]
|
||||
(let [journal-day (date->journal-day (date-with-day-offset offset-days))]
|
||||
(or (d/q '[:find ?title .
|
||||
:in $ ?journal-day
|
||||
:where
|
||||
[?p :block/journal-day ?journal-day]
|
||||
[?p :block/title ?title]]
|
||||
db journal-day)
|
||||
(str journal-day))))
|
||||
|
||||
(defn- journal-page
|
||||
[db offset-days]
|
||||
(let [journal-day (date->journal-day (date-with-day-offset offset-days))]
|
||||
(when-let [page-id (d/q '[:find ?p .
|
||||
:in $ ?journal-day
|
||||
:where
|
||||
[?p :block/journal-day ?journal-day]]
|
||||
db journal-day)]
|
||||
(d/entity db page-id))))
|
||||
|
||||
(defn- journal-page-or-title
|
||||
[db offset-days]
|
||||
(or (journal-page db offset-days)
|
||||
(journal-title db offset-days)))
|
||||
|
||||
(defn- target-page
|
||||
[target]
|
||||
(cond
|
||||
(ldb/page? target)
|
||||
target
|
||||
|
||||
:else
|
||||
(:block/page target)))
|
||||
|
||||
(defn- page-ref-for
|
||||
[page-or-title]
|
||||
(page-ref/->page-ref
|
||||
(if-let [page-uuid (:block/uuid page-or-title)]
|
||||
page-uuid
|
||||
page-or-title)))
|
||||
|
||||
(defn- dynamic-template-matches
|
||||
[content]
|
||||
(when (string? content)
|
||||
(map (comp string/lower-case string/trim second)
|
||||
(re-seq template-re content))))
|
||||
|
||||
(defn- block-template-contents
|
||||
[block]
|
||||
(concat
|
||||
[(:block/title block) (:block/raw-title block)]
|
||||
(when (map? (:block/properties-text-values block))
|
||||
(vals (:block/properties-text-values block)))))
|
||||
|
||||
(defn dynamic-template-journal-days
|
||||
[blocks]
|
||||
(->> blocks
|
||||
(mapcat block-template-contents)
|
||||
(mapcat dynamic-template-matches)
|
||||
(keep journal-variable-offsets)
|
||||
(map #(date->journal-day (date-with-day-offset %)))
|
||||
distinct
|
||||
vec))
|
||||
|
||||
(defn- variable-rules
|
||||
[db target]
|
||||
(let [today (journal-page-or-title db 0)
|
||||
current-page (or (target-page target) today)]
|
||||
{"today" (page-ref-for today)
|
||||
"yesterday" (page-ref-for (journal-page-or-title db -1))
|
||||
"tomorrow" (page-ref-for (journal-page-or-title db 1))
|
||||
"time" (current-time)
|
||||
"current page" (page-ref-for current-page)}))
|
||||
|
||||
(defn- resolve-string
|
||||
[content rules]
|
||||
(string/replace content template-re
|
||||
(fn [[_ match]]
|
||||
(let [match' (string/trim match)
|
||||
lowered (string/lower-case match')]
|
||||
(cond
|
||||
(string/blank? match')
|
||||
""
|
||||
|
||||
(contains? rules lowered)
|
||||
(or (get rules lowered) "")
|
||||
|
||||
:else
|
||||
match')))))
|
||||
|
||||
(defn- normalize-block
|
||||
[block]
|
||||
(cond-> (into {} block)
|
||||
(:db/id block)
|
||||
(assoc :db/id (:db/id block))))
|
||||
|
||||
(defn- resolve-field
|
||||
[value rules]
|
||||
(if (string? value)
|
||||
(resolve-string value rules)
|
||||
value))
|
||||
|
||||
(defn- resolve-properties-text-values
|
||||
[value rules]
|
||||
(if (map? value)
|
||||
(reduce-kv (fn [m k v]
|
||||
(assoc m k (resolve-field v rules)))
|
||||
{}
|
||||
value)
|
||||
value))
|
||||
|
||||
(defn- resolve-block
|
||||
[block rules]
|
||||
(cond-> block
|
||||
(contains? block :block/title)
|
||||
(update :block/title resolve-field rules)
|
||||
|
||||
(contains? block :block/raw-title)
|
||||
(update :block/raw-title resolve-field rules)
|
||||
|
||||
(contains? block :block/properties-text-values)
|
||||
(update :block/properties-text-values resolve-properties-text-values rules)))
|
||||
|
||||
(defn resolve-dynamic-template-blocks
|
||||
[db target blocks]
|
||||
(let [rules (variable-rules db target)]
|
||||
(mapv (fn [block]
|
||||
(-> block
|
||||
normalize-block
|
||||
(resolve-block rules)))
|
||||
blocks)))
|
||||
39
deps/outliner/src/logseq/outliner/transaction.cljc
vendored
Normal file
39
deps/outliner/src/logseq/outliner/transaction.cljc
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
(ns logseq.outliner.transaction
|
||||
"Provides a wrapper around logseq.outliner.datascript/transact! using
|
||||
transient state from logseq.outliner.core"
|
||||
#?(:cljs (:require-macros [logseq.outliner.transaction])))
|
||||
|
||||
(defmacro ^:api with-batch-tx
|
||||
[conn opts & body]
|
||||
(let [conn-sym (gensym "conn__")]
|
||||
`(logseq.db/batch-transact-with-temp-conn!
|
||||
~conn
|
||||
(dissoc ~opts :additional-tx :transact-opts :current-block)
|
||||
(fn [~conn-sym]
|
||||
(let [~conn ~conn-sym]
|
||||
~@body
|
||||
(when (seq (:additional-tx ~opts))
|
||||
(logseq.db/transact! ~conn-sym (:additional-tx ~opts) {})))))))
|
||||
|
||||
(defmacro ^:api transact!
|
||||
"Batch all the transactions in `body` to a single transaction.
|
||||
Currently there are no options, it'll execute body and collect all transaction data generated by body.
|
||||
If no transactions are included in `body`, it does not save a transaction.
|
||||
`Args`:
|
||||
`opts`: Every key is optional, opts except `additional-tx` will be transacted as `tx-meta`.
|
||||
{:transact-opts {:conn conn} \"Datascript conn\"
|
||||
:additional-tx \"Additional tx data that can be bundled together
|
||||
with the body in this macro.\"
|
||||
:persist-op? \"Boolean, store ops into db (sqlite)\"}
|
||||
`Example`:
|
||||
(transact! {:conn db-conn}
|
||||
(insert-blocks! ...)
|
||||
;; do something
|
||||
(move-blocks! ...)
|
||||
(delete-blocks! ...))"
|
||||
[opts & body]
|
||||
`(let [~'conn (:conn (:transact-opts ~opts))]
|
||||
(logseq.outliner.transaction/with-batch-tx
|
||||
~'conn
|
||||
~opts
|
||||
~@body)))
|
||||
128
deps/outliner/src/logseq/outliner/tree.cljs
vendored
Normal file
128
deps/outliner/src/logseq/outliner/tree.cljs
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
(ns logseq.outliner.tree
|
||||
"Provides tree fns and INode protocol"
|
||||
(:require [datascript.core :as d]
|
||||
[datascript.impl.entity :as de]
|
||||
[logseq.db :as ldb]))
|
||||
|
||||
(defprotocol INode
|
||||
(-save [this *txs-state conn opts])
|
||||
(-del [this *txs-state db]))
|
||||
|
||||
(defn ^:api blocks->vec-tree-data
|
||||
[blocks root {:keys [include-root? keep-block-tx-id?]}]
|
||||
(if-not (:db/id root)
|
||||
blocks
|
||||
(let [root-id (:db/id root)
|
||||
parent-blocks (group-by #(get-in % [:block/parent :db/id]) blocks)
|
||||
sort-fn (fn [parent]
|
||||
(when-let [children (get parent-blocks parent)]
|
||||
(ldb/sort-by-order children)))
|
||||
block-children (fn block-children [parent level]
|
||||
(mapv (fn [m]
|
||||
(let [id (:db/id m)
|
||||
children (block-children id (inc level))]
|
||||
(cond-> (assoc m
|
||||
:block/level level
|
||||
:block/children children
|
||||
:block/parent {:db/id parent})
|
||||
(not keep-block-tx-id?)
|
||||
(dissoc :block/tx-id))))
|
||||
(sort-fn parent)))
|
||||
children (block-children root-id 1)]
|
||||
(if include-root?
|
||||
[(cond-> (assoc root :block/children children)
|
||||
(not keep-block-tx-id?)
|
||||
(dissoc :block/tx-id))]
|
||||
children))))
|
||||
|
||||
(defn- get-root-and-page
|
||||
[db root-id]
|
||||
(cond
|
||||
(uuid? root-id)
|
||||
(let [e (d/entity db [:block/uuid root-id])]
|
||||
(if (ldb/page? e) [true e] [false e]))
|
||||
|
||||
(number? root-id)
|
||||
(let [e (d/entity db root-id)]
|
||||
(if (ldb/page? e) [true e] [false e]))
|
||||
|
||||
(string? root-id)
|
||||
(if-let [id (parse-uuid root-id)]
|
||||
[false (d/entity db [:block/uuid id])]
|
||||
[true (ldb/get-page db root-id)])
|
||||
|
||||
:else
|
||||
[false root-id]))
|
||||
|
||||
;; TODO: entity can already be used as a tree
|
||||
(defn blocks->vec-tree
|
||||
"`blocks` need to be in the same page."
|
||||
[db blocks root-id & {:as option}]
|
||||
(let [blocks (map (fn [b] (if (de/entity? b)
|
||||
(assoc (into {} b) :db/id (:db/id b))
|
||||
b)) blocks)
|
||||
[page? root] (get-root-and-page db root-id)]
|
||||
(if-not root ; custom query
|
||||
blocks
|
||||
(let [include-root? (or (not page?) (:link option))
|
||||
root (if include-root?
|
||||
(some #(when (= (:db/id %) (:db/id root)) %) blocks)
|
||||
root)]
|
||||
(blocks->vec-tree-data blocks root {:include-root? include-root?})))))
|
||||
|
||||
(defn- tree [parent->children root default-level]
|
||||
(let [root-id (:db/id root)
|
||||
nodes (fn nodes [parent-id level]
|
||||
(mapv (fn [b]
|
||||
(let [b' (assoc b :block/level (inc level))
|
||||
children (nodes (:db/id b) (inc level))]
|
||||
(if (seq children)
|
||||
(assoc b' :block/children children)
|
||||
b')))
|
||||
(let [parent {:db/id parent-id}]
|
||||
(-> (get parent->children parent)
|
||||
(ldb/sort-by-order)))))
|
||||
children (nodes root-id 1)
|
||||
root' (assoc root :block/level (or default-level 1))]
|
||||
(if (seq children)
|
||||
(assoc root' :block/children children)
|
||||
root')))
|
||||
|
||||
(defn ^:api block-entity->map
|
||||
[e]
|
||||
(if (de/entity? e)
|
||||
(cond-> {:db/id (:db/id e)
|
||||
:block/uuid (:block/uuid e)
|
||||
:block/parent {:db/id (:db/id (:block/parent e))}
|
||||
:block/page (:block/page e)}
|
||||
(:block/refs e)
|
||||
(assoc :block/refs (:block/refs e))
|
||||
(:block/children e)
|
||||
(assoc :block/children (:block/children e)))
|
||||
(cond-> e
|
||||
(:db/id (:block/parent e))
|
||||
(assoc :block/parent {:db/id (:db/id (:block/parent e))}))))
|
||||
|
||||
(defn ^:api filter-top-level-blocks
|
||||
[blocks]
|
||||
(let [id->blocks (zipmap (map :db/id blocks) blocks)]
|
||||
(filter #(nil?
|
||||
(id->blocks
|
||||
(:db/id (:block/parent (id->blocks (:db/id %)))))) blocks)))
|
||||
|
||||
(defn non-consecutive-blocks->vec-tree
|
||||
"`blocks` need to be in the same page."
|
||||
([blocks]
|
||||
(non-consecutive-blocks->vec-tree blocks 1))
|
||||
([blocks default-level]
|
||||
(let [blocks (map block-entity->map blocks)
|
||||
top-level-blocks (filter-top-level-blocks blocks)
|
||||
top-level-blocks' (ldb/sort-by-order top-level-blocks)
|
||||
parent->children (group-by :block/parent blocks)]
|
||||
(map #(tree parent->children % (or default-level 1)) top-level-blocks'))))
|
||||
|
||||
(defn get-sorted-block-and-children
|
||||
[db db-id & {:as opts}]
|
||||
(when db-id
|
||||
(when-let [root-block (d/entity db db-id)]
|
||||
(ldb/get-block-and-children db (:block/uuid root-block) opts))))
|
||||
8
deps/outliner/src/logseq/outliner/tx_meta.cljs
vendored
Normal file
8
deps/outliner/src/logseq/outliner/tx_meta.cljs
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
(ns logseq.outliner.tx-meta
|
||||
"Helpers for normalizing tx metadata with explicit outliner op entries.")
|
||||
|
||||
(defn ensure-outliner-ops
|
||||
[tx-meta entry]
|
||||
(cond-> tx-meta
|
||||
(and entry (nil? (:outliner-ops tx-meta)))
|
||||
(assoc :outliner-ops [entry])))
|
||||
378
deps/outliner/src/logseq/outliner/validate.cljs
vendored
Normal file
378
deps/outliner/src/logseq/outliner/validate.cljs
vendored
Normal file
@@ -0,0 +1,378 @@
|
||||
(ns logseq.outliner.validate
|
||||
"Reusable DB graph validations for outliner level and above. Most validations
|
||||
throw errors so the user action stops immediately to display a notification"
|
||||
(:require [clojure.set :as set]
|
||||
[clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.date :as common-date]
|
||||
[logseq.common.util.namespace :as ns-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.class :as db-class]
|
||||
[logseq.db.frontend.entity-util :as entity-util]
|
||||
[logseq.db.frontend.malli-schema :as db-malli-schema]
|
||||
[logseq.db.frontend.property :as db-property]))
|
||||
|
||||
(defn ^:api validate-page-title-no-hashtag
|
||||
"Validates a page title doesn't include hashtag character"
|
||||
[page-title meta-m]
|
||||
(when (string/includes? page-title "#")
|
||||
(throw (ex-info "Page name can't include \"#\"."
|
||||
(merge meta-m
|
||||
{:type :notification
|
||||
:payload {:message "Page name can't include \"#\"."
|
||||
:i18n-key :page.validation/name-no-hash
|
||||
:type :warning}})))))
|
||||
|
||||
(defn ^:api validate-page-title-characters
|
||||
"Validates characters that must not be in a page title"
|
||||
[page-title meta-m]
|
||||
(validate-page-title-no-hashtag page-title meta-m)
|
||||
(when (and (string/includes? page-title ns-util/parent-char)
|
||||
(not (common-date/normalize-date page-title nil)))
|
||||
(throw (ex-info "Page name can't include \"/\"."
|
||||
(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
|
||||
[page-title meta-m]
|
||||
(when (string/blank? page-title)
|
||||
(throw (ex-info "Page name can't be blank"
|
||||
(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
|
||||
"Query that finds other ids given the id to ignore, title to look up and tags to consider"
|
||||
[entity]
|
||||
(cond
|
||||
(ldb/property? entity)
|
||||
;; Property names are unique in that they can
|
||||
;; have the same names as built-in property names
|
||||
'[:find [?b ...]
|
||||
:in $ ?eid ?title [?tag-id ...]
|
||||
:where
|
||||
[?b :block/title ?title]
|
||||
[?b :block/tags ?tag-id]
|
||||
[(missing? $ ?b :logseq.property/built-in?)]
|
||||
[(not= ?b ?eid)]]
|
||||
(:block/parent entity)
|
||||
'[:find [?b ...]
|
||||
:in $ ?eid ?title [?tag-id ...]
|
||||
:where
|
||||
[?b :block/title ?title]
|
||||
[?b :block/tags ?tag-id]
|
||||
[(not= ?b ?eid)]
|
||||
;; same parent
|
||||
[?b :block/parent ?bp]
|
||||
[?eid :block/parent ?ep]
|
||||
[(= ?bp ?ep)]]
|
||||
:else
|
||||
'[:find [?b ...]
|
||||
:in $ ?eid ?title [?tag-id ...]
|
||||
:where
|
||||
[?b :block/title ?title]
|
||||
[?b :block/tags ?tag-id]
|
||||
[(not= ?b ?eid)]]))
|
||||
|
||||
(defn- validate-unique-for-page
|
||||
[db new-title {:block/keys [tags] :as entity}]
|
||||
(when (seq tags)
|
||||
(when-let [another-id (first
|
||||
(d/q (find-other-ids-with-title-and-tags entity)
|
||||
db
|
||||
(:db/id entity)
|
||||
new-title
|
||||
(map :db/id tags)))]
|
||||
(let [another (d/entity db another-id)
|
||||
this-tags (set (map :db/ident tags))
|
||||
another-tags (set (map :db/ident (:block/tags another)))
|
||||
common-tag-ids (set/intersection this-tags another-tags)]
|
||||
(when-not (and (= common-tag-ids #{:logseq.class/Page})
|
||||
(> (count this-tags) 1)
|
||||
(> (count another-tags) 1))
|
||||
(cond
|
||||
(ldb/property? entity)
|
||||
(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"
|
||||
{:type :notification
|
||||
: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
|
||||
"Validates uniqueness of nodes for the following cases:
|
||||
- Page names are unique for a tag e.g. their can be Apple #Company and Apple #Fruit
|
||||
- Property names are unique with user properties being allowed to have the same name as built-in ones
|
||||
- Class names are unique regardless of their extends or if they're built-in"
|
||||
[db new-title entity]
|
||||
(when (entity-util/page? entity)
|
||||
(validate-unique-for-page db new-title entity)))
|
||||
|
||||
(defn ^:api validate-disallow-page-with-journal-name
|
||||
"Validates a non-journal page renamed to journal format"
|
||||
[new-title entity]
|
||||
(when (and (entity-util/page? entity) (not (entity-util/journal? entity))
|
||||
(common-date/normalize-date new-title nil))
|
||||
(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
|
||||
"Validates a block title when it has changed for a entity-util/page? or tagged node"
|
||||
[db new-title existing-block-entity]
|
||||
(validate-unique-by-name-and-tags db new-title existing-block-entity)
|
||||
(validate-disallow-page-with-journal-name new-title existing-block-entity))
|
||||
|
||||
(defn validate-property-title
|
||||
"Validates a property's title when it has changed"
|
||||
([new-title] (validate-property-title new-title {}))
|
||||
([new-title meta-m]
|
||||
(when-not (db-property/valid-property-name? new-title)
|
||||
(throw (ex-info "Property name is invalid"
|
||||
(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-editing-built-in-property
|
||||
"Validates if built-in property entity is editable for the given attributes to be updated"
|
||||
[entity attribute-map-to-update]
|
||||
;; Update allowed as needed. Keep this as an allowed list to default to safe editing for built-in entities
|
||||
(let [allowed-attributes #{:logseq.property/hide-empty-value :logseq.property/description}]
|
||||
(when-let [disallowed (and (:logseq.property/built-in? entity)
|
||||
(not-empty (set/difference (set (keys attribute-map-to-update))
|
||||
allowed-attributes)))]
|
||||
(throw (ex-info "Given built-in property's attributes are not editable"
|
||||
(merge
|
||||
{:type :notification
|
||||
:payload {:message "Can't change the given attributes for a built-in property"
|
||||
:type :error}}
|
||||
{:property (:db/ident entity)
|
||||
:disallowed-attributes disallowed}))))))
|
||||
|
||||
(defn- validate-extends-property-have-correct-type
|
||||
"Validates whether given parent and children are classes"
|
||||
[parent-ent child-ents]
|
||||
(when (or (not (ldb/class? parent-ent))
|
||||
(not (every? ldb/class? child-ents)))
|
||||
(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))}))))
|
||||
|
||||
(defn- disallow-built-in-class-extends-change
|
||||
[_parent-ent child-ents]
|
||||
(when (some #(get db-class/built-in-classes (:db/ident %)) child-ents)
|
||||
(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
|
||||
[db parent-ent child-ents]
|
||||
(doseq [child child-ents]
|
||||
(let [children-ids (set (cons (:db/id child)
|
||||
(db-class/get-structured-children db (:db/id child))))]
|
||||
(when (contains? children-ids (:db/id parent-ent))
|
||||
(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])}}))))))
|
||||
|
||||
(defn validate-extends-property
|
||||
[db parent-ent* child-ents & {:keys [built-in?] :or {built-in? true}}]
|
||||
(let [parent-ent (if (integer? parent-ent*)
|
||||
(d/entity db parent-ent*)
|
||||
parent-ent*)]
|
||||
(when built-in? (disallow-built-in-class-extends-change parent-ent child-ents))
|
||||
(disallow-extends-cycle db parent-ent child-ents)
|
||||
(validate-extends-property-have-correct-type parent-ent child-ents)))
|
||||
|
||||
(defn- disallow-node-cant-tag-with-built-in-non-tags
|
||||
[db _block-eids v]
|
||||
(let [tag-ent (d/entity db v)]
|
||||
(when (and (:logseq.property/built-in? tag-ent)
|
||||
(not (ldb/class? tag-ent)))
|
||||
(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})))))
|
||||
|
||||
(defn- disallow-node-cant-tag-with-private-tags
|
||||
[db block-eids v & {:keys [delete?]}]
|
||||
;; Skip #Page as it is validated by later fns
|
||||
(when (and (contains? (disj ldb/private-tags :logseq.class/Page) (:db/ident (d/entity db v)))
|
||||
(not
|
||||
;; Allow assets to be tagged
|
||||
(and
|
||||
(every? (fn [id] (ldb/asset? (d/entity db id))) block-eids)
|
||||
(= :logseq.class/Asset (:db/ident (d/entity db 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 built-in-entity?
|
||||
"Returns true when the entity is a built-in. Ideally checking
|
||||
:logseq.property/built-in? would be enough but not all built-in nodes have
|
||||
that property. Covers:
|
||||
- entities marked with :logseq.property/built-in? (built-in pages, classes, properties)
|
||||
- file entities (logseq/config.edn, custom.css, etc.)
|
||||
- entities whose :db/ident belongs to an internal namespace (KV entries, empty-placeholder)"
|
||||
[ent]
|
||||
(or (:logseq.property/built-in? ent)
|
||||
(:file/path ent)
|
||||
(some-> (:db/ident ent) db-malli-schema/internal-ident?)))
|
||||
|
||||
(defn- disallow-tagging-a-built-in-entity
|
||||
[db block-eids & {:keys [delete?]}]
|
||||
(when-let [built-in-ent (some #(when (built-in-entity? %) %)
|
||||
(map #(d/entity db %) block-eids))]
|
||||
(throw (ex-info (str (if delete? "Can't remove tag" "Can't add tag")
|
||||
" on built-in " (pr-str (:block/title built-in-ent)))
|
||||
{: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
|
||||
"Disallow page->block when
|
||||
1. this page doesn't have :block/parent
|
||||
2. its parent is Library
|
||||
3. it has page child"
|
||||
[db eids v]
|
||||
(when (= (:db/ident (d/entity db v)) :logseq.class/Page)
|
||||
(let [library-page (ldb/get-library-page db)]
|
||||
(doseq [eid eids]
|
||||
(let [entity (d/entity db eid)]
|
||||
(when (ldb/internal-page? entity)
|
||||
(cond
|
||||
(not (:block/parent entity))
|
||||
(throw (ex-info "This page cannot be converted to a block"
|
||||
{:type :notification
|
||||
: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)))
|
||||
(throw (ex-info "This page cannot be converted to a block"
|
||||
{:type :notification
|
||||
: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))
|
||||
(throw (ex-info "This page cannot be converted to a block"
|
||||
{:type :notification
|
||||
: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}})))))))))
|
||||
|
||||
(defn- validate-block-can-tag-with-page-tag
|
||||
"Validates block can convert to page by adding #Page for allowed scenarios"
|
||||
[db eids v]
|
||||
(when (= (:db/ident (d/entity db v)) :logseq.class/Page)
|
||||
(doseq [eid eids]
|
||||
(let [block (d/entity db eid)]
|
||||
(when (:block/parent block)
|
||||
(validate-page-title (:block/title block) {:node block})
|
||||
(validate-page-title-characters (:block/title block) {:node block})
|
||||
|
||||
;; Only allow block to be page when its parent is a page to guard against invalid pages
|
||||
;; in property values or pages being created with blocks as namespace parents
|
||||
(when (or (not (entity-util/page? (:block/parent block)))
|
||||
(: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.")
|
||||
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)}})))))))))
|
||||
|
||||
(defn validate-tags-property
|
||||
"Validates adding a property value to :block/tags for given blocks"
|
||||
[db block-eids v]
|
||||
(disallow-tagging-a-built-in-entity db block-eids)
|
||||
(disallow-node-cant-tag-with-private-tags db block-eids v)
|
||||
(validate-block-can-tag-with-page-tag db block-eids v)
|
||||
(disallow-node-cant-tag-with-built-in-non-tags db block-eids v))
|
||||
|
||||
(defn validate-tags-property-deletion
|
||||
"Validates deleting a property value from :block/tags for given blocks"
|
||||
[db block-eids v]
|
||||
(disallow-tagging-a-built-in-entity db block-eids {:delete? true})
|
||||
(disallow-node-cant-tag-with-private-tags db block-eids v {:delete? true})
|
||||
(disallow-removing-page-tag db block-eids v))
|
||||
|
||||
(defn disallow-editing-private-built-in-nodes
|
||||
"Disallow editing private :built-in nodes. This explicit validation is needed for contexts
|
||||
like CLI and API which allow users to edit any built-in entity whereas the app guards this
|
||||
by not allowing users to navigate to private built-in nodes"
|
||||
[entities]
|
||||
(doseq [entity entities]
|
||||
(when (and (built-in-entity? entity)
|
||||
;; This also checks private status of non-page ents like ents with :kv/value
|
||||
(ldb/private-built-in-page? entity))
|
||||
(throw (ex-info "Built-in private nodes can't be modified"
|
||||
{:type :notification
|
||||
:payload {:message "Built-in private nodes can't be modified"
|
||||
:type :error}})))))
|
||||
327
deps/outliner/test/logseq/outliner/core_test.cljs
vendored
Normal file
327
deps/outliner/test/logseq/outliner/core_test.cljs
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
(ns logseq.outliner.core-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.common.config :as common-config]))
|
||||
|
||||
(deftest insert-blocks-does-not-trust-stale-right-order
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}
|
||||
{:block/title "original right"}]}])
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
stale-right-order (:block/order (db-test/find-block-by-content @conn "original right"))]
|
||||
(outliner-core/insert-blocks!
|
||||
conn
|
||||
[{:block/uuid (random-uuid)
|
||||
:block/title "concurrent right"}]
|
||||
target
|
||||
{:sibling? true
|
||||
:keep-uuid? true})
|
||||
(let [target (db-test/find-block-by-content @conn "target")
|
||||
current-right (ldb/get-right-sibling target)
|
||||
{:keys [blocks]} (outliner-core/insert-blocks
|
||||
@conn
|
||||
[{:block/uuid (random-uuid)
|
||||
:block/title "inserted"}]
|
||||
target
|
||||
{:sibling? true
|
||||
:keep-uuid? true
|
||||
:end-order-state [:known stale-right-order]})
|
||||
inserted (first blocks)]
|
||||
(is (= "concurrent right" (:block/title current-right)))
|
||||
(is (neg? (compare (:block/order inserted) (:block/order current-right)))
|
||||
"A stale renderer boundary must not place the new block after the current right sibling."))))
|
||||
|
||||
(deftest insert-blocks-finds-right-order-on-1k-sibling-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks (mapv (fn [idx]
|
||||
{:block/title (str "block " idx)})
|
||||
(range 1000))}])
|
||||
target (db-test/find-block-by-content @conn "block 0")
|
||||
insert! #(outliner-core/insert-blocks
|
||||
@conn
|
||||
[{:block/uuid (random-uuid)
|
||||
:block/title "inserted"}]
|
||||
target
|
||||
{:sibling? true
|
||||
:keep-uuid? true})
|
||||
_ (insert!)
|
||||
started-at (.now js/performance)
|
||||
{:keys [blocks]} (insert!)
|
||||
elapsed-ms (- (.now js/performance) started-at)
|
||||
right (ldb/get-right-sibling target)]
|
||||
(is (neg? (compare (:block/order (first blocks)) (:block/order right))))
|
||||
(is (< elapsed-ms 100)
|
||||
(str "1k sibling insertion order lookup took " elapsed-ms "ms"))))
|
||||
|
||||
(deftest blocks-with-level-handles-10k-deep-tree
|
||||
(let [blocks (mapv (fn [id]
|
||||
{:db/id id
|
||||
:block/uuid (str id)
|
||||
:block/parent {:db/id (dec id)}})
|
||||
(range 1 10001))
|
||||
started-at (.now js/performance)
|
||||
result (outliner-core/blocks-with-level blocks)
|
||||
elapsed-ms (- (.now js/performance) started-at)]
|
||||
(is (= 1 (:block/level (first result))))
|
||||
(is (= 10000 (:block/level (last result))))
|
||||
(is (< elapsed-ms 250) (str "10k level calculation took " elapsed-ms "ms"))))
|
||||
|
||||
(deftest existing-inline-class-uses-its-canonical-uuid
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"} :blocks []}])
|
||||
class-uuid (random-uuid)
|
||||
parsed-uuid (random-uuid)
|
||||
ref {:block/type "page"
|
||||
:block/name "existing"
|
||||
:block/title "Existing"
|
||||
:block/uuid parsed-uuid}
|
||||
_ (d/transact! conn [{:db/ident :user.class/existing
|
||||
:block/name "existing"
|
||||
:block/title "Existing"
|
||||
:block/uuid class-uuid
|
||||
:block/tags :logseq.class/Tag}])
|
||||
{:keys [block page-txs]}
|
||||
(#'outliner-core/resolve-page-refs
|
||||
@conn
|
||||
{:block/title (str "#[[" parsed-uuid "]]")
|
||||
:block/refs [ref]
|
||||
:block/tags [ref]})]
|
||||
(is (empty? page-txs))
|
||||
(is (= (str "#[[" class-uuid "]]") (:block/title block)))
|
||||
(is (= class-uuid (-> block :block/refs first :block/uuid)))
|
||||
(is (= class-uuid (-> block :block/tags first :block/uuid)))))
|
||||
|
||||
(deftest test-delete-block-with-default-property
|
||||
(testing "Delete block with default property hard retracts the block subtree"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:default "test block"}}]}])
|
||||
block (db-test/find-block-by-content @conn "b1")]
|
||||
(outliner-core/delete-blocks! conn [block] {})
|
||||
(is (nil? (db-test/find-block-by-content @conn "b1"))))))
|
||||
|
||||
(deftest test-delete-page-with-outliner-core
|
||||
(testing "Deleting pages through outliner-core/delete-blocks detaches page position only"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}
|
||||
{:page {:block/title "page2"}
|
||||
:blocks [{:block/title "b3"}
|
||||
{:block/title "b4"}]}])
|
||||
page2 (ldb/get-page @conn "page2")
|
||||
_ (d/transact! conn [{:db/id (:db/id page2)
|
||||
:block/order "a1"
|
||||
:block/parent (:db/id (ldb/get-page @conn "page1"))}])
|
||||
b3 (db-test/find-block-by-content @conn "b3")
|
||||
b4 (db-test/find-block-by-content @conn "b4")]
|
||||
(outliner-core/delete-blocks! conn [b3 b4 page2] {})
|
||||
(is (some? (db-test/find-block-by-content @conn "b3")))
|
||||
(is (some? (db-test/find-block-by-content @conn "b4")))
|
||||
(let [page2' (ldb/get-page @conn "page2")]
|
||||
(is (= "page2" (:block/title page2')))
|
||||
(is (nil? (:block/parent page2')))
|
||||
(is (nil? (:block/order page2')))))))
|
||||
|
||||
(deftest delete-blocks-hard-retracts-subtree
|
||||
(let [user-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}])
|
||||
parent (db-test/find-block-by-content @conn "parent")]
|
||||
(d/transact! conn [{:block/uuid user-uuid
|
||||
:block/title "Alice"}])
|
||||
(outliner-core/delete-blocks! conn [parent] {:deleted-by-uuid user-uuid})
|
||||
(let [parent' (db-test/find-block-by-content @conn "parent")
|
||||
child' (db-test/find-block-by-content @conn "child")]
|
||||
(is (nil? parent'))
|
||||
(is (nil? child')))))
|
||||
|
||||
(deftest delete-blocks-removes-range-comments-when-all-targets-are-deleted
|
||||
(let [now (js/Date.now)
|
||||
comments-uuid (random-uuid)
|
||||
comment-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target 1"}
|
||||
{:block/title "target 2"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
target-1 (db-test/find-block-by-content @conn "target 1")
|
||||
target-2 (db-test/find-block-by-content @conn "target 2")]
|
||||
(d/transact! conn [{:db/ident :logseq.class/Comments
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comments"}
|
||||
{:db/id -1
|
||||
:block/uuid comments-uuid
|
||||
:block/title "Comments"
|
||||
:block/page (:db/id page)
|
||||
:block/parent (:db/id page)
|
||||
:block/order "a3"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags #{:logseq.class/Comments}
|
||||
:logseq.property.comments/blocks #{(:db/id target-1)
|
||||
(:db/id target-2)}}
|
||||
{:block/uuid comment-uuid
|
||||
:block/title "comment"
|
||||
:block/page (:db/id page)
|
||||
:block/parent -1
|
||||
:block/order "a0"
|
||||
:block/created-at now
|
||||
:block/updated-at now}])
|
||||
(outliner-core/delete-blocks! conn [target-1 target-2] {})
|
||||
(is (nil? (d/entity @conn [:block/uuid comments-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid comment-uuid])))))
|
||||
|
||||
(deftest delete-blocks-keeps-range-comments-when-some-targets-remain
|
||||
(let [now (js/Date.now)
|
||||
comments-uuid (random-uuid)
|
||||
comment-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target 1"}
|
||||
{:block/title "target 2"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
target-1 (db-test/find-block-by-content @conn "target 1")
|
||||
target-2 (db-test/find-block-by-content @conn "target 2")]
|
||||
(d/transact! conn [{:db/ident :logseq.class/Comments
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comments"}
|
||||
{:db/id -1
|
||||
:block/uuid comments-uuid
|
||||
:block/title "Comments"
|
||||
:block/page (:db/id page)
|
||||
:block/parent (:db/id page)
|
||||
:block/order "a3"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags #{:logseq.class/Comments}
|
||||
:logseq.property.comments/blocks #{(:db/id target-1)
|
||||
(:db/id target-2)}}
|
||||
{:block/uuid comment-uuid
|
||||
:block/title "comment"
|
||||
:block/page (:db/id page)
|
||||
:block/parent -1
|
||||
:block/order "a0"
|
||||
:block/created-at now
|
||||
:block/updated-at now}])
|
||||
(outliner-core/delete-blocks! conn [target-1] {})
|
||||
(is (some? (d/entity @conn [:block/uuid comments-uuid])))
|
||||
(is (some? (d/entity @conn [:block/uuid comment-uuid])))
|
||||
(is (some? (d/entity @conn (:db/id target-2))))))
|
||||
|
||||
(deftest delete-blocks-rejects-built-in-entities
|
||||
(let [conn (db-test/create-conn)]
|
||||
(testing "built-in page is rejected"
|
||||
(let [recycle-page (ldb/get-page @conn common-config/recycle-page-name)]
|
||||
(is (true? (:logseq.property/built-in? recycle-page)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [recycle-page] {}))))))
|
||||
|
||||
(testing "built-in idents that are not a class or property like empty-placeholder are rejected"
|
||||
(let [placeholder (d/entity @conn :logseq.property/empty-placeholder)]
|
||||
(is (some? (:block/uuid placeholder)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [placeholder] {}))))))
|
||||
|
||||
(testing "file entity is rejected"
|
||||
(let [file (->> (d/datoms @conn :avet :file/path)
|
||||
first
|
||||
:e
|
||||
(d/entity @conn))]
|
||||
(is (some? (:file/path file)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [file] {}))))))
|
||||
|
||||
(testing "KV entity is rejected"
|
||||
(let [kv (d/entity @conn :logseq.kv/db-type)]
|
||||
(is (some? (:db/id kv)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [kv] {}))))))))
|
||||
|
||||
(deftest save-block-rejects-built-in-entity
|
||||
(let [conn (db-test/create-conn)
|
||||
placeholder (d/entity @conn :logseq.property/description)]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-core/save-block! conn {:db/id (:db/id placeholder) :block/title "hacked"}))))))
|
||||
|
||||
(deftest move-blocks-rejects-built-in-entity
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}])
|
||||
placeholder (d/entity @conn :logseq.property/description)
|
||||
target (db-test/find-block-by-content @conn "target")]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-core/move-blocks! conn [placeholder] target {:sibling? true}))))))
|
||||
|
||||
(deftest move-blocks-protects-comment-blocks
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}
|
||||
{:block/title "tagged comment"}
|
||||
{:block/title "normal parent"
|
||||
:build/children [{:block/title "ordinary"}]}
|
||||
{:block/title "Comments"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
tagged-comment (db-test/find-block-by-content @conn "tagged comment")
|
||||
ordinary (db-test/find-block-by-content @conn "ordinary")
|
||||
comments-area (db-test/find-block-by-content @conn "Comments")
|
||||
original-comment-parent-id (:db/id (:block/parent tagged-comment))]
|
||||
(d/transact! conn [{:db/id -1
|
||||
:db/ident :logseq.class/Tag
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Tag"
|
||||
:block/tags #{-1}}
|
||||
{:db/ident :logseq.class/Comments
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comments"
|
||||
:block/tags #{-1}}
|
||||
{:db/ident :logseq.class/Comment
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comment"
|
||||
:block/tags #{-1}}
|
||||
[:db/add (:db/id comments-area) :block/tags :logseq.class/Comments]
|
||||
[:db/add (:db/id tagged-comment) :block/tags :logseq.class/Comment]])
|
||||
|
||||
(testing "#Comment blocks cannot be moved after creation"
|
||||
(outliner-core/move-blocks! conn [tagged-comment] target {:sibling? false})
|
||||
(is (= original-comment-parent-id
|
||||
(:db/id (:block/parent (d/entity @conn (:db/id tagged-comment)))))))
|
||||
|
||||
(testing "#Comments blocks cannot be moved as children"
|
||||
(outliner-core/move-blocks! conn [comments-area] target {:sibling? false})
|
||||
(let [comments-area' (d/entity @conn (:db/id comments-area))]
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent comments-area'))))))
|
||||
|
||||
(testing "#Comments blocks can be moved as siblings"
|
||||
(outliner-core/move-blocks! conn [comments-area] target {:sibling? true})
|
||||
(let [page' (d/entity @conn (:db/id page))
|
||||
children (->> (:block/_parent page')
|
||||
ldb/sort-by-order
|
||||
(mapv :block/title))]
|
||||
(is (= ["target" "Comments" "tagged comment" "normal parent"] children))))
|
||||
|
||||
(testing "ordinary blocks can be moved as siblings of #Comments blocks"
|
||||
(outliner-core/move-blocks! conn [ordinary] comments-area {:sibling? true})
|
||||
(let [ordinary' (d/entity @conn (:db/id ordinary))]
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent ordinary'))))))
|
||||
|
||||
(testing "ordinary blocks cannot be moved as children of #Comments blocks"
|
||||
(outliner-core/move-blocks! conn [ordinary] comments-area {:sibling? false})
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent (d/entity @conn (:db/id ordinary)))))))
|
||||
|
||||
(testing "ordinary blocks cannot be moved to #Comment blocks"
|
||||
(outliner-core/move-blocks! conn [ordinary] tagged-comment {:sibling? false})
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent (d/entity @conn (:db/id ordinary)))))))))
|
||||
530
deps/outliner/test/logseq/outliner/op_construct_test.cljs
vendored
Normal file
530
deps/outliner/test/logseq/outliner/op_construct_test.cljs
vendored
Normal file
@@ -0,0 +1,530 @@
|
||||
(ns logseq.outliner.op-construct-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.op.construct :as op-construct]))
|
||||
|
||||
(defn- run-direct-outdent
|
||||
[conn block]
|
||||
(let [{:keys [tx-data]}
|
||||
(#'outliner-core/indent-outdent-blocks
|
||||
conn [block] false
|
||||
:parent-original nil
|
||||
:logical-outdenting? nil)
|
||||
tx-report (d/with @conn tx-data {})]
|
||||
{:tx-data (:tx-data tx-report)
|
||||
:db-after (:db-after tx-report)}))
|
||||
|
||||
(deftest derive-history-outliner-ops-canonicalizes-create-page-and-builds-delete-inverse-test
|
||||
(testing "create-page forward op keeps created uuid and reverse op deletes that page"
|
||||
(let [conn (db-test/create-conn-with-blocks {:pages-and-blocks []})
|
||||
page-uuid (random-uuid)
|
||||
tx-data [{:e 1 :a :block/title :v "Created Page" :added true}
|
||||
{:e 1 :a :block/uuid :v page-uuid :added true}]
|
||||
tx-meta {:outliner-op :create-page
|
||||
:outliner-ops [[:create-page ["Created Page"
|
||||
{:redirect? false
|
||||
:split-namespace? true
|
||||
:tags ()}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)]
|
||||
(is (= :create-page (ffirst forward-outliner-ops)))
|
||||
(is (= page-uuid (get-in forward-outliner-ops [0 1 1 :uuid])))
|
||||
(is (= [[:delete-page [page-uuid {}]]]
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-handles-replace-empty-target-insert-inverse-test
|
||||
(testing "replace-empty-target insert keeps source uuid and inverse deletes target placeholder"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title ""}]}]})
|
||||
empty-target (db-test/find-block-by-content @conn "")
|
||||
parent-uuid (random-uuid)
|
||||
child-uuid (random-uuid)
|
||||
tx-meta {:outliner-op :insert-blocks
|
||||
:outliner-ops [[:insert-blocks [[{:block/uuid parent-uuid
|
||||
:block/title "paste parent"}
|
||||
{:block/uuid child-uuid
|
||||
:block/title "paste child"
|
||||
:block/parent [:block/uuid parent-uuid]}]
|
||||
(:db/id empty-target)
|
||||
{:sibling? true
|
||||
:replace-empty-target? true
|
||||
:outliner-op :paste}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= parent-uuid
|
||||
(get-in forward-outliner-ops [0 1 0 0 :block/uuid])))
|
||||
(is (= true (get-in forward-outliner-ops [0 1 2 :keep-uuid?])))
|
||||
(is (some #(and (= :delete-blocks (first %))
|
||||
(= [(:block/uuid empty-target)]
|
||||
(vec (get-in % [1 0]))))
|
||||
(remove nil? inverse-outliner-ops)))
|
||||
(is (not-any? #(= :save-block (first %))
|
||||
(remove nil? inverse-outliner-ops))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-builds-upsert-property-inverse-delete-page-test
|
||||
(testing "upsert-property with qualified keyword builds delete-page inverse"
|
||||
(let [conn (db-test/create-conn-with-blocks {:pages-and-blocks []})
|
||||
property-id :user.property/custom-prop
|
||||
tx-meta {:outliner-op :upsert-property
|
||||
:outliner-ops [[:upsert-property [property-id
|
||||
{:logseq.property/type :default}
|
||||
{:property-name "custom-prop"}]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)
|
||||
expected-page-uuid (common-uuid/gen-uuid :db-ident-block-uuid property-id)]
|
||||
(is (= [[:delete-page [expected-page-uuid {}]]]
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-upsert-property-update-builds-schema-restore-inverse-test
|
||||
(testing "upsert-property on existing property builds inverse upsert-property restore op"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:p-many {:logseq.property/type :default}}
|
||||
:pages-and-blocks []})
|
||||
property-id :user.property/p-many
|
||||
_ (d/transact! conn [[:db/add property-id :logseq.property/classes :logseq.class/Root]])
|
||||
before-property (d/entity @conn property-id)
|
||||
expected-schema (-> (db-property/get-property-schema (into {} before-property))
|
||||
(update :logseq.property/classes
|
||||
(fn [classes]
|
||||
(some->> classes
|
||||
(map (fn [class]
|
||||
(if-let [class-uuid (:block/uuid class)]
|
||||
[:block/uuid class-uuid]
|
||||
(:db/ident class))))
|
||||
set))))
|
||||
tx-meta {:outliner-op :upsert-property
|
||||
:outliner-ops [[:upsert-property [property-id
|
||||
{:logseq.property/type :node
|
||||
:db/cardinality :many}
|
||||
{}]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [[:upsert-property [property-id expected-schema {:property-name "p-many"}]]]
|
||||
inverse-outliner-ops))
|
||||
(is (every? (fn [class-ref]
|
||||
(or (keyword? class-ref)
|
||||
(and (vector? class-ref)
|
||||
(= :block/uuid (first class-ref))
|
||||
(uuid? (second class-ref)))))
|
||||
(get-in inverse-outliner-ops [0 1 1 :logseq.property/classes]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-inverse-avoids-self-target-test
|
||||
(testing "delete-blocks inverse falls back to parent target when left sibling resolves to self"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-id (:db/id child)
|
||||
child-uuid (:block/uuid child)
|
||||
parent-uuid (some-> child :block/parent :block/uuid)
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[child-id] {}]]]}]
|
||||
;; Simulate stale sibling lookup returning the same entity as the deleted root.
|
||||
(with-redefs [ldb/get-left-sibling (fn [_] child)]
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)
|
||||
insert-op (first inverse-outliner-ops)]
|
||||
(is (= :insert-blocks (first insert-op)))
|
||||
(is (= parent-uuid
|
||||
(get-in insert-op [1 1])))
|
||||
(is (= false (get-in insert-op [1 2 :sibling?])))
|
||||
(is (not= child-uuid
|
||||
(get-in insert-op [1 1]))))))))
|
||||
|
||||
(deftest compound-history-inverses-run-in-reverse-dependency-order-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "target"}
|
||||
{:block/title "source"
|
||||
:build/children [{:block/title "nested"}]}]}]})
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
source (db-test/find-block-by-content @conn "source")
|
||||
nested (db-test/find-block-by-content @conn "nested")
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:move-blocks [[(:db/id nested)]
|
||||
(:db/id target)
|
||||
{:sibling? false}]]
|
||||
[:delete-blocks [[(:db/id source)] {}]]
|
||||
[:save-block [(assoc (into {} target) :block/title "combined") nil]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [:save-block :insert-blocks :move-blocks]
|
||||
(mapv first inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-with-stale-id-keeps-id-test
|
||||
(testing "delete-blocks derive-history keeps unresolved numeric ids in forward ops"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-uuid (:block/uuid child)
|
||||
stale-id 99999999
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[(:db/id child) stale-id] {}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [[:delete-blocks [[child-uuid stale-id] {}]]]
|
||||
forward-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-prefers-retracted-tx-data-ids-test
|
||||
(testing "delete-blocks derive-history should prefer tx-data retractEntity ids over stale selection ids"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-id (:db/id child)
|
||||
child-uuid (:block/uuid child)
|
||||
stale-id 99999999
|
||||
tx-report (d/with @conn [[:db/retractEntity child-id]] {})
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[child-id stale-id] {}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= [[:delete-blocks [[child-uuid] {}]]]
|
||||
forward-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-move-blocks-resolves-target-id-from-tx-data-test
|
||||
(testing "move-blocks should resolve stale numeric target id using tx-data block uuid"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "child"}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
stale-target-id 9999999
|
||||
target-uuid (random-uuid)
|
||||
tx-data [{:e stale-target-id :a :block/uuid :v target-uuid :added false}]
|
||||
tx-meta {:outliner-op :move-blocks
|
||||
:outliner-ops [[:move-blocks [[(:db/id child)] stale-target-id {:sibling? true}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)]
|
||||
(is (= target-uuid
|
||||
(get-in forward-outliner-ops [0 1 1]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-save-block-resolves-retracted-ref-id-from-db-before-test
|
||||
(testing "save-block should canonicalize ref attrs using db-before when target ref is retracted in current tx"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "child"}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-uuid (:block/uuid child)
|
||||
tag-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:db/id -1
|
||||
:block/uuid tag-uuid
|
||||
:block/title "Tag"
|
||||
:block/name "tag"
|
||||
:block/tags :logseq.class/Tag}])
|
||||
tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid]))
|
||||
tx-report (d/with @conn [[:db/retractEntity tag-id]] {})
|
||||
tx-meta {:outliner-op :save-block
|
||||
:outliner-ops [[:save-block [{:block/uuid child-uuid
|
||||
:block/tags #{tag-id}}
|
||||
{}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= #{[:block/uuid tag-uuid]}
|
||||
(get-in forward-outliner-ops [0 1 0 :block/tags]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-insert-blocks-resolves-retracted-ref-id-from-tx-data-test
|
||||
(testing "insert-blocks should canonicalize block ref attrs when ref entity is retracted in current tx"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
target-id (:db/id target)
|
||||
tag-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:db/id -1
|
||||
:block/uuid tag-uuid
|
||||
:block/title "Tag"
|
||||
:block/name "tag"
|
||||
:block/tags :logseq.class/Tag}])
|
||||
tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid]))
|
||||
tx-report (d/with @conn [[:db/retractEntity tag-id]] {})
|
||||
tx-meta {:outliner-op :insert-blocks
|
||||
:outliner-ops [[:insert-blocks [[{:block/uuid (random-uuid)
|
||||
:block/title "new child"
|
||||
:block/tags #{tag-id}}]
|
||||
target-id
|
||||
{:sibling? true}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= #{[:block/uuid tag-uuid]}
|
||||
(get-in forward-outliner-ops [0 1 0 0 :block/tags]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-apply-template-undo-canonicalizes-template-block-refs-test
|
||||
(testing "apply-template undo explicit inverse should canonicalize nested template-block ref attrs"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "template"}
|
||||
{:block/title "target"}]}]})
|
||||
template (db-test/find-block-by-content @conn "template")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
template-uuid (:block/uuid template)
|
||||
target-uuid (:block/uuid target)
|
||||
inserted-uuid (random-uuid)
|
||||
tag-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:db/id -1
|
||||
:block/uuid tag-uuid
|
||||
:block/title "Tag"
|
||||
:block/name "tag"
|
||||
:block/tags :logseq.class/Tag}])
|
||||
tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid]))
|
||||
tx-report (d/with @conn [[:db/retractEntity tag-id]] {})
|
||||
tx-meta {:outliner-op :apply-template
|
||||
:undo? true
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:apply-template [template-uuid
|
||||
target-uuid
|
||||
{:template-blocks [{:block/uuid inserted-uuid
|
||||
:block/title "inserted"
|
||||
:block/tags #{tag-id}}]
|
||||
:sibling? true}]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= #{[:block/uuid tag-uuid]}
|
||||
(get-in inverse-outliner-ops [0 1 2 :template-blocks 0 :block/tags]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-apply-template-captures-template-blocks-when-missing-in-op-test
|
||||
(testing "apply-template forward op should capture concrete template-blocks even if original op omitted them"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "template"
|
||||
:build/children [{:block/title "template child 1"}
|
||||
{:block/title "template child 2"}]}
|
||||
{:block/title "target"}]}]})
|
||||
template (db-test/find-block-by-content @conn "template")
|
||||
template-child-1 (db-test/find-block-by-content @conn "template child 1")
|
||||
template-child-2 (db-test/find-block-by-content @conn "template child 2")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
inserted-child-1-uuid (random-uuid)
|
||||
inserted-child-2-uuid (random-uuid)
|
||||
tx-data [{:e 900001 :a :block/uuid :v inserted-child-1-uuid :added true}
|
||||
{:e 900002 :a :block/uuid :v inserted-child-2-uuid :added true}]
|
||||
_ (d/transact! conn [[:db/add (:db/id template-child-1) :block/refs (:db/id template-child-2)]])
|
||||
tx-meta {:outliner-op :apply-template
|
||||
:outliner-ops [[:apply-template [(:db/id template)
|
||||
(:db/id target)
|
||||
{:sibling? true}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)]
|
||||
(is (= :apply-template (ffirst forward-outliner-ops)))
|
||||
(is (= true (get-in forward-outliner-ops [0 1 2 :keep-uuid?])))
|
||||
(is (= [inserted-child-1-uuid inserted-child-2-uuid]
|
||||
(mapv :block/uuid (get-in forward-outliner-ops [0 1 2 :template-blocks]))))
|
||||
(is (= #{[:block/uuid inserted-child-2-uuid]}
|
||||
(get-in forward-outliner-ops [0 1 2 :template-blocks 0 :block/refs]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-builds-delete-page-inverse-for-class-property-and-today-page-test
|
||||
(testing "delete-page inverse restores hard-retracted class/property/today pages with stable db/ident"
|
||||
(let [today (date-time-util/ms->journal-day (js/Date.))
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Movie {}}
|
||||
:properties {:rating {:logseq.property/type :number}}
|
||||
:pages-and-blocks [{:page {:build/journal today}
|
||||
:blocks [{:block/title "today child"}]}]})
|
||||
class-page (ldb/get-page @conn "Movie")
|
||||
property-page (d/entity @conn :user.property/rating)
|
||||
today-page (db-test/find-journal-by-journal-day @conn today)
|
||||
today-child (db-test/find-block-by-content @conn "today child")
|
||||
class-inverse (:inverse-outliner-ops
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid class-page) {}]]]}))
|
||||
property-inverse (:inverse-outliner-ops
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid property-page) {}]]]}))
|
||||
today-inverse (:inverse-outliner-ops
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid today-page) {}]]]}))]
|
||||
(is (some #(= :create-page (first %)) class-inverse))
|
||||
(is (some #(= :save-block (first %)) class-inverse))
|
||||
(is (= (:db/ident class-page)
|
||||
(get-in (some #(when (= :save-block (first %)) %) class-inverse)
|
||||
[1 0 :db/ident])))
|
||||
|
||||
(is (some #(= :upsert-property (first %)) property-inverse))
|
||||
(is (some #(= :save-block (first %)) property-inverse))
|
||||
(is (= (:db/ident property-page)
|
||||
(get-in (some #(when (= :save-block (first %)) %) property-inverse)
|
||||
[1 0 :db/ident])))
|
||||
|
||||
(is (not-any? #(= :restore-recycled (first %)) today-inverse))
|
||||
(let [today-insert-op (some #(when (= :insert-blocks (first %)) %) today-inverse)]
|
||||
(is (some? today-insert-op))
|
||||
(is (= (:block/uuid today-page)
|
||||
(get-in today-insert-op [1 1])))
|
||||
(is (= (:block/uuid today-child)
|
||||
(get-in today-insert-op [1 0 0 :block/uuid])))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-builds-inverse-for-all-supported-ops-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {:build/class-properties [:p1]}}
|
||||
:properties {:p1 {:logseq.property/type :default}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child-a"}
|
||||
{:block/title "child-b"}]}
|
||||
{:block/title "prop-block-1"
|
||||
:build/properties {:p1 "before-1"}}
|
||||
{:block/title "prop-block-2"}]}]})
|
||||
page (db-test/find-page-by-title @conn "page")
|
||||
parent (db-test/find-block-by-content @conn "parent")
|
||||
child-a (db-test/find-block-by-content @conn "child-a")
|
||||
child-b (db-test/find-block-by-content @conn "child-b")]
|
||||
(testing ":save-block"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :save-block
|
||||
:outliner-ops [[:save-block [{:block/uuid (:block/uuid child-a)
|
||||
:block/title "changed"} {}]]]})]
|
||||
(is (= :save-block (ffirst inverse-outliner-ops)))
|
||||
(is (= (:block/uuid child-a)
|
||||
(get-in inverse-outliner-ops [0 1 0 :block/uuid])))))
|
||||
|
||||
(testing ":insert-blocks"
|
||||
(let [inserted-uuid (random-uuid)
|
||||
tx-data [{:e 999999 :a :block/uuid :v inserted-uuid :added true}]
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn tx-data {:outliner-op :insert-blocks
|
||||
:outliner-ops [[:insert-blocks [[{:block/uuid inserted-uuid
|
||||
:block/title "new"}]
|
||||
(:db/id parent)
|
||||
{:sibling? false}]]]})]
|
||||
(is (= :delete-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= [inserted-uuid]
|
||||
(vec (get-in inverse-outliner-ops [0 1 0]))))))
|
||||
|
||||
(testing ":move-blocks"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :move-blocks
|
||||
:outliner-ops [[:move-blocks [[(:db/id child-b)]
|
||||
(:db/id parent)
|
||||
{:sibling? false}]]]})]
|
||||
(is (= :move-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= [(:block/uuid child-b)]
|
||||
(get-in inverse-outliner-ops [0 1 0])))))
|
||||
|
||||
(testing ":delete-blocks"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[(:db/id child-b)] {}]]]})]
|
||||
(is (= :insert-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= (:block/uuid child-b)
|
||||
(get-in inverse-outliner-ops [0 1 0 0 :block/uuid])))))
|
||||
|
||||
(testing ":create-page"
|
||||
(let [page-uuid (random-uuid)
|
||||
tx-data [{:e 1 :a :block/title :v "P2" :added true}
|
||||
{:e 1 :a :block/uuid :v page-uuid :added true}]
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn tx-data {:outliner-op :create-page
|
||||
:outliner-ops [[:create-page ["P2" {:redirect? false}]]]})]
|
||||
(is (= [[:delete-page [page-uuid {}]]]
|
||||
inverse-outliner-ops))))
|
||||
|
||||
(testing ":delete-page"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid page) {}]]]})]
|
||||
(is (= [[:restore-recycled [(:block/uuid page)]]]
|
||||
inverse-outliner-ops))))
|
||||
|
||||
(testing ":upsert-property"
|
||||
(let [property-ident :user.property/test-inverse
|
||||
expected-page-uuid (common-uuid/gen-uuid :db-ident-block-uuid property-ident)
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :upsert-property
|
||||
:outliner-ops [[:upsert-property [property-ident
|
||||
{:logseq.property/type :default}
|
||||
{:property-name "test-inverse"}]]]})]
|
||||
(is (= [[:delete-page [expected-page-uuid {}]]]
|
||||
inverse-outliner-ops))))))
|
||||
|
||||
(deftest build-history-action-metadata-direct-outdent-builds-indent-outdent-forward-and-inverse-test
|
||||
(testing "direct outdent keeps canonical indent-outdent forward and inverse ops"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child-1"}
|
||||
{:block/title "child-2"}
|
||||
{:block/title "child-3"}]}]}]})
|
||||
child-3 (db-test/find-block-by-content @conn "child-3")
|
||||
{:keys [tx-data db-after]} (run-direct-outdent conn child-3)
|
||||
tx-meta {:outliner-op :move-blocks
|
||||
:outliner-ops [[:indent-outdent-blocks [[(:db/id child-3)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn db-after tx-data tx-meta)]
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-3)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
forward-outliner-ops))
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-3)]
|
||||
true
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-direct-outdent-with-extra-moved-blocks-keeps-semantic-ops-test
|
||||
(testing "direct outdent keeps semantic indent-outdent op and inverse"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child-1"}
|
||||
{:block/title "child-2"}
|
||||
{:block/title "child-3"}]}]}]})
|
||||
child-2 (db-test/find-block-by-content @conn "child-2")
|
||||
{:keys [tx-data db-after]} (run-direct-outdent conn child-2)
|
||||
tx-meta {:outliner-op :move-blocks
|
||||
:outliner-ops [[:indent-outdent-blocks [[(:db/id child-2)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn db-after tx-data tx-meta)]
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-2)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
forward-outliner-ops))
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-2)]
|
||||
true
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
inverse-outliner-ops)))))
|
||||
251
deps/outliner/test/logseq/outliner/op_test.cljs
vendored
Normal file
251
deps/outliner/test/logseq/outliner/op_test.cljs
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
(ns logseq.outliner.op-test
|
||||
(:require [clojure.string :as string]
|
||||
[cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.op :as outliner-op]
|
||||
[logseq.outliner.property :as outliner-property]))
|
||||
|
||||
(deftest toggle-reaction-op
|
||||
(testing "toggles reactions via outliner ops"
|
||||
(let [user-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
now 1234]
|
||||
(ldb/transact! conn
|
||||
[{:block/uuid user-uuid
|
||||
:block/name "user"
|
||||
:block/title "user"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags #{:logseq.class/Page}}]
|
||||
{})
|
||||
(let [block (db-test/find-block-by-content @conn "Block")
|
||||
target-uuid (:block/uuid block)]
|
||||
(outliner-op/apply-ops! conn
|
||||
[[:toggle-reaction [target-uuid "+1" user-uuid]]]
|
||||
{})
|
||||
(let [block-entity (d/entity @conn [:block/uuid target-uuid])
|
||||
reactions (:logseq.property.reaction/_target block-entity)
|
||||
reaction (first reactions)]
|
||||
(is (= 1 (count reactions)))
|
||||
(is (uuid? (:block/uuid reaction)))
|
||||
(is (= "+1" (:logseq.property.reaction/emoji-id reaction)))
|
||||
(is (= (:db/id (d/entity @conn [:block/uuid user-uuid]))
|
||||
(:db/id (:logseq.property/created-by-ref reaction)))))
|
||||
(outliner-op/apply-ops! conn
|
||||
[[:toggle-reaction [target-uuid "+1" user-uuid]]]
|
||||
{})
|
||||
(let [block-entity (d/entity @conn [:block/uuid target-uuid])]
|
||||
(is (empty? (:logseq.property.reaction/_target block-entity))))))))
|
||||
|
||||
(deftest collapse-expand-blocks-op
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Parent"}]}])
|
||||
block (db-test/find-block-by-content @conn "Parent")
|
||||
block-id (:block/uuid block)]
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:collapse-expand-blocks [[{:block/uuid block-id
|
||||
:block/collapsed? true}]
|
||||
{}]]]
|
||||
{})
|
||||
(is (true? (:block/collapsed? (d/entity @conn [:block/uuid block-id]))))
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:collapse-expand-blocks [[{:block/uuid block-id
|
||||
:block/collapsed? false}]
|
||||
{}]]]
|
||||
{})
|
||||
(is (false? (:block/collapsed? (d/entity @conn [:block/uuid block-id]))))))
|
||||
|
||||
(deftest resolve-indent-outdent-parent-original-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Original"}]}])
|
||||
original (db-test/find-block-by-content @conn "Original")
|
||||
opts (#'outliner-op/resolve-indent-outdent-opts
|
||||
@conn
|
||||
{:parent-original {:block/uuid (:block/uuid original)}})]
|
||||
(is (= (:db/id original) (get-in opts [:parent-original :db/id])))))
|
||||
|
||||
(deftest apply-ops-plugin-property-sequence-test
|
||||
(testing "plugin property ops remain visible after a single apply-ops! batch"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-uuid (:block/uuid block)]
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:upsert-property [:plugin.property._test_plugin/x1 {:logseq.property/type :checkbox
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x1}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x1 true]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x2 {:logseq.property/type :url
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x2}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x2 "https://logseq.com"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x3 {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x3}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x3 1]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x4 {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:property-name :x4}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x4 1]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x5 {:logseq.property/type :json
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x5}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x5 "{\"foo\":\"bar\"}"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x6 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x6}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x6 "Page x"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x7 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:property-name :x7}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x7 "Page y"]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x7 "Page z"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x8 {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x8}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x8 "some content"]]]
|
||||
{})
|
||||
(let [block' (d/entity @conn [:block/uuid block-uuid])]
|
||||
(is (true? (:plugin.property._test_plugin/x1 block')))
|
||||
(is (= "https://logseq.com"
|
||||
(:block/title (:plugin.property._test_plugin/x2 block'))))
|
||||
(is (= 1
|
||||
(:logseq.property/value (:plugin.property._test_plugin/x3 block'))))
|
||||
(is (= #{1}
|
||||
(set (map :logseq.property/value (:plugin.property._test_plugin/x4 block')))))
|
||||
(is (= "{\"foo\":\"bar\"}" (:plugin.property._test_plugin/x5 block')))
|
||||
(is (= "page x"
|
||||
(:block/name (:plugin.property._test_plugin/x6 block'))))
|
||||
(is (= #{"page y" "page z"}
|
||||
(set (map :block/name (:plugin.property._test_plugin/x7 block')))))
|
||||
(is (= "some content"
|
||||
(:block/title (:plugin.property._test_plugin/x8 block'))))))))
|
||||
|
||||
(deftest remove-block-property-op-rejects-lookup-ref-block-id-test
|
||||
(testing "remove-block-property rejects lookup-ref block ids"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-uuid (:block/uuid block)]
|
||||
(outliner-property/set-block-property! conn
|
||||
[:block/uuid block-uuid]
|
||||
:logseq.property/order-list-type
|
||||
"number")
|
||||
(is (some? (:logseq.property/order-list-type
|
||||
(d/entity @conn [:block/uuid block-uuid]))))
|
||||
(is (thrown? js/Error
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:remove-block-property [[:block/uuid block-uuid]
|
||||
:logseq.property/order-list-type]]]
|
||||
{}))))))
|
||||
|
||||
(deftest direct-plugin-many-page-property-appends-values-test
|
||||
(testing "direct property operations keep both page values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-id (:db/id block)
|
||||
property-id :plugin.property._test_plugin/x7]
|
||||
(outliner-property/upsert-property! conn property-id
|
||||
{:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:property-name :x7})
|
||||
(outliner-property/set-block-property! conn block-id property-id "Page y")
|
||||
(outliner-property/set-block-property! conn block-id property-id "Page z")
|
||||
(is (= #{"page y" "page z"}
|
||||
(set (map :block/name
|
||||
(:plugin.property._test_plugin/x7 (d/entity @conn block-id)))))))))
|
||||
|
||||
(deftest apply-template-op-resolves-dynamic-variables-test
|
||||
(testing "apply-template resolves dynamic variables in block title and property values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "Target Page"}
|
||||
:blocks [{:block/title "target block"}]}
|
||||
{:page {:block/title "Templates"}
|
||||
:blocks [{:block/title "template root"
|
||||
:build/children [{:block/title "page is <% current page %>"}
|
||||
{:block/title "time block"
|
||||
:build/properties {:log-time "<%time%>"}}]}]}]
|
||||
:properties {:log-time {:logseq.property/type :default}}})
|
||||
target-page (ldb/get-page @conn "Target Page")
|
||||
template-root (db-test/find-block-by-content @conn "template root")
|
||||
target-block (db-test/find-block-by-content @conn "target block")
|
||||
template-blocks (->> (ldb/get-block-and-children @conn (:block/uuid template-root)
|
||||
{:include-property-block? true})
|
||||
rest)
|
||||
blocks-to-insert (cons (assoc (into {} (first template-blocks))
|
||||
:db/id (:db/id (first template-blocks))
|
||||
:logseq.property/used-template (:db/id template-root))
|
||||
(map (fn [block]
|
||||
(assoc (into {} block) :db/id (:db/id block)))
|
||||
(rest template-blocks)))
|
||||
_ (outliner-op/apply-ops! conn
|
||||
[[:apply-template [(:block/uuid template-root)
|
||||
(:block/uuid target-block)
|
||||
{:template-blocks blocks-to-insert}]]]
|
||||
{})
|
||||
page-var-block (db-test/find-block-by-content
|
||||
@conn
|
||||
(str "page is " (page-ref/->page-ref (:block/uuid target-page))))
|
||||
time-block (some->> (d/q '[:find [?b ...]
|
||||
:in $ ?title ?page-title
|
||||
:where
|
||||
[?b :block/title ?title]
|
||||
[?b :block/page ?page]
|
||||
[?page :block/title ?page-title]]
|
||||
@conn "time block" "Target Page")
|
||||
first
|
||||
(d/entity @conn))
|
||||
time-value (some (fn [[property-id value]]
|
||||
(when (= "log-time" (name property-id))
|
||||
value))
|
||||
(db-test/readable-properties time-block))]
|
||||
(is (some? page-var-block))
|
||||
(is (string? time-value))
|
||||
(is (not (string/blank? time-value)))
|
||||
(is (not (string/includes? time-value "<%"))))))
|
||||
|
||||
(deftest apply-ops-requires-uuid-block-ids-and-keyword-property-ids-test
|
||||
(testing "ops reject integer eids and accept UUID/keyword identifiers"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-id (:db/id block)
|
||||
block-uuid (:block/uuid block)
|
||||
property-kw :plugin.property._test_plugin/normalized-prop]
|
||||
(outliner-property/upsert-property! conn property-kw
|
||||
{:logseq.property/type :checkbox
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :normalized-prop})
|
||||
(let [property-id (:db/id (d/entity @conn property-kw))]
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:set-block-property [block-uuid property-kw true]]]
|
||||
{})
|
||||
(is (true? (property-kw (d/entity @conn [:block/uuid block-uuid]))))
|
||||
(is (thrown? js/Error
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:set-block-property [block-id property-kw true]]]
|
||||
{})))
|
||||
(is (thrown? js/Error
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:set-block-property [block-uuid property-id true]]]
|
||||
{})))))))
|
||||
206
deps/outliner/test/logseq/outliner/page_test.cljs
vendored
Normal file
206
deps/outliner/test/logseq/outliner/page_test.cljs
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
(ns logseq.outliner.page-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.frontend.db :as db-db]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.page :as outliner-page]))
|
||||
|
||||
(deftest create-class
|
||||
(let [conn (db-test/create-conn)
|
||||
_ (outliner-page/create! conn "movie" {:class? true})
|
||||
_ (outliner-page/create! conn "Movie" {:class? true})
|
||||
movie-class (ldb/get-case-page @conn "movie")
|
||||
Movie-class (ldb/get-case-page @conn "Movie")]
|
||||
|
||||
(is (ldb/class? movie-class) "Creates a class")
|
||||
(is (ldb/class? Movie-class) "Creates another class with a different case sensitive name")
|
||||
(is (not= movie-class Movie-class) "The two classes are not the same")))
|
||||
|
||||
(deftest create-namespace-pages
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/property1 {:logseq.property/type :default}}
|
||||
:classes {:class1 {}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}}]})]
|
||||
|
||||
(testing "Basic valid workflows"
|
||||
(let [[_ child-uuid] (outliner-page/create! conn "foo/bar/baz" {:split-namespace? true})
|
||||
child-page (d/entity @conn [:block/uuid child-uuid])
|
||||
;; Create a 2nd child page using existing parent pages
|
||||
[_ child-uuid2] (outliner-page/create! conn "foo/bar/baz2" {:split-namespace? true})
|
||||
child-page2 (d/entity @conn [:block/uuid child-uuid2])
|
||||
;; Create a child page for a class
|
||||
[_ child-uuid3] (outliner-page/create! conn "c1/c2" {:split-namespace? true :class? true})
|
||||
child-page3 (d/entity @conn [:block/uuid child-uuid3])
|
||||
library (ldb/get-built-in-page @conn common-config/library-page-name)
|
||||
bar (ldb/get-page @conn "bar")]
|
||||
(is (= ["foo"] (map :block/title (:block/_parent library)))
|
||||
"Namespace (non-class) pages are added to the Library page")
|
||||
(is (= ["baz" "baz2"] (map :block/title (:block/_parent bar)))
|
||||
"Child pages are created under the same parent")
|
||||
(is (= ["foo" "bar"] (map :block/title [(:block/parent (:block/parent child-page))
|
||||
(:block/parent child-page)]))
|
||||
"Child page with new parent has correct parents")
|
||||
(is (= (map :block/uuid (db-db/get-page-parents child-page))
|
||||
(map :block/uuid (db-db/get-page-parents child-page2)))
|
||||
"Child page with existing parents has correct parents")
|
||||
(is (= #{"Root Tag" "c1"} (set (map :block/title (ldb/get-classes-parents [child-page3]))))
|
||||
"Child class with new parent has correct parents")
|
||||
|
||||
(outliner-page/create! conn "foo/class1/baz3" {:split-namespace? true})
|
||||
(is (= #{"Tag" "Page"}
|
||||
(set (d/q '[:find [?tag-title ...]
|
||||
:where
|
||||
[?b :block/title "class1"]
|
||||
[?b :block/tags ?t]
|
||||
[?t :block/title ?tag-title]] @conn)))
|
||||
"Using an existing class page in a multi-parent namespace doesn't allow a page to have a class parent and instead creates a new page")))
|
||||
|
||||
(testing "Child pages with same name and different parents"
|
||||
(let [_ (outliner-page/create! conn "vim/keys" {:split-namespace? true})
|
||||
_ (outliner-page/create! conn "emacs/keys" {:split-namespace? true})]
|
||||
(is (= #{"vim" "emacs"}
|
||||
(->> (d/q '[:find [(pull ?b [{:block/parent [:block/title]}]) ...] :where [?b :block/title "keys"]] @conn)
|
||||
(map #(get-in % [:block/parent :block/title]))
|
||||
set))
|
||||
"Two child pages with same name exist and have different parents")))
|
||||
|
||||
(testing "Invalid workflows"
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Cannot create"
|
||||
(outliner-page/create! conn "class1/page" {:split-namespace? true}))
|
||||
"Page can't have a class parent")
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Cannot create"
|
||||
(outliner-page/create! conn "property1/page" {:split-namespace? true}))
|
||||
"Page can't have a property parent")
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Cannot create"
|
||||
(outliner-page/create! conn "property1/class" {:split-namespace? true :class? true}))
|
||||
"Class can't have a property parent"))))
|
||||
|
||||
(deftest create-page
|
||||
(let [conn (db-test/create-conn)
|
||||
[_ page-uuid] (outliner-page/create! conn "fooz" {})]
|
||||
(is (= "fooz" (:block/title (d/entity @conn [:block/uuid page-uuid])))
|
||||
"Page created correctly")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"can't include \"/"
|
||||
(outliner-page/create! conn "foo/bar" {}))
|
||||
"Page can't have '/'n title")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"can't include \"#\""
|
||||
(outliner-page/create! conn "foo#bar" {}))
|
||||
"Page can't have '#' in title")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"can't include \"#\""
|
||||
(outliner-page/create! conn "#tagstyle" {}))
|
||||
"Page can't have leading '#' in title")))
|
||||
|
||||
(deftest delete-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "D1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
d1 (ldb/get-page @conn "D1")
|
||||
b1 (db-test/find-block-by-content @conn "b1")]
|
||||
(ldb/transact! conn [{:db/id (:db/id b1)
|
||||
:block/title (str "b1 " (page-ref/->page-ref (:block/uuid d1)))
|
||||
:block/refs #{(:db/id d1)}}])
|
||||
(is (contains? (set (map :db/id (:block/refs (d/entity @conn (:db/id b1)))))
|
||||
(:db/id d1)))
|
||||
(outliner-page/delete! conn (:block/uuid d1))
|
||||
(let [d1' (d/entity @conn (:db/id d1))
|
||||
b1' (d/entity @conn (:db/id b1))
|
||||
recycle-page (ldb/get-built-in-page @conn "Recycle")]
|
||||
(is (some? d1'))
|
||||
(is (some? b1'))
|
||||
(is (= (:block/uuid recycle-page) (:block/uuid (:block/parent d1'))))
|
||||
(is (integer? (:logseq.property/deleted-at d1')))
|
||||
(is (nil? (:block/raw-title b1')))
|
||||
(is (contains? (set (map :db/id (:block/refs b1')))
|
||||
(:db/id d1)))
|
||||
(is (= (:block/uuid d1') (:block/uuid (:block/page b1')))))))
|
||||
|
||||
(deftest delete-class-page-hard-retracts-page-tree
|
||||
(let [conn (db-test/create-conn-with-blocks {:classes {:Movie {}}})
|
||||
class-page (ldb/get-page @conn "Movie")
|
||||
child-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:block/uuid child-uuid
|
||||
:block/title "class child"
|
||||
:block/page (:db/id class-page)
|
||||
:block/parent (:db/id class-page)
|
||||
:block/order (db-order/gen-key)}])]
|
||||
(outliner-page/delete! conn (:block/uuid class-page))
|
||||
(is (nil? (d/entity @conn [:block/uuid (:block/uuid class-page)])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest delete-property-page-hard-retracts-page-tree
|
||||
(let [conn (db-test/create-conn-with-blocks {:properties {:rating {:logseq.property/type :number}}})
|
||||
property-page (d/entity @conn :user.property/rating)
|
||||
child-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:block/uuid child-uuid
|
||||
:block/title "property child"
|
||||
:block/page (:db/id property-page)
|
||||
:block/parent (:db/id property-page)
|
||||
:block/order (db-order/gen-key)}])]
|
||||
(outliner-page/delete! conn (:block/uuid property-page))
|
||||
(is (nil? (d/entity @conn :user.property/rating)))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest create-journal
|
||||
(let [conn (db-test/create-conn)
|
||||
[_ page-uuid] (outliner-page/create! conn "Dec 16th, 2024" {})]
|
||||
|
||||
(is (= "Dec 16th, 2024" (:block/title (d/entity @conn [:block/uuid page-uuid])))
|
||||
"Journal created correctly")
|
||||
|
||||
(is (= [:logseq.class/Journal]
|
||||
(->> (d/entity @conn [:block/uuid page-uuid])
|
||||
:block/tags
|
||||
(map #(:db/ident (d/entity @conn (:db/id %))))))
|
||||
"New journal only has Journal tag")))
|
||||
|
||||
(deftest create-journal-keeps-default-block-name-with-custom-title-format
|
||||
(let [conn (db-test/create-conn)
|
||||
_ (d/transact! conn [[:db/add :logseq.class/Journal :logseq.property.journal/title-format "yyyy-MM-dd EEEE"]])
|
||||
[_ page-uuid] (outliner-page/create! conn "Dec 16th, 2024" {})
|
||||
page (d/entity @conn [:block/uuid page-uuid])
|
||||
default-name (-> (:block/journal-day page)
|
||||
(date-time-util/int->journal-title date-time-util/default-journal-title-formatter)
|
||||
common-util/page-name-sanity-lc)]
|
||||
(is (= "2024-12-16 Monday" (:block/title page))
|
||||
"Journal title follows configured formatter")
|
||||
(is (= default-name (:block/name page))
|
||||
"Journal block/name remains the default formatter, independent of title format")))
|
||||
|
||||
(deftest create-slash-formatted-journal-does-not-create-namespace-pages
|
||||
(let [conn (db-test/create-conn)
|
||||
_ (d/transact! conn [[:db/add :logseq.class/Journal :logseq.property.journal/title-format "yyyy/MM/dd"]])
|
||||
[_ page-uuid] (outliner-page/create! conn "May 18th, 2026" {:split-namespace? true
|
||||
:journal? true})
|
||||
page (d/entity @conn [:block/uuid page-uuid])]
|
||||
(is (= "2026/05/18" (:block/title page))
|
||||
"Journal title follows slash title format")
|
||||
(is (= (common-uuid/gen-uuid :journal-page-uuid 20260518) (:block/uuid page))
|
||||
"Journal page has the standard journal uuid")
|
||||
(is (nil? (ldb/get-page @conn "2026"))
|
||||
"Journal title is not split into a year namespace page")
|
||||
(is (nil? (ldb/get-page @conn "05"))
|
||||
"Journal title is not split into a month namespace page")
|
||||
(is (nil? (ldb/get-page @conn "18"))
|
||||
"Journal title is not split into a day namespace page")))
|
||||
47
deps/outliner/test/logseq/outliner/pipeline_test.cljs
vendored
Normal file
47
deps/outliner/test/logseq/outliner/pipeline_test.cljs
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
(ns logseq.outliner.pipeline-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.pipeline :as outliner-pipeline]))
|
||||
|
||||
(deftest block-content-refs
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"} :blocks [{:block/title "b1"}]}])
|
||||
block (db-test/find-block-by-content @conn "b1")]
|
||||
(assert block)
|
||||
(is (= [(:db/id block)]
|
||||
(outliner-pipeline/block-content-refs @conn
|
||||
{:block/title (str "ref to " (page-ref/->page-ref (:block/uuid block)))})))))
|
||||
|
||||
(deftest db-rebuild-block-refs-for-query-block
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "Todo query"
|
||||
:build/tags [:logseq.class/Query]
|
||||
:build/properties
|
||||
{:logseq.property/query
|
||||
{:build/property-value :block
|
||||
:block/title "(task Todo)"}}}]}]})
|
||||
block (db-test/find-block-by-content @conn "Todo query")
|
||||
refs (set (outliner-pipeline/db-rebuild-block-refs @conn block))
|
||||
query-property-id (:db/id (d/entity @conn :logseq.property/query))
|
||||
query-class-id (:db/id (d/entity @conn :logseq.class/Query))]
|
||||
(is (some? query-property-id)
|
||||
"Sanity: :logseq.property/query entity exists")
|
||||
(is (contains? refs query-class-id)
|
||||
"#Query class tag is included in :block/refs")
|
||||
(is (not (contains? refs query-property-id))
|
||||
"#Query block does not reference logseq.property/query through :block/refs")))
|
||||
|
||||
(deftest db-rebuild-block-refs-removes-recursive-self-ref
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "self"}]}])
|
||||
block (db-test/find-block-by-content @conn "self")
|
||||
block' (assoc (d/pull @conn '[:db/id :block/uuid :block/title] (:db/id block))
|
||||
:block/title
|
||||
(str "self " (page-ref/->page-ref (:block/uuid block))))]
|
||||
(is (empty? (outliner-pipeline/db-rebuild-block-refs @conn block'))
|
||||
"A block should not rebuild a recursive ref to itself")))
|
||||
694
deps/outliner/test/logseq/outliner/property_test.cljs
vendored
Normal file
694
deps/outliner/test/logseq/outliner/property_test.cljs
vendored
Normal file
@@ -0,0 +1,694 @@
|
||||
(ns logseq.outliner.property-test
|
||||
(:require [cljs.test :refer [deftest is testing are]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.property :as outliner-property]))
|
||||
|
||||
(deftest upsert-property!
|
||||
(testing "Creates a property"
|
||||
(let [conn (db-test/create-conn-with-blocks [])
|
||||
_ (outliner-property/upsert-property! conn nil {:logseq.property/type :number} {:property-name "num"})]
|
||||
(is (= :number
|
||||
(:logseq.property/type (d/entity @conn :user.property/num)))
|
||||
"Creates property with property-name")))
|
||||
|
||||
(testing "Updates a property"
|
||||
(let [conn (db-test/create-conn-with-blocks {:properties {:num {:logseq.property/type :number}}})
|
||||
old-updated-at (:block/updated-at (d/entity @conn :user.property/num))]
|
||||
|
||||
(testing "and change its cardinality"
|
||||
(outliner-property/upsert-property! conn :user.property/num {:db/cardinality :many} {})
|
||||
(is (db-property/many? (d/entity @conn :user.property/num)))
|
||||
(is (> (:block/updated-at (d/entity @conn :user.property/num))
|
||||
old-updated-at)))
|
||||
|
||||
(testing "and change its type from a ref to a non-ref type"
|
||||
(outliner-property/upsert-property! conn :user.property/num {:logseq.property/type :checkbox} {})
|
||||
(is (= :checkbox (:logseq.property/type (d/entity @conn :user.property/num))))
|
||||
(is (= nil (:db/valueType (d/entity @conn :user.property/num)))))))
|
||||
|
||||
(testing "Multiple properties that generate the same initial :db/ident"
|
||||
(let [conn (db-test/create-conn-with-blocks [])]
|
||||
(outliner-property/upsert-property! conn nil {:logseq.property/type :default} {:property-name "p1"})
|
||||
(outliner-property/upsert-property! conn nil {} {:property-name "p1"})
|
||||
(outliner-property/upsert-property! conn nil {} {:property-name "p1"})
|
||||
|
||||
(is (= {:block/name "p1" :block/title "p1" :logseq.property/type :default}
|
||||
(select-keys (d/entity @conn :user.property/p1) [:block/name :block/title :logseq.property/type]))
|
||||
"Existing db/ident does not get modified")
|
||||
(is (= "p1"
|
||||
(:block/title (d/entity @conn :user.property/p1-1)))
|
||||
"2nd property gets unique ident")
|
||||
(is (= "p1"
|
||||
(:block/title (d/entity @conn :user.property/p1-2)))
|
||||
"3rd property gets unique ident"))))
|
||||
|
||||
(deftest upsert-property-rejects-type-change-with-existing-data
|
||||
(testing "Changing type is rejected when property has values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:status "active"}}]}])]
|
||||
(is (thrown-with-msg?
|
||||
js/Error #"Disallowed type change with existing data"
|
||||
(outliner-property/upsert-property! conn :user.property/status {:logseq.property/type :number} {})))))
|
||||
|
||||
(testing "Changing type is allowed when property has no values"
|
||||
(let [conn (db-test/create-conn-with-blocks {:properties {:empty-prop {:logseq.property/type :default}}})]
|
||||
(outliner-property/upsert-property! conn :user.property/empty-prop {:logseq.property/type :number} {})
|
||||
(is (= :number (:logseq.property/type (d/entity @conn :user.property/empty-prop)))))))
|
||||
|
||||
(deftest convert-property-input-string
|
||||
(testing "Convert property input string according to its schema type"
|
||||
(let [test-uuid (random-uuid)]
|
||||
(are [x y]
|
||||
(= (let [[schema-type value] x]
|
||||
(outliner-property/convert-property-input-string nil {:logseq.property/type schema-type} value)) y)
|
||||
[:number "1"] 1
|
||||
[:number "1.2"] 1.2
|
||||
[:url test-uuid] test-uuid
|
||||
[:date test-uuid] test-uuid
|
||||
[:any test-uuid] test-uuid
|
||||
[nil test-uuid] test-uuid))))
|
||||
|
||||
(deftest create-property-text-block!
|
||||
(testing "Create a new :default property value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:default "foo"}}
|
||||
{:block/title "b2"}]}])
|
||||
block (db-test/find-block-by-content @conn "b2")
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/default "" {})
|
||||
new-property-value (:user.property/default (db-test/find-block-by-content @conn "b2"))]
|
||||
|
||||
(is (some? (:db/id new-property-value)) "New property value created")
|
||||
(is (= "" (db-property/property-value-content new-property-value))
|
||||
"Property value has correct content")
|
||||
(is (= :user.property/default
|
||||
(get-in (d/entity @conn (:db/id new-property-value)) [:logseq.property/created-from-property :db/ident]))
|
||||
"Has correct created-from-property")))
|
||||
|
||||
(testing "Create cases for a new :one :number property value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 2}}
|
||||
{:block/title "b2"}]}])
|
||||
block (db-test/find-block-by-content @conn "b2")
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num "3" {})
|
||||
new-property-value (:user.property/num (db-test/find-block-by-content @conn "b2"))]
|
||||
|
||||
(is (some? (:db/id new-property-value)) "New property value created")
|
||||
(is (= 3 (db-property/property-value-content new-property-value))
|
||||
"Property value has correct content")
|
||||
(is (= :user.property/num
|
||||
(get-in (d/entity @conn (:db/id new-property-value)) [:logseq.property/created-from-property :db/ident]))
|
||||
"Has correct created-from-property")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't convert"
|
||||
(outliner-property/create-property-text-block! conn (:db/id block) :user.property/num "Not a number" {}))
|
||||
"Wrong value isn't transacted")))
|
||||
|
||||
(testing "Create new :many :number property values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num-many #{2}}}
|
||||
{:block/title "b2"}]}])
|
||||
block (db-test/find-block-by-content @conn "b2")
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num-many "3" {})
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num-many "4" {})
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num-many "5" {})
|
||||
new-property-values (:user.property/num-many (db-test/find-block-by-content @conn "b2"))]
|
||||
|
||||
(is (seq new-property-values) "New property values created")
|
||||
(is (= #{3 4 5} (set (map db-property/property-value-content new-property-values)))
|
||||
"Property value has correct content"))))
|
||||
|
||||
(deftest set-block-property-basic-cases
|
||||
(testing "Set a :number value with existing value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 2}}
|
||||
{:block/title "b2"}]}])
|
||||
property-value (:user.property/num (db-test/find-block-by-content @conn "b1"))
|
||||
_ (assert (:db/id property-value))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/num (:db/id property-value))]
|
||||
(is (= (:db/id property-value)
|
||||
(:db/id (:user.property/num (db-test/find-block-by-content @conn "b2")))))
|
||||
(let [empty-placeholder-id (:db/id (d/entity @conn :logseq.property/empty-placeholder))]
|
||||
(outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/num empty-placeholder-id)
|
||||
(is (= empty-placeholder-id
|
||||
(:logseq.property/value (:user.property/num (d/entity @conn [:block/uuid block-uuid]))))))))
|
||||
|
||||
(testing "Update a :number value with existing value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 2}}
|
||||
{:block/title "b2" :build/properties {:num 3}}]}])
|
||||
property-value (:user.property/num (db-test/find-block-by-content @conn "b1"))
|
||||
_ (assert (:db/id property-value))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/num (:db/id property-value))]
|
||||
(is (= (:db/id property-value)
|
||||
(:db/id (:user.property/num (db-test/find-block-by-content @conn "b2"))))))))
|
||||
|
||||
(deftest set-block-property-with-non-ref-values
|
||||
(testing "Setting :default with same property value reuses existing entity"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:logseq.property/order-list-type "number"}}
|
||||
{:block/title "b2"}]}])
|
||||
property-value (:logseq.property/order-list-type (db-test/find-block-by-content @conn "b1"))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :logseq.property/order-list-type "number")]
|
||||
(is (some? (:db/id (:logseq.property/order-list-type (db-test/find-block-by-content @conn "b2"))))
|
||||
"New block has property set")
|
||||
(is (= (:db/id property-value)
|
||||
(:db/id (:logseq.property/order-list-type (db-test/find-block-by-content @conn "b2")))))))
|
||||
|
||||
(testing "Setting :checkbox with same property value reuses existing entity"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:checkbox true}}
|
||||
{:block/title "b2"}]}])
|
||||
property-value (:user.property/checkbox (db-test/find-block-by-content @conn "b1"))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/checkbox true)]
|
||||
(is (true? (:user.property/checkbox (db-test/find-block-by-content @conn "b2")))
|
||||
"New block has property set")
|
||||
(is (= property-value (:user.property/checkbox (db-test/find-block-by-content @conn "b2")))))))
|
||||
|
||||
(deftest remove-block-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:default "foo"}}]}])
|
||||
block (db-test/find-block-by-content @conn "b1")
|
||||
_ (assert (:user.property/default block))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/remove-block-property! conn [:block/uuid (:block/uuid block)] :user.property/default)
|
||||
updated-block (db-test/find-block-by-content @conn "b1")]
|
||||
(is (some? updated-block))
|
||||
(is (nil? (:user.property/default updated-block)) "Block property is deleted")))
|
||||
|
||||
(deftest batch-set-property!
|
||||
(testing "Set built-in property values for multiple blocks"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "item 1"}
|
||||
{:block/title "item 2"}]}])
|
||||
block-ids (map #(-> (db-test/find-block-by-content @conn %) :block/uuid) ["item 1" "item 2"])
|
||||
_ (outliner-property/batch-set-property! conn block-ids :logseq.property/order-list-type "number")
|
||||
updated-blocks (map #(db-test/find-block-by-content @conn %) ["item 1" "item 2"])]
|
||||
(is (= ["number" "number"]
|
||||
(map #(db-property/property-value-content (:logseq.property/order-list-type %))
|
||||
updated-blocks))
|
||||
"Property values are batch set")))
|
||||
|
||||
(testing "Set custom default-many property values from string vector"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
block-id (:block/uuid (db-test/find-block-by-content @conn "target"))]
|
||||
(outliner-property/batch-set-property! conn [block-id] :user.property/reproducible-steps ["Step 1" "Step 2" "Step 3"])
|
||||
(is (= #{"Step 1" "Step 2" "Step 3"}
|
||||
(->> (:user.property/reproducible-steps (d/entity @conn [:block/uuid block-id]))
|
||||
(map db-property/property-value-content)
|
||||
set))
|
||||
"String vector values are persisted as many property values")))
|
||||
|
||||
(testing "Set custom default-many property values from id vector"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "source"
|
||||
:build/properties {:user.property/reproducible-steps #{"Step 1" "Step 2" "Step 3"}}}
|
||||
{:block/title "target"}]}]})
|
||||
source-values (->> (:user.property/reproducible-steps (db-test/find-block-by-content @conn "source"))
|
||||
(map :db/id)
|
||||
vec)
|
||||
target-id (:block/uuid (db-test/find-block-by-content @conn "target"))]
|
||||
(outliner-property/batch-set-property! conn [target-id] :user.property/reproducible-steps source-values)
|
||||
(is (= #{"Step 1" "Step 2" "Step 3"}
|
||||
(->> (:user.property/reproducible-steps (d/entity @conn [:block/uuid target-id]))
|
||||
(map db-property/property-value-content)
|
||||
set))
|
||||
"Id vector values are persisted as many property values")))
|
||||
|
||||
(testing "Invalid many values throw and don't partially persist"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
block-id (:block/uuid (db-test/find-block-by-content @conn "target"))]
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Schema validation failed"
|
||||
(outliner-property/batch-set-property! conn [block-id] :user.property/reproducible-steps [999999])))
|
||||
(is (nil? (:user.property/reproducible-steps (d/entity @conn [:block/uuid block-id])))
|
||||
"No partial values are persisted on failure"))))
|
||||
|
||||
(deftest status-property-setting-classes
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Cat {}
|
||||
:Project {:build/class-properties [:logseq.property/status]}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title ""}
|
||||
{:block/title "cat task" :build/tags [:Cat]}
|
||||
{:block/title "project task" :build/tags [:Project]}]}]})
|
||||
page1 (:block/uuid (db-test/find-page-by-title @conn "page1"))
|
||||
[empty-task cat-task project]
|
||||
(map #(:block/uuid (db-test/find-block-by-content @conn %)) ["" "cat task" "project task"])]
|
||||
|
||||
(outliner-property/batch-set-property! conn [empty-task] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= [:logseq.class/Task]
|
||||
(mapv :db/ident (:block/tags (d/entity @conn [:block/uuid empty-task]))))
|
||||
"Adds Task to block when it is not tagged")
|
||||
|
||||
(outliner-property/batch-set-property! conn [page1] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= #{:logseq.class/Task :logseq.class/Page}
|
||||
(set (map :db/ident (:block/tags (d/entity @conn [:block/uuid page1])))))
|
||||
"Adds Task to page without tag")
|
||||
|
||||
(outliner-property/batch-set-property! conn [cat-task] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= #{:logseq.class/Task :user.class/Cat}
|
||||
(set (map :db/ident (:block/tags (d/entity @conn [:block/uuid cat-task])))))
|
||||
"Adds Task to tagged block when its classes do not provide status")
|
||||
|
||||
(outliner-property/batch-set-property! conn [project] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= [:user.class/Project]
|
||||
(mapv :db/ident (:block/tags (d/entity @conn [:block/uuid project]))))
|
||||
"Doesn't add Task to block when its class provides status")))
|
||||
|
||||
(deftest batch-set-property-rejects-private-built-in-entity
|
||||
(let [conn (db-test/create-conn)
|
||||
placeholder (d/entity @conn :logseq.property/empty-placeholder)
|
||||
prop (d/entity @conn :logseq.property/description)]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-property/batch-set-property! conn [(:db/id placeholder)] (:db/ident prop) "hacked"))))))
|
||||
|
||||
(deftest batch-remove-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:C1 {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "item 1" :build/properties {:logseq.property/order-list-type "number"}}
|
||||
{:block/title "item 2" :build/properties {:logseq.property/order-list-type "number"}}]}]})
|
||||
block-ids (map #(-> (db-test/find-block-by-content @conn %) :block/uuid) ["item 1" "item 2"])
|
||||
_ (outliner-property/batch-remove-property! conn block-ids :logseq.property/order-list-type)
|
||||
updated-blocks (map #(db-test/find-block-by-content @conn %) ["item 1" "item 2"])]
|
||||
(is (= [nil nil]
|
||||
(map :logseq.property/order-list-type updated-blocks))
|
||||
"Property values are batch removed")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove private"
|
||||
(outliner-property/batch-remove-property! conn [(:db/id (db-test/find-page-by-title @conn "page1"))] :block/tags)))
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove required"
|
||||
(outliner-property/batch-remove-property! conn [(:db/id (d/entity @conn :user.class/C1))] :logseq.property.class/extends)))))
|
||||
|
||||
(deftest batch-remove-property-rejects-private-built-in-entity
|
||||
(let [conn (db-test/create-conn)
|
||||
placeholder (d/entity @conn :logseq.property/empty-placeholder)
|
||||
prop (d/entity @conn :logseq.property/description)]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-property/batch-remove-property! conn [(:db/id placeholder)] (:db/ident prop)))))))
|
||||
|
||||
(deftest add-existing-values-to-closed-values!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 1}}
|
||||
{:block/title "b2" :build/properties {:num 2}}]}])
|
||||
values (map (fn [d] (:block/uuid (d/entity @conn (:v d)))) (d/datoms @conn :avet :user.property/num))
|
||||
_ (outliner-property/add-existing-values-to-closed-values! conn :user.property/num values)]
|
||||
(is (= [1 2]
|
||||
(map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num)))))))
|
||||
|
||||
(deftest add-existing-generated-value-to-closed-values-reparents-to-property
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
property (d/entity @conn :user.property/reproducible-steps)
|
||||
block (db-test/find-block-by-content @conn "target")
|
||||
block-uuid (:block/uuid block)]
|
||||
(outliner-property/batch-set-property! conn [block-uuid] :user.property/reproducible-steps ["Step 1"])
|
||||
(let [generated-value (first (:user.property/reproducible-steps (d/entity @conn [:block/uuid block-uuid])))
|
||||
value-uuid (:block/uuid generated-value)]
|
||||
(is (= (:db/id block) (:db/id (:block/parent generated-value))))
|
||||
|
||||
(outliner-property/add-existing-values-to-closed-values! conn :user.property/reproducible-steps [value-uuid])
|
||||
(let [closed-value (d/entity @conn [:block/uuid value-uuid])]
|
||||
(is (= #{(:db/id property)}
|
||||
(set (map :db/id (:block/closed-value-property closed-value)))))
|
||||
(is (= (:db/id property) (:db/id (:block/parent closed-value))))
|
||||
(is (= (:db/id property) (:db/id (:block/page closed-value))))
|
||||
(is (= "Step 1" (db-property/closed-value-content closed-value)))
|
||||
|
||||
(outliner-core/delete-blocks! conn [(d/entity @conn [:block/uuid block-uuid])] {})
|
||||
(is (some? (d/entity @conn [:block/uuid value-uuid])))))))
|
||||
|
||||
(deftest upsert-closed-value!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:num {:build/closed-values [{:uuid (random-uuid) :value 2}]
|
||||
:logseq.property/type :number}}})]
|
||||
|
||||
(testing "Add non-number choice shouldn't work"
|
||||
(is
|
||||
(thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't convert"
|
||||
(outliner-property/upsert-closed-value! conn :user.property/num {:value "not a number"}))))
|
||||
|
||||
(testing "Can't add existing choice"
|
||||
(is
|
||||
(thrown-with-msg?
|
||||
js/Error
|
||||
#"Closed value choice already exists"
|
||||
(outliner-property/upsert-closed-value! conn :user.property/num {:value 2}))))
|
||||
|
||||
(testing "Add choice successfully"
|
||||
(let [_ (outliner-property/upsert-closed-value! conn :user.property/num {:value 3})
|
||||
b (first (d/q '[:find [(pull ?b [*]) ...] :where [?b :logseq.property/value 3]] @conn))]
|
||||
(is (ldb/closed-value? (d/entity @conn (:db/id b))))
|
||||
(is (= [2 3]
|
||||
(map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num)))))))
|
||||
|
||||
(testing "Update choice successfully"
|
||||
(let [b (first (d/q '[:find [(pull ?b [*]) ...] :where [?b :logseq.property/value 2]] @conn))
|
||||
_ (outliner-property/upsert-closed-value! conn :user.property/num {:id (:block/uuid b)
|
||||
:value 4
|
||||
:description "choice 4"})
|
||||
updated-b (d/entity @conn [:block/uuid (:block/uuid b)])]
|
||||
(is (= 4 (db-property/closed-value-content updated-b)))
|
||||
(is (= "choice 4" (db-property/property-value-content (:logseq.property/description updated-b))))))))
|
||||
|
||||
(deftest delete-closed-value!
|
||||
(let [closed-value-uuid (random-uuid)
|
||||
used-closed-value-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:default {:build/closed-values [{:uuid closed-value-uuid :value "foo"}
|
||||
{:uuid used-closed-value-uuid :value "bar"}]
|
||||
:logseq.property/type :default}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :user.property/default [:block/uuid used-closed-value-uuid]}]}]})
|
||||
_ (assert (:user.property/default (db-test/find-block-by-content @conn "b1")))
|
||||
property-id (:db/id (d/entity @conn :user.property/default))
|
||||
closed-value-id (:db/id (d/entity @conn [:block/uuid closed-value-uuid]))
|
||||
child-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:block/uuid child-uuid
|
||||
:block/title "closed value child"
|
||||
:block/page property-id
|
||||
:block/parent closed-value-id
|
||||
:block/order "a0"}])
|
||||
_ (outliner-property/delete-closed-value! conn property-id [:block/uuid closed-value-uuid])]
|
||||
(is (nil? (d/entity @conn [:block/uuid closed-value-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))
|
||||
(is (= [used-closed-value-uuid]
|
||||
(mapv :block/uuid (:block/_closed-value-property (d/entity @conn :user.property/default)))))))
|
||||
|
||||
(deftest class-add-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {}}
|
||||
:properties {:p1 {:logseq.property/type :default}
|
||||
:p2 {:logseq.property/type :default}}})
|
||||
_ (outliner-property/class-add-property! conn :user.class/c1 :user.property/p1)
|
||||
_ (outliner-property/class-add-property! conn :user.class/c1 :user.property/p2)]
|
||||
(is (= [:user.property/p1 :user.property/p2]
|
||||
(map :db/ident (:logseq.property.class/properties (d/entity @conn :user.class/c1)))))))
|
||||
|
||||
(deftest class-remove-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {:build/class-properties [:p1 :p2]}}})
|
||||
_ (outliner-property/class-remove-property! conn :user.class/c1 :user.property/p1)]
|
||||
(is (= [:user.property/p2]
|
||||
(map :db/ident (:logseq.property.class/properties (d/entity @conn :user.class/c1)))))))
|
||||
|
||||
(deftest get-block-classes-properties
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {:build/class-properties [:p1]}
|
||||
:c2 {:build/class-properties [:p2 :p3]}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "p1"}
|
||||
:blocks [{:block/title "o1"
|
||||
:build/tags [:c1 :c2]}]}]})
|
||||
block (db-test/find-block-by-content @conn "o1")]
|
||||
(is (= [:user.property/p1 :user.property/p2 :user.property/p3]
|
||||
(map :db/ident (:classes-properties (outliner-property/get-block-classes-properties @conn (:db/id block))))))))
|
||||
|
||||
(deftest property-with-other-position-default-bottom-rules
|
||||
(testing "explicit non-properties ui-position remains other-position"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :number
|
||||
:logseq.property/ui-position :block-left}))))
|
||||
|
||||
(testing "number property without explicit ui-position defaults to bottom position"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :number}))))
|
||||
|
||||
(testing "default property without closed values stays in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :default
|
||||
:property/closed-values []}))))
|
||||
|
||||
(testing "default property with closed values is positioned in bottom row"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :default
|
||||
:property/closed-values [{:db/id 1}]}))))
|
||||
|
||||
(testing "url property without explicit ui-position stays in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :url}))))
|
||||
|
||||
(testing "explicit left/right ui-position remains in positioned rows"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :url
|
||||
:logseq.property/ui-position :block-left}))))
|
||||
|
||||
(testing "explicit properties ui-position keeps property in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :number
|
||||
:logseq.property/ui-position :properties}))))
|
||||
|
||||
(testing "many node property without explicit ui-position defaults to bottom position"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :node
|
||||
:db/cardinality :db.cardinality/many}))))
|
||||
|
||||
(testing "bidirectional config property stays in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :logseq.property.class/enable-bidirectional?
|
||||
:logseq.property/type :checkbox}))))
|
||||
|
||||
(testing "tag properties and schema-related properties stay in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :block/tags
|
||||
:logseq.property/type :class})))
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :logseq.property.class/properties
|
||||
:logseq.property/type :property})))
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :logseq.property/public?
|
||||
:logseq.property/type :checkbox}))))
|
||||
|
||||
(testing "a schema property is part of the shared schema set"
|
||||
(is (contains? db-property/schema-properties :logseq.property/public?))))
|
||||
|
||||
(deftest get-block-positioned-properties-filters-non-public
|
||||
(let [conn (db-test/create-conn-with-blocks [])
|
||||
journal-class (d/entity @conn :logseq.class/Journal)
|
||||
positioned-properties (outliner-property/get-block-positioned-properties @conn (:db/id journal-class) :block-below)]
|
||||
(is (every? #(not (false? (:logseq.property/public? %))) positioned-properties))
|
||||
(is (not-any? #(= :logseq.property.journal/title-format (:db/ident %))
|
||||
positioned-properties))))
|
||||
|
||||
(deftest get-block-positioned-properties-keeps-empty-inherited-tag-properties
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:authors {:logseq.property/type :node
|
||||
:db/cardinality :db.cardinality/many}}
|
||||
:classes {:Paper {:build/class-properties [:authors]}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "paper1"
|
||||
:build/tags [:Paper]}]}]})
|
||||
paper1 (db-test/find-block-by-content @conn "paper1")
|
||||
positioned-properties (outliner-property/get-block-positioned-properties @conn (:db/id paper1) :block-below)
|
||||
positioned-idents (set (map :db/ident positioned-properties))]
|
||||
(is (contains? positioned-idents :user.property/authors))))
|
||||
|
||||
(deftest extends-cycle
|
||||
(testing "Fail when creating a cycle of extends"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Class1 {}
|
||||
:Class2 {}
|
||||
:Class3 {}}})
|
||||
db @conn
|
||||
class1 (d/entity db :user.class/Class1)
|
||||
class2 (d/entity db :user.class/Class2)
|
||||
class3 (d/entity db :user.class/Class3)]
|
||||
(outliner-property/set-block-property! conn (:db/id class1) :logseq.property.class/extends (:db/id class2))
|
||||
(outliner-property/set-block-property! conn (:db/id class2) :logseq.property.class/extends (:db/id class3))
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Extends cycle"
|
||||
(outliner-property/set-block-property! conn (:db/id class3) :logseq.property.class/extends (:db/id class1)))
|
||||
"Extends cycle"))))
|
||||
|
||||
(deftest extends-redundant-direct-parent-cleanup
|
||||
(testing "Clean redundant direct parents from descendants when a class gets a new parent"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:A {}
|
||||
:B {}
|
||||
:C {:build/class-extends [:B]}
|
||||
:D {:build/class-extends [:A :C]}}})
|
||||
b (d/entity @conn :user.class/B)]
|
||||
(outliner-property/set-block-property! conn
|
||||
(:db/id b)
|
||||
:logseq.property.class/extends
|
||||
(:db/id (d/entity @conn :user.class/A)))
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D keeps only the nearest direct parent"))))
|
||||
|
||||
(deftest extends-redundant-cleanup-with-lookup-ref-parent
|
||||
(testing "Treat lookup refs as a single parent reference"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:A {}
|
||||
:B {}
|
||||
:C {:build/class-extends [:B]}
|
||||
:D {:build/class-extends [:A :C]}}})]
|
||||
(outliner-property/set-block-property! conn
|
||||
(:db/id (d/entity @conn :user.class/B))
|
||||
:logseq.property.class/extends
|
||||
[:db/ident :user.class/A])
|
||||
(is (= [:user.class/A]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/B))))
|
||||
"B uses the lookup ref as one parent")
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D removes the direct parent inherited through C"))))
|
||||
|
||||
(deftest extends-redundant-cleanup-with-keyword-vector-parents
|
||||
(testing "Treat a vector of class idents as multiple parents"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:A {}
|
||||
:B {}
|
||||
:C {:build/class-extends [:A]}
|
||||
:D {:build/class-extends [:A :B]}}})]
|
||||
(outliner-property/batch-set-property! conn
|
||||
[(:db/id (d/entity @conn :user.class/B))]
|
||||
:logseq.property.class/extends
|
||||
[:user.class/A :user.class/C])
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/B))))
|
||||
"B keeps only the nearest parent")
|
||||
(is (= [:user.class/B]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D removes the direct parent inherited through B"))))
|
||||
|
||||
(deftest extends-redundant-direct-parent-cleanup-for-root-reset
|
||||
(testing "Clean descendant Root parents when an ancestor is reset to Root"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:B {}
|
||||
:C {:build/class-extends [:B]}
|
||||
:D {:build/class-extends [:logseq.class/Root :C]}}})]
|
||||
(outliner-property/set-block-property! conn
|
||||
(:db/id (d/entity @conn :user.class/B))
|
||||
:logseq.property.class/extends
|
||||
:logseq.class/Root)
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D removes the direct Root parent inherited through C"))))
|
||||
|
||||
(deftest delete-property-value!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:C1 {}
|
||||
:C2 {}
|
||||
:C3 {:build/class-extends [:C1 :C2]}}})]
|
||||
(outliner-property/delete-property-value! conn :user.class/C3 :logseq.property.class/extends
|
||||
(:db/id (d/entity @conn :user.class/C2)))
|
||||
(is (= [:user.class/C1]
|
||||
(:logseq.property.class/extends (db-test/readable-properties (d/entity @conn :user.class/C3))))
|
||||
"Specific property value is deleted")
|
||||
|
||||
(outliner-property/delete-property-value! conn :user.class/C3 :logseq.property.class/extends
|
||||
(:db/id (d/entity @conn :user.class/C1)))
|
||||
(is (= [:logseq.class/Root]
|
||||
(:logseq.property.class/extends (db-test/readable-properties (d/entity @conn :user.class/C3))))
|
||||
"Extends property is restored back to Root")))
|
||||
214
deps/outliner/test/logseq/outliner/recycle_test.cljs
vendored
Normal file
214
deps/outliner/test/logseq/outliner/recycle_test.cljs
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
(ns logseq.outliner.recycle-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.op :as outliner-op]
|
||||
[logseq.outliner.recycle :as recycle]))
|
||||
|
||||
(deftest restore-recycled-page-removes-recycle-parent
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")]
|
||||
(recycle/recycle-page-tx-data @conn page {})
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(recycle/restore! conn (:block/uuid page))
|
||||
(let [page' (ldb/get-page @conn "page1")]
|
||||
(is (nil? (:block/parent page')))
|
||||
(is (nil? (:logseq.property/deleted-at page')))
|
||||
(is (nil? (:logseq.property.recycle/original-parent page'))))))
|
||||
|
||||
(deftest apply-ops-restore-recycled-page-removes-recycle-parent
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
page-uuid (:block/uuid page)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(outliner-op/apply-ops! conn [[:restore-recycled [page-uuid]]] {})
|
||||
(let [page' (ldb/get-page @conn "page1")]
|
||||
(is (nil? (:block/parent page')))
|
||||
(is (nil? (:logseq.property/deleted-at page')))
|
||||
(is (nil? (:logseq.property.recycle/original-parent page'))))))
|
||||
|
||||
(deftest permanently-delete-recycled-page-removes-page-and-descendants
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
block (db-test/find-block-by-content @conn "b1")
|
||||
page-uuid (:block/uuid page)
|
||||
block-uuid (:block/uuid block)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(is (true? (recycle/permanently-delete! conn page-uuid)))
|
||||
(is (nil? (d/entity @conn [:block/uuid page-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid block-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-page-removes-blocks-parented-by-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}}
|
||||
{:page {:block/title "page2"}}])
|
||||
page1 (ldb/get-page @conn "page1")
|
||||
page2 (ldb/get-page @conn "page2")
|
||||
block-uuid (random-uuid)
|
||||
now (common-util/time-ms)]
|
||||
(d/transact! conn [{:block/uuid block-uuid
|
||||
:block/title "parented by page1"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/parent (:db/id page1)
|
||||
:block/page (:db/id page2)
|
||||
:block/order "a0"}])
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page1 {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn (:db/id page1)))))
|
||||
(is (true? (recycle/permanently-delete! conn (:block/uuid page1))))
|
||||
(is (nil? (d/entity @conn [:block/uuid block-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-converted-page-removes-property-value-blocks
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "outer"}
|
||||
:blocks [{:block/title "target"
|
||||
:build/properties {:default "value"}}]}])
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
target-id (:db/id target)
|
||||
target-uuid (:block/uuid target)
|
||||
value-id (d/q '[:find ?value .
|
||||
:in $ ?target
|
||||
:where
|
||||
[?value :block/parent ?target]
|
||||
[?value :logseq.property/created-from-property]]
|
||||
@conn target-id)]
|
||||
(d/transact! conn [[:db/retract target-id :block/page]
|
||||
[:db/add target-id :block/name "target"]
|
||||
[:db/add target-id :block/tags :logseq.class/Page]])
|
||||
(let [page (d/entity @conn target-id)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page}))
|
||||
(is (true? (recycle/permanently-delete! conn target-uuid)))
|
||||
(is (nil? (d/entity @conn target-id)))
|
||||
(is (nil? (d/entity @conn value-id)))))
|
||||
|
||||
(deftest gc-recycled-converted-page-removes-property-value-blocks
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "outer"}
|
||||
:blocks [{:block/title "target"
|
||||
:build/properties {:default "value"}}
|
||||
{:block/title "unrelated"}]}])
|
||||
outer (ldb/get-page @conn "outer")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
unrelated (db-test/find-block-by-content @conn "unrelated")
|
||||
target-id (:db/id target)
|
||||
value-id (d/q '[:find ?value .
|
||||
:in $ ?target
|
||||
:where
|
||||
[?value :block/parent ?target]
|
||||
[?value :logseq.property/created-from-property]]
|
||||
@conn target-id)]
|
||||
(d/transact! conn [[:db/retract target-id :block/page]
|
||||
[:db/add target-id :block/name "target"]
|
||||
[:db/add target-id :block/tags :logseq.class/Page]])
|
||||
(let [page (d/entity @conn target-id)]
|
||||
(ldb/transact! conn
|
||||
(recycle/recycle-page-tx-data @conn page {:now-ms 0})
|
||||
{:outliner-op :delete-page}))
|
||||
(is (true? (recycle/gc! conn {:now-ms (* 31 24 3600 1000)})))
|
||||
(is (nil? (d/entity @conn target-id)))
|
||||
(is (nil? (d/entity @conn value-id)))
|
||||
(is (some? (d/entity @conn (:db/id outer))))
|
||||
(is (some? (d/entity @conn (:db/id unrelated))))))
|
||||
|
||||
(deftest gc-keeps-unexpired-recycled-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
page-id (:db/id page)]
|
||||
(ldb/transact! conn
|
||||
(recycle/recycle-page-tx-data @conn page {:now-ms 0})
|
||||
{:outliner-op :delete-page})
|
||||
(is (nil? (recycle/gc! conn {:now-ms (* 29 24 3600 1000)})))
|
||||
(is (some? (d/entity @conn page-id)))))
|
||||
|
||||
(deftest apply-ops-permanently-delete-recycled-page-removes-page-and-descendants
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
block (db-test/find-block-by-content @conn "b1")
|
||||
page-uuid (:block/uuid page)
|
||||
block-uuid (:block/uuid block)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(outliner-op/apply-ops! conn [[:recycle-delete-permanently [page-uuid]]] {})
|
||||
(is (nil? (d/entity @conn [:block/uuid page-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid block-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-block-removes-subtree-only
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
parent (db-test/find-block-by-content @conn "parent")
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
parent-uuid (:block/uuid parent)
|
||||
child-uuid (:block/uuid child)]
|
||||
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid parent-uuid]))))
|
||||
(is (true? (recycle/permanently-delete! conn parent-uuid)))
|
||||
(is (some? (d/entity @conn [:block/uuid (:block/uuid page)])))
|
||||
(is (nil? (d/entity @conn [:block/uuid parent-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest apply-ops-permanently-delete-recycled-block-removes-subtree-only
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}])
|
||||
parent (db-test/find-block-by-content @conn "parent")
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
parent-uuid (:block/uuid parent)
|
||||
child-uuid (:block/uuid child)]
|
||||
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid parent-uuid]))))
|
||||
(outliner-op/apply-ops! conn [[:recycle-delete-permanently [parent-uuid]]] {})
|
||||
(is (nil? (d/entity @conn [:block/uuid parent-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-block-removes-corresponding-view-history
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}])
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
target-uuid (:block/uuid target)
|
||||
view-uuid (random-uuid)
|
||||
target-history-uuid (random-uuid)
|
||||
view-history-uuid (random-uuid)
|
||||
now (common-util/time-ms)
|
||||
_ (d/transact! conn [{:block/uuid view-uuid
|
||||
:block/title "target view"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:logseq.property/view-for (:db/id target)
|
||||
:logseq.property.view/type :logseq.property.view/type.table
|
||||
:logseq.property.view/feature-type :linked-references}
|
||||
{:block/uuid target-history-uuid
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:logseq.property.history/block (:db/id target)
|
||||
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
|
||||
:logseq.property.history/scalar-value "Todo"}
|
||||
{:block/uuid view-history-uuid
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:logseq.property.history/block [:block/uuid view-uuid]
|
||||
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
|
||||
:logseq.property.history/scalar-value "List"}])]
|
||||
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [target] {}) {:outliner-op :delete-blocks})
|
||||
(is (true? (recycle/permanently-delete! conn target-uuid)))
|
||||
(is (nil? (d/entity @conn [:block/uuid target-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid view-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid target-history-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid view-history-uuid])))))
|
||||
18
deps/outliner/test/logseq/outliner/tree_test.cljs
vendored
Normal file
18
deps/outliner/test/logseq/outliner/tree_test.cljs
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
(ns logseq.outliner.tree-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[logseq.outliner.tree :as otree]))
|
||||
|
||||
(deftest blocks->vec-tree-data-preserves-caller-field-policy
|
||||
(let [root {:db/id 1}
|
||||
child {:db/id 2
|
||||
:block/uuid (random-uuid)
|
||||
:block/order "a0"
|
||||
:block/parent {:db/id 1}
|
||||
:block/tx-id 9}
|
||||
default-result (otree/blocks->vec-tree-data [child] root {:include-root? false})
|
||||
renderer-result (otree/blocks->vec-tree-data [child] root {:include-root? false
|
||||
:keep-block-tx-id? true})]
|
||||
(is (vector? default-result))
|
||||
(is (= [1] (mapv :block/level default-result)))
|
||||
(is (nil? (:block/tx-id (first default-result))))
|
||||
(is (= 9 (:block/tx-id (first renderer-result))))))
|
||||
308
deps/outliner/test/logseq/outliner/validate_test.cljs
vendored
Normal file
308
deps/outliner/test/logseq/outliner/validate_test.cljs
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
(ns logseq.outliner.validate-test
|
||||
(:require [cljs.test :refer [are deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db.common.entity-plus :as entity-plus]
|
||||
[logseq.db.frontend.entity-util :as entity-util]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.validate :as outliner-validate]))
|
||||
|
||||
(deftest validate-block-title-unique-for-properties
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:color {:logseq.property/type :default}
|
||||
:color2 {:logseq.property/type :default}}})]
|
||||
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
(:block/title (d/entity @conn :logseq.property/background-color))
|
||||
(d/entity @conn :user.property/color)))
|
||||
"Allow user property to have same name as built-in property")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate property"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"color"
|
||||
(d/entity @conn :user.property/color2)))
|
||||
"Disallow duplicate user property")))
|
||||
|
||||
(deftest validate-block-title-unique-for-tags
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Class1 {}
|
||||
:Class2 {:logseq.property.class/extends :logseq.class/Task}}})]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate class"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Class1"
|
||||
(d/entity @conn :user.class/Class2)))
|
||||
"Disallow duplicate class names, regardless of extends")
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate class"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Card"
|
||||
(d/entity @conn :user.class/Class1)))
|
||||
"Disallow duplicate class names even if it's built-in")))
|
||||
|
||||
(deftest validate-block-title-unique-for-namespaced-pages
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "Library"
|
||||
:block/uuid #uuid "d246c71a-3e71-42f0-928f-afe607ee5ce0"
|
||||
:build/keep-uuid? true
|
||||
:build/properties {:logseq.property/built-in? true}}}
|
||||
{:page {:block/title "n1"
|
||||
:block/uuid #uuid "3aa1e950-5a9b-4efc-81d4-b6d89a504591"
|
||||
:build/keep-uuid? true
|
||||
:block/parent [:block/uuid #uuid "d246c71a-3e71-42f0-928f-afe607ee5ce0"]}}
|
||||
{:page {:block/title "n2"
|
||||
:block/parent [:block/uuid #uuid "3aa1e950-5a9b-4efc-81d4-b6d89a504591"]}}
|
||||
{:page {:block/title "n3"
|
||||
:block/parent [:block/uuid #uuid "3aa1e950-5a9b-4efc-81d4-b6d89a504591"]}}]
|
||||
:build-existing-tx? true})]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate page"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"n2"
|
||||
(db-test/find-page-by-title @conn "n3")))
|
||||
"Disallow duplicate namespace child")
|
||||
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"n4"
|
||||
(db-test/find-page-by-title @conn "n3")))
|
||||
"Allow namespace child if unique")))
|
||||
|
||||
(deftest validate-block-title-unique-for-pages
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}}
|
||||
{:page {:block/title "another page"}}
|
||||
{:page {:block/title "Apple" :build/tags [:Company]}}
|
||||
{:page {:block/title "Another Company" :build/tags [:Company]}}
|
||||
{:page {:block/title "Banana" :build/tags [:Fruit]}}])]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate page"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Apple"
|
||||
(db-test/find-page-by-title @conn "Another Company")))
|
||||
"Disallow duplicate page with tag")
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Apple"
|
||||
(db-test/find-page-by-title @conn "Banana")))
|
||||
"Allow page with same name for different tag")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate page"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"page1"
|
||||
(db-test/find-page-by-title @conn "another page")))
|
||||
"Disallow duplicate page without tag")
|
||||
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Apple"
|
||||
(db-test/find-page-by-title @conn "Fruit")))
|
||||
"Allow class to have same name as a page")))
|
||||
|
||||
(deftest validate-extends-property
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:prop1 {:logseq.property/type :default}}
|
||||
:classes {:Class1 {} :Class2 {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}}
|
||||
{:page {:block/title "page2"}}]})
|
||||
page1 (db-test/find-page-by-title @conn "page1")
|
||||
class1 (db-test/find-page-by-title @conn "Class1")
|
||||
class2 (db-test/find-page-by-title @conn "Class2")
|
||||
property (db-test/find-page-by-title @conn "prop1")
|
||||
db @conn]
|
||||
|
||||
(testing "valid parent and child combinations"
|
||||
(is (nil? (outliner-validate/validate-extends-property db class1 [class2]))
|
||||
"parent class to child class is valid"))
|
||||
|
||||
(testing "invalid parent and child combinations"
|
||||
(are [parent child]
|
||||
(thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't extend"
|
||||
(outliner-validate/validate-extends-property db parent [child]))
|
||||
|
||||
class1 page1
|
||||
page1 class1
|
||||
property class1))
|
||||
|
||||
(testing "built-in tag can't have parent changed"
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't change.*built-in"
|
||||
(outliner-validate/validate-extends-property db
|
||||
(entity-plus/entity-memoized @conn :logseq.class/Task)
|
||||
[(entity-plus/entity-memoized @conn :logseq.class/Cards)]))))))
|
||||
|
||||
(deftest validate-tags-property
|
||||
(let [class-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:classes {:SomeTag {:block/uuid class-uuid :build/keep-uuid? true}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "block"
|
||||
:build/children [{:block/title "block - invalid location"}]}
|
||||
{:block/title "block / invalid title"}]}
|
||||
{:page {:block/uuid class-uuid}
|
||||
:blocks [{:block/title "class block"}]}]
|
||||
:build-existing-tx? true})
|
||||
block (db-test/find-block-by-content @conn "block")
|
||||
block-invalid-title (db-test/find-block-by-content @conn #"invalid title")
|
||||
block-invalid-location (db-test/find-block-by-content @conn #"invalid location")]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't add tag.*Tag"
|
||||
(outliner-validate/validate-tags-property @conn [:logseq.class/Tag] :user.class/SomeTag))
|
||||
"built-in tag must not be tagged by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't add tag.*Heading"
|
||||
(outliner-validate/validate-tags-property @conn [:logseq.property/heading] :user.class/SomeTag))
|
||||
"built-in property must not be tagged by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't add tag.*Contents"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id (db-test/find-page-by-title @conn "Contents"))] :user.class/SomeTag))
|
||||
"built-in page must not be tagged by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't set tag.*Tag"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Tag))
|
||||
"Nodes can't be tagged with built-in private tags")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't set tag.*Priority"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.property/priority))
|
||||
"Nodes can't be tagged with built-in non tags")
|
||||
|
||||
(is (nil? (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Page))
|
||||
"Blocks can be tagged with #Page")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Page name can't.*/"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block-invalid-title)] :logseq.class/Page))
|
||||
"Block with invalid title can't be tagged with #Page")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't convert this block to page"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block-invalid-location)] :logseq.class/Page))
|
||||
"Block with invalid location can't be tagged with #Page")))
|
||||
|
||||
(deftest validate-tags-property-deletion
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:SomeTag {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "block" :build/tags [:logseq.class/Page]}]}]})
|
||||
page (db-test/find-page-by-title @conn "page1")
|
||||
page-with-parent (db-test/find-block-by-content @conn "block")]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove tag.*Task"
|
||||
(outliner-validate/validate-tags-property-deletion @conn [(:db/id (d/entity @conn :logseq.class/Task))] :logseq.class/Tag))
|
||||
"built-in class must not have tag deleted by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove tag.*Tag"
|
||||
(outliner-validate/validate-tags-property-deletion @conn [(:db/id (d/entity @conn :user.class/SomeTag))] :logseq.class/Tag))
|
||||
"Node can't have private tag deleted by user")
|
||||
|
||||
(is (nil? (outliner-validate/validate-tags-property-deletion @conn [(:db/id page-with-parent)] :logseq.class/Page))
|
||||
"Page with parent can remove #Page")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"This page cannot be converted"
|
||||
(outliner-validate/validate-tags-property-deletion @conn [(:db/id page)] :logseq.class/Page))
|
||||
"Page without parent can't remove #Page")))
|
||||
|
||||
(deftest validate-editing-built-in-property
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:myprop {:logseq.property/type :default}}})
|
||||
user-prop (d/entity @conn :user.property/myprop)
|
||||
built-in-prop (d/entity @conn :block/tags)]
|
||||
|
||||
(testing "user property can edit any attribute"
|
||||
(is (nil? (outliner-validate/validate-editing-built-in-property
|
||||
user-prop {:db/cardinality :db.cardinality/many}))))
|
||||
|
||||
(testing "built-in property cannot edit disallowed attribute"
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"not editable"
|
||||
(outliner-validate/validate-editing-built-in-property
|
||||
built-in-prop {:block/title "renamed"}))))
|
||||
|
||||
(testing "built-in property can edit allowed attribute"
|
||||
(is (nil? (outliner-validate/validate-editing-built-in-property
|
||||
built-in-prop {:logseq.property/hide-empty-value true}))))))
|
||||
|
||||
;; Try as many of the validations against a new graph to confirm
|
||||
;; that validations make sense and are valid for a new graph
|
||||
(deftest new-graph-should-be-valid
|
||||
(let [conn (db-test/create-conn)]
|
||||
|
||||
(testing "Validate pages"
|
||||
(let [pages (->> (d/q '[:find [?b ...] :where
|
||||
[?b :block/title]
|
||||
[?b :block/tags]] @conn)
|
||||
(map (fn [id]
|
||||
(d/entity @conn id))))
|
||||
page-errors (atom {})]
|
||||
(doseq [page pages]
|
||||
(try
|
||||
(outliner-validate/validate-unique-by-name-and-tags @conn (:block/title page) page)
|
||||
(outliner-validate/validate-page-title (:block/title page) {:node page})
|
||||
(outliner-validate/validate-page-title-characters (:block/title page) {:node page})
|
||||
(when (entity-util/property? page) (outliner-validate/validate-property-title (:block/title page)))
|
||||
(when (entity-util/class? page)
|
||||
(doseq [parent (:logseq.property.class/extends page)]
|
||||
(outliner-validate/validate-extends-property @conn parent [page] {:built-in? false})))
|
||||
|
||||
(catch :default e
|
||||
(if (= :notification (:type (ex-data e)))
|
||||
(swap! page-errors update (select-keys page [:block/title :db/ident :block/uuid]) (fnil conj []) e)
|
||||
(throw e)))))
|
||||
(is (= {} @page-errors)
|
||||
"Default pages shouldn't have any validation errors")))
|
||||
|
||||
(testing "Validate property relationships"
|
||||
(let [parent-child-pairs (d/q '[:find ?parent ?child
|
||||
:where [?child :logseq.property.class/extends ?parent]] @conn)]
|
||||
(doseq [[parent-id child-id] parent-child-pairs]
|
||||
(let [parent (d/entity @conn parent-id)
|
||||
child (d/entity @conn child-id)]
|
||||
(is (nil? (#'outliner-validate/validate-extends-property-have-correct-type parent [child]))
|
||||
(str "Parent and child page is valid: " (pr-str (:block/title parent)) " " (pr-str (:block/title child))))))))))
|
||||
Reference in New Issue
Block a user