mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 01:52:28 +08:00
Merge remote-tracking branch 'origin/develop' into renovate/vitest-monorepo
This commit is contained in:
commit
c61713333d
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@ -12,7 +12,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- .github/actions/build-electron/*
|
- .github/actions/build-electron/*
|
||||||
- .github/workflows/nightly.yml
|
- .github/workflows/nightly.yml
|
||||||
- forge.config.cjs
|
- forge.config.ts
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
@ -35,10 +35,10 @@
|
|||||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.53.0",
|
"@playwright/test": "1.53.1",
|
||||||
"@stylistic/eslint-plugin": "4.4.1",
|
"@stylistic/eslint-plugin": "4.4.1",
|
||||||
"@types/express": "5.0.3",
|
"@types/express": "5.0.3",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.32",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.29.0",
|
"eslint": "9.29.0",
|
||||||
|
4
apps/client/.env
Normal file
4
apps/client/.env
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# The development license key for premium CKEditor features.
|
||||||
|
# Note: This key must only be used for the Trilium Notes project.
|
||||||
|
# Expires on: 2025-09-13
|
||||||
|
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
|
@ -1,6 +0,0 @@
|
|||||||
VITE_CKEDITOR_ENABLE_INSPECTOR=false
|
|
||||||
|
|
||||||
# The development license key for premium CKEditor features.
|
|
||||||
# Note: This key is for development purposes only and should not be used in production.
|
|
||||||
# Expires on: 2025-09-13
|
|
||||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6IjRmMjdkYmYxLTcwOTEtNDYwZi04ZDZmLTc0NzBiZjQwNjg2MCIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwibGljZW5zZVR5cGUiOiJkZXZlbG9wbWVudCIsImZlYXR1cmVzIjpbIkRSVVAiLCJDTVQiLCJETyIsIkZQIiwiU0MiLCJUT0MiLCJUUEwiLCJQT0UiLCJDQyIsIk1GIiwiU0VFIiwiRUNIIiwiRUlTIl0sInZjIjoiMjMxYzMwNTEifQ.9Ct5lIKbioC3dM8EFatDTmimEIVOdItE3Uh_ICHlS_A_8ueqIfkZpsN3L4_EqprvteNki9yqbuZVGpZTaQ51xg
|
|
@ -1,6 +1 @@
|
|||||||
VITE_CKEDITOR_ENABLE_INSPECTOR=false
|
VITE_CKEDITOR_ENABLE_INSPECTOR=false
|
||||||
|
|
||||||
# The development license key for premium CKEditor features.
|
|
||||||
# Note: This key must only be used for the Trilium Notes project.
|
|
||||||
# Expires on: 2025-09-13
|
|
||||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
|
|
@ -27,7 +27,7 @@
|
|||||||
"@triliumnext/highlightjs": "workspace:*",
|
"@triliumnext/highlightjs": "workspace:*",
|
||||||
"@triliumnext/share-theme": "workspace:*",
|
"@triliumnext/share-theme": "workspace:*",
|
||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.6",
|
"bootstrap": "5.3.7",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
@ -48,7 +48,7 @@
|
|||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "15.0.12",
|
"marked": "15.0.12",
|
||||||
"mermaid": "11.6.0",
|
"mermaid": "11.6.0",
|
||||||
"mind-elixir": "4.6.0",
|
"mind-elixir": "4.6.1",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.26.9",
|
"preact": "10.26.9",
|
||||||
@ -75,6 +75,9 @@
|
|||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"^build"
|
"^build"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"circular-deps": {
|
||||||
|
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,424 +0,0 @@
|
|||||||
/*
|
|
||||||
* Remove template code below
|
|
||||||
*/
|
|
||||||
html {
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
||||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
|
||||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
|
||||||
'Noto Color Emoji';
|
|
||||||
line-height: 1.5;
|
|
||||||
tab-size: 4;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
p,
|
|
||||||
pre {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
*,
|
|
||||||
::before,
|
|
||||||
::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-width: 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: currentColor;
|
|
||||||
}
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
font-size: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
||||||
'Liberation Mono', 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
display: block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
shape-rendering: auto;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
background-color: rgba(55, 65, 81, 1);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
color: rgba(229, 231, 235, 1);
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
||||||
'Liberation Mono', 'Courier New', monospace;
|
|
||||||
overflow: scroll;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow {
|
|
||||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
||||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
.rounded {
|
|
||||||
border-radius: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
max-width: 768px;
|
|
||||||
padding-bottom: 3rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
color: rgba(55, 65, 81, 1);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#welcome {
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
}
|
|
||||||
#welcome h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
#welcome span {
|
|
||||||
display: block;
|
|
||||||
font-size: 1.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 2.25rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
#hero {
|
|
||||||
align-items: center;
|
|
||||||
background-color: hsla(214, 62%, 21%, 1);
|
|
||||||
border: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: rgba(55, 65, 81, 1);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
#hero .text-container {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
padding: 3rem 2rem;
|
|
||||||
}
|
|
||||||
#hero .text-container h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
#hero .text-container h2 svg {
|
|
||||||
color: hsla(162, 47%, 50%, 1);
|
|
||||||
height: 2rem;
|
|
||||||
left: -0.25rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
#hero .text-container h2 span {
|
|
||||||
margin-left: 2.5rem;
|
|
||||||
}
|
|
||||||
#hero .text-container a {
|
|
||||||
background-color: rgba(255, 255, 255, 1);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: rgba(55, 65, 81, 1);
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
#hero .logo-container {
|
|
||||||
display: none;
|
|
||||||
justify-content: center;
|
|
||||||
padding-left: 2rem;
|
|
||||||
padding-right: 2rem;
|
|
||||||
}
|
|
||||||
#hero .logo-container svg {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
width: 66.666667%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#middle-content {
|
|
||||||
align-items: flex-start;
|
|
||||||
display: grid;
|
|
||||||
gap: 4rem;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#learning-materials {
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
}
|
|
||||||
#learning-materials h2 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
.list-item-link {
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.list-item-link svg:first-child {
|
|
||||||
margin-right: 1rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
width: 1.5rem;
|
|
||||||
}
|
|
||||||
.list-item-link > span {
|
|
||||||
flex-grow: 1;
|
|
||||||
font-weight: 400;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
.list-item-link > span > span {
|
|
||||||
color: rgba(107, 114, 128, 1);
|
|
||||||
display: block;
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1rem;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
.list-item-link svg:last-child {
|
|
||||||
height: 1rem;
|
|
||||||
transition-property: all;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
width: 1rem;
|
|
||||||
}
|
|
||||||
.list-item-link:hover {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
background-color: hsla(162, 47%, 50%, 1);
|
|
||||||
}
|
|
||||||
.list-item-link:hover > span {
|
|
||||||
}
|
|
||||||
.list-item-link:hover > span > span {
|
|
||||||
color: rgba(243, 244, 246, 1);
|
|
||||||
}
|
|
||||||
.list-item-link:hover svg:last-child {
|
|
||||||
transform: translateX(0.25rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
#other-links {
|
|
||||||
}
|
|
||||||
.button-pill {
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
transition-duration: 300ms;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.button-pill svg {
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 3rem;
|
|
||||||
}
|
|
||||||
.button-pill > span {
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
.button-pill span span {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
.button-pill:hover svg,
|
|
||||||
.button-pill:hover {
|
|
||||||
color: rgba(255, 255, 255, 1) !important;
|
|
||||||
}
|
|
||||||
#nx-console:hover {
|
|
||||||
background-color: rgba(0, 122, 204, 1);
|
|
||||||
}
|
|
||||||
#nx-console svg {
|
|
||||||
color: rgba(0, 122, 204, 1);
|
|
||||||
}
|
|
||||||
#nx-console-jetbrains {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
#nx-console-jetbrains:hover {
|
|
||||||
background-color: rgba(255, 49, 140, 1);
|
|
||||||
}
|
|
||||||
#nx-console-jetbrains svg {
|
|
||||||
color: rgba(255, 49, 140, 1);
|
|
||||||
}
|
|
||||||
#nx-repo:hover {
|
|
||||||
background-color: rgba(24, 23, 23, 1);
|
|
||||||
}
|
|
||||||
#nx-repo svg {
|
|
||||||
color: rgba(24, 23, 23, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nx-cloud {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
}
|
|
||||||
#nx-cloud > div {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
#nx-cloud > div svg {
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 3rem;
|
|
||||||
}
|
|
||||||
#nx-cloud > div h2 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
#nx-cloud > div h2 span {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
#nx-cloud p {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
#nx-cloud pre {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
#nx-cloud a {
|
|
||||||
color: rgba(107, 114, 128, 1);
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#nx-cloud a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#commands {
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
#commands h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
#commands p {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
details {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
details pre > span {
|
|
||||||
color: rgba(181, 181, 181, 1);
|
|
||||||
}
|
|
||||||
summary {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
summary:hover {
|
|
||||||
background-color: rgba(243, 244, 246, 1);
|
|
||||||
}
|
|
||||||
summary svg {
|
|
||||||
height: 1.5rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#love {
|
|
||||||
color: rgba(107, 114, 128, 1);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
opacity: 0.6;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#love svg {
|
|
||||||
color: rgba(252, 165, 165, 1);
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
display: inline;
|
|
||||||
margin-top: -0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
#hero {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
#hero .logo-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
#middle-content {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { AppElement } from './app.element';
|
|
||||||
|
|
||||||
describe('AppElement', () => {
|
|
||||||
let app: AppElement;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
app = new AppElement();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create successfully', () => {
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have a greeting', () => {
|
|
||||||
app.connectedCallback();
|
|
||||||
|
|
||||||
expect(app.querySelector('h1').innerHTML).toContain(
|
|
||||||
'Welcome @triliumnext/client'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,409 +0,0 @@
|
|||||||
import './app.element.css';
|
|
||||||
|
|
||||||
export class AppElement extends HTMLElement {
|
|
||||||
public static observedAttributes = [
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
const title = '@triliumnext/client';
|
|
||||||
this.innerHTML = `
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="container">
|
|
||||||
<!-- WELCOME -->
|
|
||||||
<div id="welcome">
|
|
||||||
<h1>
|
|
||||||
<span> Hello there, </span>
|
|
||||||
Welcome ${title} 👋
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<div id="hero" class="rounded">
|
|
||||||
<div class="text-container">
|
|
||||||
<h2>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>You're up and running</span>
|
|
||||||
</h2>
|
|
||||||
<a href="#commands"> What's next? </a>
|
|
||||||
</div>
|
|
||||||
<div class="logo-container">
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MIDDLE CONTENT -->
|
|
||||||
<div id="middle-content">
|
|
||||||
<div id="learning-materials" class="rounded shadow">
|
|
||||||
<h2>Learning materials</h2>
|
|
||||||
<a href="https://nx.dev/getting-started/intro?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Documentation
|
|
||||||
<span> Everything is in there </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://nx.dev/blog/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Blog
|
|
||||||
<span> Changelog, features & events </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>YouTube</title>
|
|
||||||
<path
|
|
||||||
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
YouTube channel
|
|
||||||
<span> Nx Show, talks & tutorials </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Interactive tutorials
|
|
||||||
<span> Create an app, step-by-step </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://nxplaybook.com/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M12 14l9-5-9-5-9 5 9 5z" />
|
|
||||||
<path
|
|
||||||
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Video courses
|
|
||||||
<span> Nx custom courses </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div id="other-links">
|
|
||||||
<a id="nx-console" class="button-pill rounded shadow" href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project" target="_blank" rel="noreferrer">
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Visual Studio Code</title>
|
|
||||||
<path
|
|
||||||
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Install Nx Console for VSCode
|
|
||||||
<span>The official VSCode extension for Nx.</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
id="nx-console-jetbrains"
|
|
||||||
class="button-pill rounded shadow"
|
|
||||||
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
height="48"
|
|
||||||
width="48"
|
|
||||||
viewBox="20 20 60 60"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="m22.5 22.5h60v60h-60z" />
|
|
||||||
<g fill="#fff">
|
|
||||||
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
|
|
||||||
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
|
|
||||||
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
|
|
||||||
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
|
|
||||||
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
|
|
||||||
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
|
|
||||||
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
|
|
||||||
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
|
|
||||||
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
|
|
||||||
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Install Nx Console for JetBrains
|
|
||||||
<span>
|
|
||||||
Available for WebStorm, Intellij IDEA Ultimate and more!
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<div id="nx-cloud" class="rounded shadow">
|
|
||||||
<div>
|
|
||||||
<svg id="nx-cloud-logo" role="img" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="transparent" viewBox="0 0 24 24">
|
|
||||||
<path stroke-width="2" d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z" />
|
|
||||||
<path stroke-width="2" d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z" />
|
|
||||||
</svg>
|
|
||||||
<h2>
|
|
||||||
Nx Cloud
|
|
||||||
<span>
|
|
||||||
Enable faster CI & better DX
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
You can activate distributed tasks executions and caching by
|
|
||||||
running:
|
|
||||||
</p>
|
|
||||||
<pre>nx connect</pre>
|
|
||||||
<a href="https://nx.app/?utm_source=nx-project" target="_blank" rel="noreferrer"> What is Nx Cloud? </a>
|
|
||||||
</div>
|
|
||||||
<a id="nx-repo" class="button-pill rounded shadow" href="https://github.com/nrwl/nx?utm_source=nx-project" target="_blank" rel="noreferrer">
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Nx is open source
|
|
||||||
<span> Love Nx? Give us a star! </span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- COMMANDS -->
|
|
||||||
<div id="commands" class="rounded shadow">
|
|
||||||
<h2>Next steps</h2>
|
|
||||||
<p>Here are some things you can do with Nx:</p>
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Add UI library
|
|
||||||
</summary>
|
|
||||||
<pre><span># Generate UI lib</span>
|
|
||||||
nx g @nx/angular:lib ui
|
|
||||||
|
|
||||||
<span># Add a component</span>
|
|
||||||
nx g @nx/angular:component ui/src/lib/button</pre>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
View interactive project graph
|
|
||||||
</summary>
|
|
||||||
<pre>nx graph</pre>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Run affected commands
|
|
||||||
</summary>
|
|
||||||
<pre><span># see what's been affected by changes</span>
|
|
||||||
nx affected:graph
|
|
||||||
|
|
||||||
<span># run tests for current changes</span>
|
|
||||||
nx affected:test
|
|
||||||
|
|
||||||
<span># run e2e tests for current changes</span>
|
|
||||||
nx affected:e2e</pre>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p id="love">
|
|
||||||
Carefully crafted with
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('triliumnext-root', AppElement);
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Client</title>
|
|
||||||
<base href="/" />
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<triliumnext-root></triliumnext-root>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1 +0,0 @@
|
|||||||
import './app/app.element';
|
|
@ -1 +0,0 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
|
@ -1,5 +1,4 @@
|
|||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import bundleService from "../services/bundle.js";
|
|
||||||
import RootCommandExecutor from "./root_command_executor.js";
|
import RootCommandExecutor from "./root_command_executor.js";
|
||||||
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
|
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
@ -470,6 +469,7 @@ export class AppContext extends Component {
|
|||||||
|
|
||||||
this.tabManager.loadTabs();
|
this.tabManager.loadTabs();
|
||||||
|
|
||||||
|
const bundleService = (await import("../services/bundle.js")).default;
|
||||||
setTimeout(() => bundleService.executeStartupBundles(), 2000);
|
setTimeout(() => bundleService.executeStartupBundles(), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import type FNote from "../entities/fnote.js";
|
|||||||
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
|
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
|
import { closeActiveDialog } from "../services/dialog.js";
|
||||||
|
|
||||||
export interface SetNoteOpts {
|
export interface SetNoteOpts {
|
||||||
triggerSwitchEvent?: unknown;
|
triggerSwitchEvent?: unknown;
|
||||||
@ -83,7 +84,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
|
|
||||||
await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
|
await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
|
||||||
|
|
||||||
utils.closeActiveDialog();
|
closeActiveDialog();
|
||||||
|
|
||||||
this.notePath = resolvedNotePath;
|
this.notePath = resolvedNotePath;
|
||||||
this.viewScope = opts.viewScope;
|
this.viewScope = opts.viewScope;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import ws from "../services/ws.js";
|
import ws from "../services/ws.js";
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import cssClassManager from "../services/css_class_manager.js";
|
import cssClassManager from "../services/css_class_manager.js";
|
||||||
import type { Froca } from "../services/froca-interface.js";
|
import type { Froca } from "../services/froca-interface.js";
|
||||||
@ -410,8 +409,8 @@ class FNote {
|
|||||||
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
||||||
notePath: path,
|
notePath: path,
|
||||||
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
|
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
|
||||||
isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
|
isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived),
|
||||||
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
|
isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"),
|
||||||
isHidden: path.includes("_hidden")
|
isHidden: path.includes("_hidden")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -982,7 +981,7 @@ class FNote {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNote = froca.notes[parentNoteId];
|
const parentNote = this.froca.notes[parentNoteId];
|
||||||
|
|
||||||
if (!parentNote || parentNote.type === "search") {
|
if (!parentNote || parentNote.type === "search") {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import ScriptContext from "./script_context.js";
|
import ScriptContext from "./script_context.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import toastService from "./toast.js";
|
import toastService, { showError } from "./toast.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
@ -37,7 +37,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const note = await froca.getNote(bundle.noteId);
|
const note = await froca.getNote(bundle.noteId);
|
||||||
|
|
||||||
toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`);
|
const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
|
||||||
|
showError(message);
|
||||||
|
logError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import froca from "./froca.js";
|
|||||||
import linkService from "./link.js";
|
import linkService from "./link.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import toast from "./toast.js";
|
import { throwError } from "./ws.js";
|
||||||
|
|
||||||
let clipboardBranchIds: string[] = [];
|
let clipboardBranchIds: string[] = [];
|
||||||
let clipboardMode: string | null = null;
|
let clipboardMode: string | null = null;
|
||||||
@ -37,7 +37,7 @@ async function pasteAfter(afterBranchId: string) {
|
|||||||
|
|
||||||
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
||||||
} else {
|
} else {
|
||||||
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ async function pasteInto(parentBranchId: string) {
|
|||||||
|
|
||||||
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
||||||
} else {
|
} else {
|
||||||
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,41 @@
|
|||||||
|
import { Modal } from "bootstrap";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||||
|
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||||
|
|
||||||
|
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
||||||
|
if (closeActDialog) {
|
||||||
|
closeActiveDialog();
|
||||||
|
glob.activeDialog = $dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFocusedElement();
|
||||||
|
Modal.getOrCreateInstance($dialog[0]).show();
|
||||||
|
|
||||||
|
$dialog.on("hidden.bs.modal", () => {
|
||||||
|
const $autocompleteEl = $(".aa-input");
|
||||||
|
if ("autocomplete" in $autocompleteEl) {
|
||||||
|
$autocompleteEl.autocomplete("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!glob.activeDialog || glob.activeDialog === $dialog) {
|
||||||
|
focusSavedElement();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
|
||||||
|
keyboardActionsService.updateDisplayedShortcuts($dialog);
|
||||||
|
|
||||||
|
return $dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeActiveDialog() {
|
||||||
|
if (glob.activeDialog) {
|
||||||
|
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
||||||
|
glob.activeDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function info(message: string) {
|
async function info(message: string) {
|
||||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
||||||
|
29
apps/client/src/services/focus.ts
Normal file
29
apps/client/src/services/focus.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
let $lastFocusedElement: JQuery<HTMLElement> | null;
|
||||||
|
|
||||||
|
// perhaps there should be saved focused element per tab?
|
||||||
|
export function saveFocusedElement() {
|
||||||
|
$lastFocusedElement = $(":focus");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusSavedElement() {
|
||||||
|
if (!$lastFocusedElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lastFocusedElement.hasClass("ck")) {
|
||||||
|
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
|
||||||
|
// the bug manifests itself in resetting the cursor position to the first character - jumping above
|
||||||
|
|
||||||
|
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.editing.view.focus();
|
||||||
|
} else {
|
||||||
|
console.log("Could not find CKEditor instance to focus last element");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$lastFocusedElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastFocusedElement = null;
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { LOCALES } from "@triliumnext/commons";
|
import { LOCALES } from "@triliumnext/commons";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("i18n", () => {
|
describe("i18n", () => {
|
||||||
it("translations are valid JSON", () => {
|
it("translations are valid JSON", () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import toastService from "./toast.js";
|
import toastService, { showError } from "./toast.js";
|
||||||
|
|
||||||
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
||||||
try {
|
try {
|
||||||
@ -11,7 +11,9 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
|||||||
if (success) {
|
if (success) {
|
||||||
toastService.showMessage(t("image.copied-to-clipboard"));
|
toastService.showMessage(t("image.copied-to-clipboard"));
|
||||||
} else {
|
} else {
|
||||||
toastService.showAndLogError(t("image.cannot-copy"));
|
const message = t("image.cannot-copy");
|
||||||
|
showError(message);
|
||||||
|
logError(message);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
window.getSelection()?.removeAllRanges();
|
window.getSelection()?.removeAllRanges();
|
||||||
|
@ -289,13 +289,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.action === "create-note") {
|
if (suggestion.action === "create-note") {
|
||||||
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType();
|
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
|
||||||
const { note } = await noteCreateService.createNote(suggestion.parentNoteId, {
|
|
||||||
title: suggestion.noteTitle,
|
title: suggestion.noteTitle,
|
||||||
activate: false,
|
activate: false,
|
||||||
type: noteType,
|
type: noteType,
|
||||||
|
@ -116,7 +116,7 @@ async function chooseNoteType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
|
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
|
||||||
const { success, noteType, templateNoteId } = await chooseNoteType();
|
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return;
|
return;
|
||||||
@ -125,7 +125,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN
|
|||||||
options.type = noteType;
|
options.type = noteType;
|
||||||
options.templateNoteId = templateNoteId;
|
options.templateNoteId = templateNoteId;
|
||||||
|
|
||||||
return await createNote(parentNotePath, options);
|
return await createNote(notePath || parentNotePath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If the first element is heading, parse it out and use it as a new heading. */
|
/* If the first element is heading, parse it out and use it as a new heading. */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import FrontendScriptApi, { type Entity } from "./frontend_script_api.js";
|
import type { Entity } from "./frontend_script_api.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn
|
|||||||
throw new Error(`Could not find start note ${startNoteId}.`);
|
throw new Error(`Could not find start note ${startNoteId}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FrontendScriptApi = (await import("./frontend_script_api.js")).default;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modules: modules,
|
modules: modules,
|
||||||
notes: utils.toObject(allNotes, (note) => [note.noteId, note]),
|
notes: utils.toObject(allNotes, (note) => [note.noteId, note]),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import utils, { isShare } from "./utils.js";
|
import utils, { isShare } from "./utils.js";
|
||||||
import ValidationError from "./validation_error.js";
|
import ValidationError from "./validation_error.js";
|
||||||
|
import { throwError } from "./ws.js";
|
||||||
|
|
||||||
type Headers = Record<string, string | null | undefined>;
|
type Headers = Record<string, string | null | undefined>;
|
||||||
|
|
||||||
@ -276,7 +277,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
|||||||
} else {
|
} else {
|
||||||
const title = `${statusCode} ${method} ${url}`;
|
const title = `${statusCode} ${method} ${url}`;
|
||||||
toastService.showErrorTitleAndMessage(title, messageStr);
|
toastService.showErrorTitleAndMessage(title, messageStr);
|
||||||
toastService.throwError(`${title} - ${message}`);
|
throwError(`${title} - ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,13 +78,7 @@ function showMessage(message: string, delay = 2000) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAndLogError(message: string, delay = 10000) {
|
export function showError(message: string, delay = 10000) {
|
||||||
showError(message, delay);
|
|
||||||
|
|
||||||
ws.logError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message: string, delay = 10000) {
|
|
||||||
console.log(utils.now(), "error: ", message);
|
console.log(utils.now(), "error: ", message);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -108,18 +102,10 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function throwError(message: string) {
|
|
||||||
ws.logError(message);
|
|
||||||
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
showMessage,
|
showMessage,
|
||||||
showError,
|
showError,
|
||||||
showErrorTitleAndMessage,
|
showErrorTitleAndMessage,
|
||||||
showAndLogError,
|
|
||||||
throwError,
|
|
||||||
showPersistent,
|
showPersistent,
|
||||||
closePersistent
|
closePersistent
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Modal } from "bootstrap";
|
|
||||||
import type { ViewScope } from "./link.js";
|
import type { ViewScope } from "./link.js";
|
||||||
|
|
||||||
const SVG_MIME = "image/svg+xml";
|
const SVG_MIME = "image/svg+xml";
|
||||||
@ -275,69 +274,6 @@ function getMimeTypeClass(mime: string) {
|
|||||||
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeActiveDialog() {
|
|
||||||
if (glob.activeDialog) {
|
|
||||||
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
|
||||||
glob.activeDialog = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let $lastFocusedElement: JQuery<HTMLElement> | null;
|
|
||||||
|
|
||||||
// perhaps there should be saved focused element per tab?
|
|
||||||
function saveFocusedElement() {
|
|
||||||
$lastFocusedElement = $(":focus");
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusSavedElement() {
|
|
||||||
if (!$lastFocusedElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lastFocusedElement.hasClass("ck")) {
|
|
||||||
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
|
|
||||||
// the bug manifests itself in resetting the cursor position to the first character - jumping above
|
|
||||||
|
|
||||||
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
|
|
||||||
|
|
||||||
if (editor) {
|
|
||||||
editor.editing.view.focus();
|
|
||||||
} else {
|
|
||||||
console.log("Could not find CKEditor instance to focus last element");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$lastFocusedElement.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastFocusedElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
|
||||||
if (closeActDialog) {
|
|
||||||
closeActiveDialog();
|
|
||||||
glob.activeDialog = $dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveFocusedElement();
|
|
||||||
Modal.getOrCreateInstance($dialog[0]).show();
|
|
||||||
|
|
||||||
$dialog.on("hidden.bs.modal", () => {
|
|
||||||
const $autocompleteEl = $(".aa-input");
|
|
||||||
if ("autocomplete" in $autocompleteEl) {
|
|
||||||
$autocompleteEl.autocomplete("close");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!glob.activeDialog || glob.activeDialog === $dialog) {
|
|
||||||
focusSavedElement();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
|
|
||||||
keyboardActionsService.updateDisplayedShortcuts($dialog);
|
|
||||||
|
|
||||||
return $dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHtmlEmpty(html: string) {
|
function isHtmlEmpty(html: string) {
|
||||||
if (!html) {
|
if (!html) {
|
||||||
return true;
|
return true;
|
||||||
@ -823,10 +759,6 @@ export default {
|
|||||||
setCookie,
|
setCookie,
|
||||||
getNoteTypeClass,
|
getNoteTypeClass,
|
||||||
getMimeTypeClass,
|
getMimeTypeClass,
|
||||||
closeActiveDialog,
|
|
||||||
openDialog,
|
|
||||||
saveFocusedElement,
|
|
||||||
focusSavedElement,
|
|
||||||
isHtmlEmpty,
|
isHtmlEmpty,
|
||||||
clearBrowserCache,
|
clearBrowserCache,
|
||||||
copySelectionToClipboard,
|
copySelectionToClipboard,
|
||||||
|
@ -17,7 +17,7 @@ let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
|
|||||||
let lastPingTs: number;
|
let lastPingTs: number;
|
||||||
let frontendUpdateDataQueue: EntityChange[] = [];
|
let frontendUpdateDataQueue: EntityChange[] = [];
|
||||||
|
|
||||||
function logError(message: string) {
|
export function logError(message: string) {
|
||||||
console.error(utils.now(), message); // needs to be separate from .trace()
|
console.error(utils.now(), message); // needs to be separate from .trace()
|
||||||
|
|
||||||
if (ws && ws.readyState === 1) {
|
if (ws && ws.readyState === 1) {
|
||||||
@ -301,6 +301,12 @@ setTimeout(() => {
|
|||||||
setInterval(sendPing, 1000);
|
setInterval(sendPing, 1000);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
export function throwError(message: string) {
|
||||||
|
logError(message);
|
||||||
|
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
logError,
|
logError,
|
||||||
subscribeToMessages,
|
subscribeToMessages,
|
||||||
|
@ -440,10 +440,11 @@ body #context-menu-container .dropdown-item > span {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
|
font-size: var(--monospace-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
body .cm-editor {
|
.cm-scroller {
|
||||||
font-size: var(--monospace-font-size);
|
font-family: var(--monospace-font-family) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body .cm-editor .cm-gutters {
|
body .cm-editor .cm-gutters {
|
||||||
|
@ -233,6 +233,8 @@
|
|||||||
"move_success_message": "Selected notes have been moved into "
|
"move_success_message": "Selected notes have been moved into "
|
||||||
},
|
},
|
||||||
"note_type_chooser": {
|
"note_type_chooser": {
|
||||||
|
"change_path_prompt": "Change where to create the new note:",
|
||||||
|
"search_placeholder": "search path by name (default if empty)",
|
||||||
"modal_title": "Choose note type",
|
"modal_title": "Choose note type",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"modal_body": "Choose note type / template of the new note:",
|
"modal_body": "Choose note type / template of the new note:",
|
||||||
|
@ -11,6 +11,7 @@ import utils from "../../services/utils.js";
|
|||||||
import shortcutService from "../../services/shortcuts.js";
|
import shortcutService from "../../services/shortcuts.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext from "../../components/app_context.js";
|
||||||
import type { Attribute } from "../../services/attribute_parser.js";
|
import type { Attribute } from "../../services/attribute_parser.js";
|
||||||
|
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="attr-detail tn-tool-dialog">
|
<div class="attr-detail tn-tool-dialog">
|
||||||
@ -483,7 +484,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.saveFocusedElement();
|
saveFocusedElement();
|
||||||
|
|
||||||
this.attrType = this.getAttrType(attribute);
|
this.attrType = this.getAttrType(attribute);
|
||||||
|
|
||||||
@ -605,7 +606,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.hide();
|
this.hide();
|
||||||
|
|
||||||
utils.focusSavedElement();
|
focusSavedElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAndClose() {
|
async cancelAndClose() {
|
||||||
@ -613,7 +614,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.hide();
|
this.hide();
|
||||||
|
|
||||||
utils.focusSavedElement();
|
focusSavedElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
userEditedAttribute() {
|
userEditedAttribute() {
|
||||||
|
@ -4,6 +4,7 @@ import BasicWidget from "../basic_widget.js";
|
|||||||
import openService from "../../services/open.js";
|
import openService from "../../services/open.js";
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
interface AppInfo {
|
interface AppInfo {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
@ -111,6 +112,6 @@ export default class AboutDialog extends BasicWidget {
|
|||||||
|
|
||||||
async openAboutDialogEvent() {
|
async openAboutDialogEvent() {
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import treeService from "../../services/tree.js";
|
import treeService from "../../services/tree.js";
|
||||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import type { Suggestion } from "../../services/note_autocomplete.js";
|
import type { Suggestion } from "../../services/note_autocomplete.js";
|
||||||
import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog">
|
<div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -111,7 +111,7 @@ export default class AddLinkDialog extends BasicWidget {
|
|||||||
|
|
||||||
this.updateTitleSettingsVisibility();
|
this.updateTitleSettingsVisibility();
|
||||||
|
|
||||||
await utils.openDialog(this.$widget);
|
await openDialog(this.$widget);
|
||||||
|
|
||||||
this.$autoComplete.val("");
|
this.$autoComplete.val("");
|
||||||
this.$linkTitle.val("");
|
this.$linkTitle.val("");
|
||||||
|
@ -2,11 +2,11 @@ import treeService from "../../services/tree.js";
|
|||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext from "../../components/app_context.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
const TPL = /*html*/`<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
@ -93,7 +93,7 @@ export default class BranchPrefixDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.refresh(notePath);
|
await this.refresh(notePath);
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
async savePrefix() {
|
async savePrefix() {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import bulkActionService from "../../services/bulk_action.js";
|
import bulkActionService from "../../services/bulk_action.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
@ -104,7 +104,7 @@ export default class BulkActionsDialog extends BasicWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
|
toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
|
||||||
utils.closeActiveDialog();
|
closeActiveDialog();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +170,6 @@ export default class BulkActionsDialog extends BasicWidget {
|
|||||||
this.$includeDescendants.prop("checked", false);
|
this.$includeDescendants.prop("checked", false);
|
||||||
|
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import treeService from "../../services/tree.js";
|
import treeService from "../../services/tree.js";
|
||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
@ -8,6 +7,7 @@ import appContext from "../../components/app_context.js";
|
|||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
@ -94,7 +94,7 @@ export default class CloneToDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
this.$noteAutoComplete.val("").trigger("focus");
|
this.$noteAutoComplete.val("").trigger("focus");
|
||||||
this.$noteList.empty();
|
this.$noteList.empty();
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import linkService from "../../services/link.js";
|
import linkService from "../../services/link.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import type { FAttributeRow } from "../../entities/fattribute.js";
|
import type { FAttributeRow } from "../../entities/fattribute.js";
|
||||||
|
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
// TODO: Use common with server.
|
// TODO: Use common with server.
|
||||||
interface Response {
|
interface Response {
|
||||||
@ -119,13 +119,13 @@ export default class DeleteNotesDialog extends BasicWidget {
|
|||||||
this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus"));
|
this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus"));
|
||||||
|
|
||||||
this.$cancelButton.on("click", () => {
|
this.$cancelButton.on("click", () => {
|
||||||
utils.closeActiveDialog();
|
closeActiveDialog();
|
||||||
|
|
||||||
this.resolve({ proceed: false });
|
this.resolve({ proceed: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$okButton.on("click", () => {
|
this.$okButton.on("click", () => {
|
||||||
utils.closeActiveDialog();
|
closeActiveDialog();
|
||||||
|
|
||||||
this.resolve({
|
this.resolve({
|
||||||
proceed: true,
|
proceed: true,
|
||||||
@ -179,7 +179,7 @@ export default class DeleteNotesDialog extends BasicWidget {
|
|||||||
|
|
||||||
await this.renderDeletePreview();
|
await this.renderDeletePreview();
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
this.$deleteAllClones.prop("checked", !!forceDeleteAllClones).prop("disabled", !!forceDeleteAllClones);
|
this.$deleteAllClones.prop("checked", !!forceDeleteAllClones).prop("disabled", !!forceDeleteAllClones);
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import BasicWidget from "../basic_widget.js";
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
<div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -214,7 +215,7 @@ export default class ExportDialog extends BasicWidget {
|
|||||||
|
|
||||||
this.$widget.find(".opml-v2").prop("checked", true); // setting default
|
this.$widget.find(".opml-v2").prop("checked", true); // setting default
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="help-dialog modal use-tn-links" tabindex="-1" role="dialog">
|
<div class="help-dialog modal use-tn-links" tabindex="-1" role="dialog">
|
||||||
@ -155,6 +155,6 @@ export default class HelpDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showCheatsheetEvent() {
|
showCheatsheetEvent() {
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import utils, { escapeQuotes } from "../../services/utils.js";
|
import { escapeQuotes } from "../../services/utils.js";
|
||||||
import treeService from "../../services/tree.js";
|
import treeService from "../../services/tree.js";
|
||||||
import importService, { type UploadFilesOptions } from "../../services/import.js";
|
import importService, { type UploadFilesOptions } from "../../services/import.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
@ -6,6 +6,7 @@ import BasicWidget from "../basic_widget.js";
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import { Modal, Tooltip } from "bootstrap";
|
import { Modal, Tooltip } from "bootstrap";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -155,7 +156,7 @@ export default class ImportDialog extends BasicWidget {
|
|||||||
|
|
||||||
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
|
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
async importIntoNote(parentNoteId: string) {
|
async importIntoNote(parentNoteId: string) {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import treeService from "../../services/tree.js";
|
import treeService from "../../services/tree.js";
|
||||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
|
import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -83,7 +83,7 @@ export default class IncludeNoteDialog extends BasicWidget {
|
|||||||
async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
|
async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
|
||||||
this.textTypeWidget = textTypeWidget;
|
this.textTypeWidget = textTypeWidget;
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
|
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
import type { ConfirmDialogCallback } from "./confirm.js";
|
import type { ConfirmDialogCallback } from "./confirm.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
|
<div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
|
||||||
@ -72,7 +72,7 @@ export default class InfoDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
this.resolve = callback;
|
this.resolve = callback;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import appContext from "../../components/app_context.js";
|
|||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import shortcutService from "../../services/shortcuts.js";
|
import shortcutService from "../../services/shortcuts.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
@ -54,7 +55,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async jumpToNoteEvent() {
|
async jumpToNoteEvent() {
|
||||||
const dialogPromise = utils.openDialog(this.$widget);
|
const dialogPromise = openDialog(this.$widget);
|
||||||
if (utils.isMobile()) {
|
if (utils.isMobile()) {
|
||||||
dialogPromise.then(($dialog) => {
|
dialogPromise.then(($dialog) => {
|
||||||
const el = $dialog.find(">.modal-dialog")[0];
|
const el = $dialog.find(">.modal-dialog")[0];
|
||||||
|
@ -6,6 +6,7 @@ import BasicWidget from "../basic_widget.js";
|
|||||||
import shortcutService from "../../services/shortcuts.js";
|
import shortcutService from "../../services/shortcuts.js";
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="markdown-import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
<div class="markdown-import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -89,7 +90,7 @@ export default class MarkdownImportDialog extends BasicWidget {
|
|||||||
|
|
||||||
this.convertMarkdownToHtml(text);
|
this.convertMarkdownToHtml(text);
|
||||||
} else {
|
} else {
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import branchService from "../../services/branches.js";
|
import branchService from "../../services/branches.js";
|
||||||
@ -7,6 +6,7 @@ import treeService from "../../services/tree.js";
|
|||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
|
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -83,7 +83,7 @@ export default class MoveToDialog extends BasicWidget {
|
|||||||
async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
|
async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
|
||||||
this.movedBranchIds = branchIds;
|
this.movedBranchIds = branchIds;
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
this.$noteAutoComplete.val("").trigger("focus");
|
this.$noteAutoComplete.val("").trigger("focus");
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import type { CommandNames } from "../../components/app_context.js";
|
|||||||
import type { MenuCommandItem } from "../../menus/context_menu.js";
|
import type { MenuCommandItem } from "../../menus/context_menu.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import noteTypesService from "../../services/note_types.js";
|
import noteTypesService from "../../services/note_types.js";
|
||||||
|
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Dropdown, Modal } from "bootstrap";
|
import { Dropdown, Modal } from "bootstrap";
|
||||||
|
|
||||||
@ -13,6 +14,11 @@ const TPL = /*html*/`
|
|||||||
z-index: 1100 !important;
|
z-index: 1100 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-type-chooser-dialog .input-group {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.note-type-chooser-dialog .note-type-dropdown {
|
.note-type-chooser-dialog .note-type-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: large;
|
font-size: large;
|
||||||
@ -30,6 +36,12 @@ const TPL = /*html*/`
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("note_type_chooser.close")}"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("note_type_chooser.close")}"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
${t("note_type_chooser.change_path_prompt")}
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="choose-note-path form-control" placeholder="${t("note_type_chooser.search_placeholder")}">
|
||||||
|
</div>
|
||||||
|
|
||||||
${t("note_type_chooser.modal_body")}
|
${t("note_type_chooser.modal_body")}
|
||||||
|
|
||||||
<div class="dropdown" style="display: flex;">
|
<div class="dropdown" style="display: flex;">
|
||||||
@ -48,6 +60,7 @@ export interface ChooseNoteTypeResponse {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
noteType?: string;
|
noteType?: string;
|
||||||
templateNoteId?: string;
|
templateNoteId?: string;
|
||||||
|
notePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Callback = (data: ChooseNoteTypeResponse) => void;
|
type Callback = (data: ChooseNoteTypeResponse) => void;
|
||||||
@ -57,6 +70,7 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
|||||||
private dropdown!: Dropdown;
|
private dropdown!: Dropdown;
|
||||||
private modal!: Modal;
|
private modal!: Modal;
|
||||||
private $noteTypeDropdown!: JQuery<HTMLElement>;
|
private $noteTypeDropdown!: JQuery<HTMLElement>;
|
||||||
|
private $autoComplete!: JQuery<HTMLElement>;
|
||||||
private $originalFocused: JQuery<HTMLElement> | null;
|
private $originalFocused: JQuery<HTMLElement> | null;
|
||||||
private $originalDialog: JQuery<HTMLElement> | null;
|
private $originalDialog: JQuery<HTMLElement> | null;
|
||||||
|
|
||||||
@ -71,7 +85,8 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
|||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||||
|
|
||||||
|
this.$autoComplete = this.$widget.find(".choose-note-path");
|
||||||
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
|
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
|
||||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger")[0]);
|
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger")[0]);
|
||||||
|
|
||||||
@ -116,9 +131,20 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
noteAutocompleteService
|
||||||
|
.initNoteAutocomplete(this.$autoComplete, {
|
||||||
|
allowCreatingNotes: false,
|
||||||
|
hideGoToSelectedNoteButton: true,
|
||||||
|
allowJumpToSearchNotes: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
|
async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
|
||||||
this.$originalFocused = $(":focus");
|
this.$originalFocused = $(":focus");
|
||||||
|
|
||||||
|
await this.refresh();
|
||||||
|
|
||||||
const noteTypes = await noteTypesService.getNoteTypeItems();
|
const noteTypes = await noteTypesService.getNoteTypeItems();
|
||||||
|
|
||||||
this.$noteTypeDropdown.empty();
|
this.$noteTypeDropdown.empty();
|
||||||
@ -153,12 +179,14 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
|||||||
const $item = $(e.target).closest(".dropdown-item");
|
const $item = $(e.target).closest(".dropdown-item");
|
||||||
const noteType = $item.attr("data-note-type");
|
const noteType = $item.attr("data-note-type");
|
||||||
const templateNoteId = $item.attr("data-template-note-id");
|
const templateNoteId = $item.attr("data-template-note-id");
|
||||||
|
const notePath = this.$autoComplete.getSelectedNotePath() || undefined;
|
||||||
|
|
||||||
if (this.resolve) {
|
if (this.resolve) {
|
||||||
this.resolve({
|
this.resolve({
|
||||||
success: true,
|
success: true,
|
||||||
noteType,
|
noteType,
|
||||||
templateNoteId
|
templateNoteId,
|
||||||
|
notePath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.resolve = null;
|
this.resolve = null;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
@ -37,6 +37,6 @@ export default class PasswordNoteSetDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showPasswordNotSetEvent() {
|
showPasswordNotSetEvent() {
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
@ -110,6 +110,6 @@ export default class PromptDialog extends BasicWidget {
|
|||||||
|
|
||||||
this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer));
|
this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer));
|
||||||
|
|
||||||
utils.openDialog(this.$widget, false);
|
openDialog(this.$widget, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import protectedSessionService from "../../services/protected_session.js";
|
import protectedSessionService from "../../services/protected_session.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export default class ProtectedSessionPasswordDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showProtectedSessionPasswordDialogEvent() {
|
showProtectedSessionPasswordDialogEvent() {
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
this.$passwordInput.trigger("focus");
|
this.$passwordInput.trigger("focus");
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,12 @@ import { formatDateTime } from "../../utils/formatters.js";
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import appContext, { type EventData } from "../../components/app_context.js";
|
import appContext, { type EventData } from "../../components/app_context.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import dialogService from "../../services/dialog.js";
|
import dialogService, { openDialog } from "../../services/dialog.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import hoistedNoteService from "../../services/hoisted_note.js";
|
import hoistedNoteService from "../../services/hoisted_note.js";
|
||||||
import linkService from "../../services/link.js";
|
import linkService from "../../services/link.js";
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import ws from "../../services/ws.js";
|
import ws from "../../services/ws.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
@ -62,7 +61,7 @@ export default class RecentChangesDialog extends BasicWidget {
|
|||||||
|
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
|
@ -6,7 +6,7 @@ import appContext from "../../components/app_context.js";
|
|||||||
import openService from "../../services/open.js";
|
import openService from "../../services/open.js";
|
||||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import dialogService from "../../services/dialog.js";
|
import dialogService, { openDialog } from "../../services/dialog.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type { NoteType } from "../../entities/fnote.js";
|
import type { NoteType } from "../../entities/fnote.js";
|
||||||
@ -182,7 +182,7 @@ export default class RevisionsDialog extends BasicWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
await this.loadRevisions(noteId);
|
await this.loadRevisions(noteId);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
|
|
||||||
const TPL = /*html*/`<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
|
const TPL = /*html*/`<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -97,14 +97,14 @@ export default class SortChildNotesDialog extends BasicWidget {
|
|||||||
|
|
||||||
await server.put(`notes/${this.parentNoteId}/sort-children`, { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale });
|
await server.put(`notes/${this.parentNoteId}/sort-children`, { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale });
|
||||||
|
|
||||||
utils.closeActiveDialog();
|
closeActiveDialog();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) {
|
async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) {
|
||||||
this.parentNoteId = node.data.noteId;
|
this.parentNoteId = node.data.noteId;
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
|
|
||||||
this.$form.find("input:first").focus();
|
this.$form.find("input:first").focus();
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import utils, { escapeQuotes } from "../../services/utils.js";
|
import { escapeQuotes } from "../../services/utils.js";
|
||||||
import treeService from "../../services/tree.js";
|
import treeService from "../../services/tree.js";
|
||||||
import importService from "../../services/import.js";
|
import importService from "../../services/import.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Modal, Tooltip } from "bootstrap";
|
import { Modal, Tooltip } from "bootstrap";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { openDialog } from "../../services/dialog.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
<div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||||
@ -98,7 +99,7 @@ export default class UploadAttachmentsDialog extends BasicWidget {
|
|||||||
|
|
||||||
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
|
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadAttachments(parentNoteId: string) {
|
async uploadAttachments(parentNoteId: string) {
|
||||||
|
@ -52,9 +52,9 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
|
|||||||
toastService.showMessage(t("code_buttons.opening_api_docs_message"));
|
toastService.showMessage(t("code_buttons.opening_api_docs_message"));
|
||||||
|
|
||||||
if (this.note?.mime.endsWith("frontend")) {
|
if (this.note?.mime.endsWith("frontend")) {
|
||||||
window.open("https://zadam.github.io/trilium/frontend_api/FrontendScriptApi.html", "_blank");
|
window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html", "_blank");
|
||||||
} else {
|
} else {
|
||||||
window.open("https://zadam.github.io/trilium/backend_api/BackendScriptApi.html", "_blank");
|
window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html", "_blank");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,6 +56,8 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
|
|||||||
return byNoteType[note.type];
|
return byNoteType[note.type];
|
||||||
} else if (note?.hasLabel("calendarRoot")) {
|
} else if (note?.hasLabel("calendarRoot")) {
|
||||||
return "l0tKav7yLHGF";
|
return "l0tKav7yLHGF";
|
||||||
|
} else if (note?.hasLabel("textSnippet")) {
|
||||||
|
return "pwc194wlRzcH";
|
||||||
} else if (note && note.type === "book") {
|
} else if (note && note.type === "book") {
|
||||||
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
|
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,64 @@ import { buildExtraCommands, type EditorConfig } from "@triliumnext/ckeditor5";
|
|||||||
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
||||||
import options from "../../../services/options.js";
|
import options from "../../../services/options.js";
|
||||||
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||||
import utils from "../../../services/utils.js";
|
|
||||||
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
|
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
|
||||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||||
import getTemplates from "./snippets.js";
|
import getTemplates from "./snippets.js";
|
||||||
|
import { PREMIUM_PLUGINS } from "../../../../../../packages/ckeditor5/src/plugins.js";
|
||||||
|
import { t } from "../../../services/i18n.js";
|
||||||
|
import { getMermaidConfig } from "../../../services/mermaid.js";
|
||||||
|
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
|
||||||
|
import mimeTypesService from "../../../services/mime_types.js";
|
||||||
|
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||||
|
import { buildToolbarConfig } from "./toolbar.js";
|
||||||
|
|
||||||
const TEXT_FORMATTING_GROUP = {
|
export const OPEN_SOURCE_LICENSE_KEY = "GPL";
|
||||||
label: "Text formatting",
|
|
||||||
icon: "text"
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function buildConfig(): Promise<EditorConfig> {
|
export interface BuildEditorOptions {
|
||||||
return {
|
forceGplLicense: boolean;
|
||||||
|
isClassicEditor: boolean;
|
||||||
|
contentLanguage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfig> {
|
||||||
|
const licenseKey = (opts.forceGplLicense ? OPEN_SOURCE_LICENSE_KEY : getLicenseKey());
|
||||||
|
const hasPremiumLicense = (licenseKey !== OPEN_SOURCE_LICENSE_KEY);
|
||||||
|
|
||||||
|
const config: EditorConfig = {
|
||||||
|
licenseKey,
|
||||||
|
placeholder: t("editable_text.placeholder"),
|
||||||
|
mention: {
|
||||||
|
feeds: [
|
||||||
|
{
|
||||||
|
marker: "@",
|
||||||
|
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||||
|
itemRenderer: (item) => {
|
||||||
|
const itemElement = document.createElement("button");
|
||||||
|
|
||||||
|
itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `;
|
||||||
|
|
||||||
|
return itemElement;
|
||||||
|
},
|
||||||
|
minimumCharacters: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
codeBlock: {
|
||||||
|
languages: buildListOfLanguages()
|
||||||
|
},
|
||||||
|
math: {
|
||||||
|
engine: "katex",
|
||||||
|
outputType: "span", // or script
|
||||||
|
lazyLoad: async () => {
|
||||||
|
(window as any).katex = (await import("../../../services/math.js")).default
|
||||||
|
},
|
||||||
|
forceOutputType: false, // forces output to use outputType
|
||||||
|
enablePreview: true // Enable preview view
|
||||||
|
},
|
||||||
|
mermaid: {
|
||||||
|
lazyLoad: async () => (await import("mermaid")).default, // FIXME
|
||||||
|
config: getMermaidConfig()
|
||||||
|
},
|
||||||
image: {
|
image: {
|
||||||
styles: {
|
styles: {
|
||||||
options: [
|
options: [
|
||||||
@ -130,149 +176,59 @@ export async function buildConfig(): Promise<EditorConfig> {
|
|||||||
template: {
|
template: {
|
||||||
definitions: await getTemplates()
|
definitions: await getTemplates()
|
||||||
},
|
},
|
||||||
|
htmlSupport: {
|
||||||
|
allow: JSON.parse(options.get("allowedHtmlTags"))
|
||||||
|
},
|
||||||
// This value must be kept in sync with the language defined in webpack.config.js.
|
// This value must be kept in sync with the language defined in webpack.config.js.
|
||||||
language: "en"
|
language: "en"
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function buildToolbarConfig(isClassicToolbar: boolean) {
|
// Set up content language.
|
||||||
if (utils.isMobile()) {
|
const { contentLanguage } = opts;
|
||||||
return buildMobileToolbar();
|
if (contentLanguage) {
|
||||||
} else if (isClassicToolbar) {
|
config.language = {
|
||||||
const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true";
|
ui: (typeof config.language === "string" ? config.language : "en"),
|
||||||
return buildClassicToolbar(multilineToolbar);
|
content: contentLanguage
|
||||||
} else {
|
|
||||||
return buildFloatingToolbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildMobileToolbar() {
|
|
||||||
const classicConfig = buildClassicToolbar(false);
|
|
||||||
const items: string[] = [];
|
|
||||||
|
|
||||||
for (const item of classicConfig.toolbar.items) {
|
|
||||||
if (typeof item === "object" && "items" in item) {
|
|
||||||
for (const subitem of item.items) {
|
|
||||||
items.push(subitem);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable premium plugins.
|
||||||
|
if (hasPremiumLicense) {
|
||||||
|
config.extraPlugins = [
|
||||||
|
...PREMIUM_PLUGINS
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...classicConfig,
|
...config,
|
||||||
toolbar: {
|
...buildToolbarConfig(opts.isClassicEditor)
|
||||||
...classicConfig.toolbar,
|
|
||||||
items
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildClassicToolbar(multilineToolbar: boolean) {
|
function buildListOfLanguages() {
|
||||||
// For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars.
|
const userLanguages = mimeTypesService
|
||||||
return {
|
.getMimeTypes()
|
||||||
toolbar: {
|
.filter((mt) => mt.enabled)
|
||||||
items: [
|
.map((mt) => ({
|
||||||
"heading",
|
language: normalizeMimeTypeForCKEditor(mt.mime),
|
||||||
"fontSize",
|
label: mt.title
|
||||||
"|",
|
}));
|
||||||
"bold",
|
|
||||||
"italic",
|
|
||||||
{
|
|
||||||
...TEXT_FORMATTING_GROUP,
|
|
||||||
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
|
|
||||||
},
|
|
||||||
"|",
|
|
||||||
"fontColor",
|
|
||||||
"fontBackgroundColor",
|
|
||||||
"removeFormat",
|
|
||||||
"|",
|
|
||||||
"bulletedList",
|
|
||||||
"numberedList",
|
|
||||||
"todoList",
|
|
||||||
"|",
|
|
||||||
"blockQuote",
|
|
||||||
"admonition",
|
|
||||||
"insertTable",
|
|
||||||
"|",
|
|
||||||
"code",
|
|
||||||
"codeBlock",
|
|
||||||
"|",
|
|
||||||
"footnote",
|
|
||||||
{
|
|
||||||
label: "Insert",
|
|
||||||
icon: "plus",
|
|
||||||
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
|
||||||
},
|
|
||||||
"|",
|
|
||||||
"alignment",
|
|
||||||
"outdent",
|
|
||||||
"indent",
|
|
||||||
"|",
|
|
||||||
"insertTemplate",
|
|
||||||
"markdownImport",
|
|
||||||
"cuttonote",
|
|
||||||
"findAndReplace"
|
|
||||||
],
|
|
||||||
shouldNotGroupWhenFull: multilineToolbar
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildFloatingToolbar() {
|
return [
|
||||||
return {
|
{
|
||||||
toolbar: {
|
language: mimeTypesService.MIME_TYPE_AUTO,
|
||||||
items: [
|
label: t("editable-text.auto-detect-language")
|
||||||
"fontSize",
|
|
||||||
"bold",
|
|
||||||
"italic",
|
|
||||||
"underline",
|
|
||||||
{
|
|
||||||
...TEXT_FORMATTING_GROUP,
|
|
||||||
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
|
|
||||||
},
|
|
||||||
"|",
|
|
||||||
"fontColor",
|
|
||||||
"fontBackgroundColor",
|
|
||||||
"|",
|
|
||||||
"code",
|
|
||||||
"link",
|
|
||||||
"bookmark",
|
|
||||||
"removeFormat",
|
|
||||||
"internallink",
|
|
||||||
"cuttonote"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
...userLanguages
|
||||||
blockToolbar: [
|
];
|
||||||
"heading",
|
}
|
||||||
"|",
|
|
||||||
"bulletedList",
|
function getLicenseKey() {
|
||||||
"numberedList",
|
const premiumLicenseKey = import.meta.env.VITE_CKEDITOR_KEY;
|
||||||
"todoList",
|
if (!premiumLicenseKey) {
|
||||||
"|",
|
logError("CKEditor license key is not set, premium features will not be available.");
|
||||||
"blockQuote",
|
return OPEN_SOURCE_LICENSE_KEY;
|
||||||
"admonition",
|
}
|
||||||
"codeBlock",
|
|
||||||
"insertTable",
|
return premiumLicenseKey;
|
||||||
"footnote",
|
|
||||||
{
|
|
||||||
label: "Insert",
|
|
||||||
icon: "plus",
|
|
||||||
items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
|
||||||
},
|
|
||||||
"|",
|
|
||||||
"alignment",
|
|
||||||
"outdent",
|
|
||||||
"indent",
|
|
||||||
"|",
|
|
||||||
"insertTemplate",
|
|
||||||
"imageUpload",
|
|
||||||
"markdownImport",
|
|
||||||
"specialCharacters",
|
|
||||||
"emoji",
|
|
||||||
"findAndReplace"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { buildClassicToolbar, buildFloatingToolbar } from "./config.js";
|
import { buildClassicToolbar, buildFloatingToolbar } from "./toolbar.js";
|
||||||
|
|
||||||
type ToolbarConfig = string | "|" | { items: ToolbarConfig[] };
|
type ToolbarConfig = string | "|" | { items: ToolbarConfig[] };
|
||||||
|
|
149
apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts
Normal file
149
apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import utils from "../../../services/utils.js";
|
||||||
|
import options from "../../../services/options.js";
|
||||||
|
|
||||||
|
const TEXT_FORMATTING_GROUP = {
|
||||||
|
label: "Text formatting",
|
||||||
|
icon: "text"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildToolbarConfig(isClassicToolbar: boolean) {
|
||||||
|
if (utils.isMobile()) {
|
||||||
|
return buildMobileToolbar();
|
||||||
|
} else if (isClassicToolbar) {
|
||||||
|
const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true";
|
||||||
|
return buildClassicToolbar(multilineToolbar);
|
||||||
|
} else {
|
||||||
|
return buildFloatingToolbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMobileToolbar() {
|
||||||
|
const classicConfig = buildClassicToolbar(false);
|
||||||
|
const items: string[] = [];
|
||||||
|
|
||||||
|
for (const item of classicConfig.toolbar.items) {
|
||||||
|
if (typeof item === "object" && "items" in item) {
|
||||||
|
for (const subitem of item.items) {
|
||||||
|
items.push(subitem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...classicConfig,
|
||||||
|
toolbar: {
|
||||||
|
...classicConfig.toolbar,
|
||||||
|
items
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildClassicToolbar(multilineToolbar: boolean) {
|
||||||
|
// For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars.
|
||||||
|
return {
|
||||||
|
toolbar: {
|
||||||
|
items: [
|
||||||
|
"heading",
|
||||||
|
"fontSize",
|
||||||
|
"|",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
{
|
||||||
|
...TEXT_FORMATTING_GROUP,
|
||||||
|
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
|
||||||
|
},
|
||||||
|
"|",
|
||||||
|
"fontColor",
|
||||||
|
"fontBackgroundColor",
|
||||||
|
"removeFormat",
|
||||||
|
"|",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"todoList",
|
||||||
|
"|",
|
||||||
|
"blockQuote",
|
||||||
|
"admonition",
|
||||||
|
"insertTable",
|
||||||
|
"|",
|
||||||
|
"code",
|
||||||
|
"codeBlock",
|
||||||
|
"|",
|
||||||
|
"footnote",
|
||||||
|
{
|
||||||
|
label: "Insert",
|
||||||
|
icon: "plus",
|
||||||
|
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||||
|
},
|
||||||
|
"|",
|
||||||
|
"alignment",
|
||||||
|
"outdent",
|
||||||
|
"indent",
|
||||||
|
"|",
|
||||||
|
"insertTemplate",
|
||||||
|
"markdownImport",
|
||||||
|
"cuttonote",
|
||||||
|
"findAndReplace"
|
||||||
|
],
|
||||||
|
shouldNotGroupWhenFull: multilineToolbar
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFloatingToolbar() {
|
||||||
|
return {
|
||||||
|
toolbar: {
|
||||||
|
items: [
|
||||||
|
"fontSize",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"underline",
|
||||||
|
{
|
||||||
|
...TEXT_FORMATTING_GROUP,
|
||||||
|
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
|
||||||
|
},
|
||||||
|
"|",
|
||||||
|
"fontColor",
|
||||||
|
"fontBackgroundColor",
|
||||||
|
"|",
|
||||||
|
"code",
|
||||||
|
"link",
|
||||||
|
"bookmark",
|
||||||
|
"removeFormat",
|
||||||
|
"internallink",
|
||||||
|
"cuttonote"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
blockToolbar: [
|
||||||
|
"heading",
|
||||||
|
"|",
|
||||||
|
"bulletedList",
|
||||||
|
"numberedList",
|
||||||
|
"todoList",
|
||||||
|
"|",
|
||||||
|
"blockQuote",
|
||||||
|
"admonition",
|
||||||
|
"codeBlock",
|
||||||
|
"insertTable",
|
||||||
|
"footnote",
|
||||||
|
{
|
||||||
|
label: "Insert",
|
||||||
|
icon: "plus",
|
||||||
|
items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||||
|
},
|
||||||
|
"|",
|
||||||
|
"alignment",
|
||||||
|
"outdent",
|
||||||
|
"indent",
|
||||||
|
"|",
|
||||||
|
"insertTemplate",
|
||||||
|
"imageUpload",
|
||||||
|
"markdownImport",
|
||||||
|
"specialCharacters",
|
||||||
|
"emoji",
|
||||||
|
"findAndReplace"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,3 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
|
|
||||||
import mimeTypesService from "../../services/mime_types.js";
|
|
||||||
import utils, { hasTouchBar } from "../../services/utils.js";
|
import utils, { hasTouchBar } from "../../services/utils.js";
|
||||||
import keyboardActionService from "../../services/keyboard_actions.js";
|
import keyboardActionService from "../../services/keyboard_actions.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
@ -12,29 +9,12 @@ import dialogService from "../../services/dialog.js";
|
|||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import toast from "../../services/toast.js";
|
import toast from "../../services/toast.js";
|
||||||
import { buildSelectedBackgroundColor } from "../../components/touch_bar.js";
|
import { buildSelectedBackgroundColor } from "../../components/touch_bar.js";
|
||||||
import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js";
|
import { buildConfig, BuildEditorOptions, OPEN_SOURCE_LICENSE_KEY } from "./ckeditor/config.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import { getMermaidConfig } from "../../services/mermaid.js";
|
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5";
|
||||||
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5";
|
|
||||||
import "@triliumnext/ckeditor5/index.css";
|
import "@triliumnext/ckeditor5/index.css";
|
||||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
|
||||||
import { updateTemplateCache } from "./ckeditor/snippets.js";
|
import { updateTemplateCache } from "./ckeditor/snippets.js";
|
||||||
|
|
||||||
const mentionSetup: MentionFeed[] = [
|
|
||||||
{
|
|
||||||
marker: "@",
|
|
||||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
|
||||||
itemRenderer: (item) => {
|
|
||||||
const itemElement = document.createElement("button");
|
|
||||||
|
|
||||||
itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `;
|
|
||||||
|
|
||||||
return itemElement;
|
|
||||||
},
|
|
||||||
minimumCharacters: 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="note-detail-editable-text note-detail-printable">
|
<div class="note-detail-editable-text note-detail-printable">
|
||||||
<style>
|
<style>
|
||||||
@ -97,24 +77,6 @@ const TPL = /*html*/`
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function buildListOfLanguages() {
|
|
||||||
const userLanguages = mimeTypesService
|
|
||||||
.getMimeTypes()
|
|
||||||
.filter((mt) => mt.enabled)
|
|
||||||
.map((mt) => ({
|
|
||||||
language: normalizeMimeTypeForCKEditor(mt.mime),
|
|
||||||
label: mt.title
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
language: mimeTypesService.MIME_TYPE_AUTO,
|
|
||||||
label: t("editable-text.auto-detect-language")
|
|
||||||
},
|
|
||||||
...userLanguages
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The editor can operate into two distinct modes:
|
* The editor can operate into two distinct modes:
|
||||||
*
|
*
|
||||||
@ -147,7 +109,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
|
|
||||||
async initEditor() {
|
async initEditor() {
|
||||||
const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic";
|
const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic";
|
||||||
const editorClass = isClassicEditor ? ClassicEditor : PopupEditor;
|
|
||||||
|
|
||||||
// CKEditor since version 12 needs the element to be visible before initialization. At the same time,
|
// CKEditor since version 12 needs the element to be visible before initialization. At the same time,
|
||||||
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
|
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
|
||||||
@ -192,34 +153,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
this.watchdog.setCreator(async (_, editorConfig) => {
|
this.watchdog.setCreator(async (_, editorConfig) => {
|
||||||
logInfo("Creating new CKEditor");
|
logInfo("Creating new CKEditor");
|
||||||
|
|
||||||
const finalConfig = {
|
|
||||||
...editorConfig,
|
|
||||||
...(await buildConfig()),
|
|
||||||
...buildToolbarConfig(isClassicEditor),
|
|
||||||
htmlSupport: {
|
|
||||||
allow: JSON.parse(options.get("allowedHtmlTags")),
|
|
||||||
styles: true,
|
|
||||||
classes: true,
|
|
||||||
attributes: true
|
|
||||||
},
|
|
||||||
licenseKey: getLicenseKey()
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentLanguage = this.note?.getLabelValue("language");
|
const contentLanguage = this.note?.getLabelValue("language");
|
||||||
if (contentLanguage) {
|
this.contentLanguage = contentLanguage ?? null;
|
||||||
// TODO: Wrong type?
|
|
||||||
//@ts-ignore
|
|
||||||
finalConfig.language = {
|
|
||||||
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
|
|
||||||
content: contentLanguage
|
|
||||||
}
|
|
||||||
this.contentLanguage = contentLanguage;
|
|
||||||
} else {
|
|
||||||
this.contentLanguage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//@ts-ignore
|
const opts: BuildEditorOptions = {
|
||||||
const editor = await editorClass.create(this.$editor[0], finalConfig);
|
contentLanguage: this.contentLanguage,
|
||||||
|
forceGplLicense: false,
|
||||||
|
isClassicEditor
|
||||||
|
};
|
||||||
|
const editor = await buildEditor(this.$editor[0], isClassicEditor, opts);
|
||||||
|
|
||||||
const notificationsPlugin = editor.plugins.get("Notification");
|
const notificationsPlugin = editor.plugins.get("Notification");
|
||||||
notificationsPlugin.on("show:warning", (evt, data) => {
|
notificationsPlugin.on("show:warning", (evt, data) => {
|
||||||
@ -296,28 +238,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createEditor() {
|
async createEditor() {
|
||||||
await this.watchdog.create(this.$editor[0], {
|
await this.watchdog.create(this.$editor[0]);
|
||||||
placeholder: t("editable_text.placeholder"),
|
|
||||||
mention: {
|
|
||||||
feeds: mentionSetup,
|
|
||||||
},
|
|
||||||
codeBlock: {
|
|
||||||
languages: buildListOfLanguages()
|
|
||||||
},
|
|
||||||
math: {
|
|
||||||
engine: "katex",
|
|
||||||
outputType: "span", // or script
|
|
||||||
lazyLoad: async () => {
|
|
||||||
(window as any).katex = (await import("../../services/math.js")).default
|
|
||||||
},
|
|
||||||
forceOutputType: false, // forces output to use outputType
|
|
||||||
enablePreview: true // Enable preview view
|
|
||||||
},
|
|
||||||
mermaid: {
|
|
||||||
lazyLoad: async () => (await import("mermaid")).default, // FIXME
|
|
||||||
config: getMermaidConfig()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh(note: FNote) {
|
async doRefresh(note: FNote) {
|
||||||
@ -656,12 +577,18 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLicenseKey() {
|
async function buildEditor(element: HTMLElement, isClassicEditor: boolean, opts: BuildEditorOptions) {
|
||||||
const premiumLicenseKey = import.meta.env.VITE_CKEDITOR_KEY;
|
const editorClass = isClassicEditor ? ClassicEditor : PopupEditor;
|
||||||
if (!premiumLicenseKey) {
|
let config = await buildConfig(opts);
|
||||||
logError("CKEditor license key is not set, premium features will not be available.");
|
let editor = await editorClass.create(element, config);
|
||||||
return "GPL";
|
|
||||||
}
|
if (editor.isReadOnly) {
|
||||||
|
editor.destroy();
|
||||||
|
|
||||||
|
opts.forceGplLicense = true;
|
||||||
|
config = await buildConfig(opts);
|
||||||
|
editor = await editorClass.create(element, config);
|
||||||
|
}
|
||||||
|
return editor;
|
||||||
|
|
||||||
return premiumLicenseKey;
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/vitest",
|
"outDir": "./out-tsc/vitest",
|
||||||
"types": [
|
"types": [
|
||||||
"vitest/globals",
|
|
||||||
"vitest/importMeta",
|
"vitest/importMeta",
|
||||||
"vite/client",
|
"vite/client",
|
||||||
"node",
|
"node",
|
||||||
|
@ -97,6 +97,12 @@ export default defineConfig(() => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
test: {
|
||||||
|
environment: "happy-dom",
|
||||||
|
setupFiles: [
|
||||||
|
"./src/test/setup.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: [
|
exclude: [
|
||||||
"@triliumnext/highlightjs"
|
"@triliumnext/highlightjs"
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
const path = require("path");
|
import path from "path";
|
||||||
const fs = require("fs-extra");
|
import fs from "fs-extra";
|
||||||
const { LOCALES } = require("@triliumnext/commons");
|
import { LOCALES } from "@triliumnext/commons";
|
||||||
|
import { PRODUCT_NAME } from "../src/app-info.js";
|
||||||
|
|
||||||
const ELECTRON_FORGE_DIR = __dirname;
|
const ELECTRON_FORGE_DIR = __dirname;
|
||||||
|
|
||||||
const EXECUTABLE_NAME = "trilium"; // keep in sync with server's package.json -> packagerConfig.executableName
|
const EXECUTABLE_NAME = "trilium"; // keep in sync with server's package.json -> packagerConfig.executableName
|
||||||
const PRODUCT_NAME = "TriliumNext Notes";
|
|
||||||
const APP_ICON_PATH = path.join(ELECTRON_FORGE_DIR, "app-icon");
|
const APP_ICON_PATH = path.join(ELECTRON_FORGE_DIR, "app-icon");
|
||||||
|
|
||||||
const extraResourcesForPlatform = getExtraResourcesForPlatform();
|
const extraResourcesForPlatform = getExtraResourcesForPlatform();
|
||||||
@ -147,13 +147,13 @@ module.exports = {
|
|||||||
const isMac = (process.platform === "darwin");
|
const isMac = (process.platform === "darwin");
|
||||||
let localesToKeep = LOCALES
|
let localesToKeep = LOCALES
|
||||||
.filter(locale => !locale.contentOnly)
|
.filter(locale => !locale.contentOnly)
|
||||||
.map(locale => locale.electronLocale);
|
.map(locale => locale.electronLocale) as string[];
|
||||||
if (!isMac) {
|
if (!isMac) {
|
||||||
localesToKeep = localesToKeep.map(locale => locale.replace("_", "-"))
|
localesToKeep = localesToKeep.map(locale => locale.replace("_", "-"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const keptLocales = new Set();
|
const keptLocales = new Set();
|
||||||
const removedLocales = [];
|
const removedLocales: string[] = [];
|
||||||
const extension = (isMac ? ".lproj" : ".pak");
|
const extension = (isMac ? ".lproj" : ".pak");
|
||||||
|
|
||||||
for (const outputPath of packageResult.outputPaths) {
|
for (const outputPath of packageResult.outputPaths) {
|
||||||
@ -169,39 +169,39 @@ module.exports = {
|
|||||||
console.log(`No locales directory found in '${localeDir}'.`);
|
console.log(`No locales directory found in '${localeDir}'.`);
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(localeDir);
|
const files = fs.readdirSync(localeDir);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(extension)) {
|
if (!file.endsWith(extension)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let localeName = path.basename(file, extension);
|
let localeName = path.basename(file, extension);
|
||||||
if (localeName === "en-US" && !isMac) {
|
if (localeName === "en-US" && !isMac) {
|
||||||
// If the locale is "en-US" on Windows, we treat it as "en".
|
// If the locale is "en-US" on Windows, we treat it as "en".
|
||||||
// This is because the Windows version of Electron uses "en-US.pak" instead of "en.pak".
|
// This is because the Windows version of Electron uses "en-US.pak" instead of "en.pak".
|
||||||
localeName = "en";
|
localeName = "en";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localesToKeep.includes(localeName)) {
|
if (localesToKeep.includes(localeName)) {
|
||||||
keptLocales.add(localeName);
|
keptLocales.add(localeName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(localeDir, file);
|
const filePath = path.join(localeDir, file);
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
fs.rm(filePath, { recursive: true });
|
fs.rm(filePath, { recursive: true });
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
removedLocales.push(file);
|
removedLocales.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Removed unused locale files: ${removedLocales.join(", ")}`);
|
console.log(`Removed unused locale files: ${removedLocales.join(", ")}`);
|
||||||
|
|
||||||
// Ensure all locales that should be kept are actually present.
|
// Ensure all locales that should be kept are actually present.
|
||||||
for (const locale of localesToKeep) {
|
for (const locale of localesToKeep) {
|
||||||
@ -229,7 +229,7 @@ module.exports = {
|
|||||||
if (TRILIUM_ARTIFACT_NAME_HINT) {
|
if (TRILIUM_ARTIFACT_NAME_HINT) {
|
||||||
fileName = TRILIUM_ARTIFACT_NAME_HINT.replaceAll("/", "-") + extension;
|
fileName = TRILIUM_ARTIFACT_NAME_HINT.replaceAll("/", "-") + extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputPath = path.join(outputDir, fileName);
|
const outputPath = path.join(outputDir, fileName);
|
||||||
console.log(`[Artifact] ${artifactPath} -> ${outputPath}`);
|
console.log(`[Artifact] ${artifactPath} -> ${outputPath}`);
|
||||||
fs.copyFileSync(artifactPath, outputPath);
|
fs.copyFileSync(artifactPath, outputPath);
|
||||||
@ -240,7 +240,7 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getExtraResourcesForPlatform() {
|
function getExtraResourcesForPlatform() {
|
||||||
const resources = [];
|
const resources: string[] = [];
|
||||||
|
|
||||||
const getScriptResources = () => {
|
const getScriptResources = () => {
|
||||||
const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];
|
const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];
|
@ -17,7 +17,7 @@
|
|||||||
"@types/electron-squirrel-startup": "1.0.2",
|
"@types/electron-squirrel-startup": "1.0.2",
|
||||||
"@triliumnext/server": "workspace:*",
|
"@triliumnext/server": "workspace:*",
|
||||||
"copy-webpack-plugin": "13.0.0",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"electron": "36.4.0",
|
"electron": "36.5.0",
|
||||||
"@electron-forge/cli": "7.8.1",
|
"@electron-forge/cli": "7.8.1",
|
||||||
"@electron-forge/maker-deb": "7.8.1",
|
"@electron-forge/maker-deb": "7.8.1",
|
||||||
"@electron-forge/maker-dmg": "7.8.1",
|
"@electron-forge/maker-dmg": "7.8.1",
|
||||||
@ -29,7 +29,7 @@
|
|||||||
"prebuild-install": "^7.1.1"
|
"prebuild-install": "^7.1.1"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"forge": "./electron-forge/forge.config.cjs"
|
"forge": "./electron-forge/forge.config.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js"
|
"start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js"
|
||||||
|
4
apps/desktop/src/app-info.ts
Normal file
4
apps/desktop/src/app-info.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* The Electron product name (can be used for the window WMClass or passed down to the Electron packager).
|
||||||
|
*/
|
||||||
|
export const PRODUCT_NAME = "TriliumNext Notes";
|
@ -8,6 +8,7 @@ import options from "@triliumnext/server/src/services/options.js";
|
|||||||
import electronDebug from "electron-debug";
|
import electronDebug from "electron-debug";
|
||||||
import electronDl from "electron-dl";
|
import electronDl from "electron-dl";
|
||||||
import { deferred } from "@triliumnext/server/src/services/utils.js";
|
import { deferred } from "@triliumnext/server/src/services/utils.js";
|
||||||
|
import { PRODUCT_NAME } from "./app-info";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const serverInitializedPromise = deferred<void>();
|
const serverInitializedPromise = deferred<void>();
|
||||||
@ -28,6 +29,7 @@ async function main() {
|
|||||||
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
||||||
// See https://github.com/electron/electron/issues/46538 for more info.
|
// See https://github.com/electron/electron/issues/46538 for more info.
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
|
electron.app.setName(PRODUCT_NAME);
|
||||||
electron.app.commandLine.appendSwitch("gtk-version", "3");
|
electron.app.commandLine.appendSwitch("gtk-version", "3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
apps/desktop/tsconfig.forge.json
Normal file
23
apps/desktop/tsconfig.forge.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"target": "ES2020",
|
||||||
|
"outDir": "dist",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"express"
|
||||||
|
],
|
||||||
|
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"../server/src/*.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"eslint.config.js",
|
||||||
|
"eslint.config.cjs",
|
||||||
|
"eslint.config.mjs"
|
||||||
|
]
|
||||||
|
}
|
@ -11,6 +11,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.app.json"
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.forge.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"@triliumnext/desktop": "workspace:*",
|
"@triliumnext/desktop": "workspace:*",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"copy-webpack-plugin": "13.0.0",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"electron": "36.4.0",
|
"electron": "36.5.0",
|
||||||
"fs-extra": "11.3.0"
|
"fs-extra": "11.3.0"
|
||||||
},
|
},
|
||||||
"nx": {
|
"nx": {
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
"debounce": "2.2.0",
|
"debounce": "2.2.0",
|
||||||
"debug": "4.4.1",
|
"debug": "4.4.1",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"electron": "36.4.0",
|
"electron": "36.5.0",
|
||||||
"electron-debug": "4.1.0",
|
"electron-debug": "4.1.0",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
|
@ -5,7 +5,7 @@ import cls from "../services/cls.js";
|
|||||||
import sql from "../services/sql.js";
|
import sql from "../services/sql.js";
|
||||||
import becca from "../becca/becca.js";
|
import becca from "../becca/becca.js";
|
||||||
import type { Request, Response, Router } from "express";
|
import type { Request, Response, Router } from "express";
|
||||||
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
|
import { safeExtractMessageAndStackFromError, normalizeCustomHandlerPattern } from "../services/utils.js";
|
||||||
|
|
||||||
function handleRequest(req: Request, res: Response) {
|
function handleRequest(req: Request, res: Response) {
|
||||||
|
|
||||||
@ -38,11 +38,19 @@ function handleRequest(req: Request, res: Response) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = new RegExp(`^${attr.value}$`);
|
// Get normalized patterns to handle both trailing slash cases
|
||||||
let match;
|
const patterns = normalizeCustomHandlerPattern(attr.value);
|
||||||
|
let match: RegExpMatchArray | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
match = path.match(regex);
|
// Try each pattern until we find a match
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const regex = new RegExp(`^${pattern}$`);
|
||||||
|
match = path.match(regex);
|
||||||
|
if (match) {
|
||||||
|
break; // Found a match, exit pattern loop
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||||
log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${errMessage}, stack: ${errStack}`);
|
log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${errMessage}, stack: ${errStack}`);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import optionService from "./options.js";
|
import optionService from "./options.js";
|
||||||
import config from "./config.js";
|
import config from "./config.js";
|
||||||
|
import { normalizeUrl } from "./utils.js";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Primary configuration for sync is in the options (document), but we allow to override
|
* Primary configuration for sync is in the options (document), but we allow to override
|
||||||
@ -17,7 +18,10 @@ function get(name: keyof typeof config.Sync) {
|
|||||||
export default {
|
export default {
|
||||||
// env variable is the easiest way to guarantee we won't overwrite prod data during development
|
// env variable is the easiest way to guarantee we won't overwrite prod data during development
|
||||||
// after copying prod document/data directory
|
// after copying prod document/data directory
|
||||||
getSyncServerHost: () => get("syncServerHost"),
|
getSyncServerHost: () => {
|
||||||
|
const host = get("syncServerHost");
|
||||||
|
return host ? normalizeUrl(host) : host;
|
||||||
|
},
|
||||||
isSyncSetup: () => {
|
isSyncSetup: () => {
|
||||||
const syncServerHost = get("syncServerHost");
|
const syncServerHost = get("syncServerHost");
|
||||||
|
|
||||||
|
@ -628,3 +628,56 @@ describe("#formatDownloadTitle", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#normalizeUrl", () => {
|
||||||
|
const testCases: TestCase<typeof utils.normalizeUrl>[] = [
|
||||||
|
[ "should remove trailing slash from simple URL", [ "https://example.com/" ], "https://example.com" ],
|
||||||
|
[ "should remove trailing slash from URL with path", [ "https://example.com/path/" ], "https://example.com/path" ],
|
||||||
|
[ "should preserve URL without trailing slash", [ "https://example.com" ], "https://example.com" ],
|
||||||
|
[ "should preserve URL without trailing slash with path", [ "https://example.com/path" ], "https://example.com/path" ],
|
||||||
|
[ "should preserve protocol-only URLs", [ "https://" ], "https://" ],
|
||||||
|
[ "should preserve protocol-only URLs", [ "http://" ], "http://" ],
|
||||||
|
[ "should fix double slashes in path", [ "https://example.com//api//test" ], "https://example.com/api/test" ],
|
||||||
|
[ "should handle multiple double slashes", [ "https://example.com///api///test" ], "https://example.com/api/test" ],
|
||||||
|
[ "should handle trailing slash with double slashes", [ "https://example.com//api//" ], "https://example.com/api" ],
|
||||||
|
[ "should preserve protocol double slash", [ "https://example.com/api" ], "https://example.com/api" ],
|
||||||
|
[ "should handle empty string", [ "" ], "" ],
|
||||||
|
[ "should handle whitespace-only string", [ " " ], "" ],
|
||||||
|
[ "should trim whitespace", [ " https://example.com/ " ], "https://example.com" ],
|
||||||
|
[ "should handle null as empty", [ null as any ], null ],
|
||||||
|
[ "should handle undefined as empty", [ undefined as any ], undefined ]
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const [ desc, fnParams, expected ] = testCase;
|
||||||
|
it(desc, () => {
|
||||||
|
const result = utils.normalizeUrl(...fnParams);
|
||||||
|
expect(result).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#normalizeCustomHandlerPattern", () => {
|
||||||
|
const testCases: TestCase<typeof utils.normalizeCustomHandlerPattern>[] = [
|
||||||
|
[ "should handle pattern without ending - add both versions", [ "foo" ], [ "foo", "foo/" ] ],
|
||||||
|
[ "should handle pattern with trailing slash - add both versions", [ "foo/" ], [ "foo", "foo/" ] ],
|
||||||
|
[ "should handle pattern ending with $ - add optional slash", [ "foo$" ], [ "foo/?$" ] ],
|
||||||
|
[ "should handle pattern with trailing slash and $ - add both versions", [ "foo/$" ], [ "foo$", "foo/$" ] ],
|
||||||
|
[ "should preserve existing optional slash pattern", [ "foo/?$" ], [ "foo/?$" ] ],
|
||||||
|
[ "should preserve existing optional slash pattern (alternative)", [ "foo/?)" ], [ "foo/?)" ] ],
|
||||||
|
[ "should handle regex pattern with special chars", [ "api/[a-z]+$" ], [ "api/[a-z]+/?$" ] ],
|
||||||
|
[ "should handle complex regex pattern", [ "user/([0-9]+)/profile$" ], [ "user/([0-9]+)/profile/?$" ] ],
|
||||||
|
[ "should handle empty string", [ "" ], [ "" ] ],
|
||||||
|
[ "should handle whitespace-only string", [ " " ], [ "" ] ],
|
||||||
|
[ "should handle null", [ null as any ], [ null ] ],
|
||||||
|
[ "should handle undefined", [ undefined as any ], [ undefined ] ]
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const [ desc, fnParams, expected ] = testCase;
|
||||||
|
it(desc, () => {
|
||||||
|
const result = utils.normalizeCustomHandlerPattern(...fnParams);
|
||||||
|
expect(result).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -375,6 +375,85 @@ export function safeExtractMessageAndStackFromError(err: unknown): [errMessage:
|
|||||||
return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const;
|
return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes URL by removing trailing slashes and fixing double slashes.
|
||||||
|
* Preserves the protocol (http://, https://) but removes trailing slashes from the rest.
|
||||||
|
*
|
||||||
|
* @param url The URL to normalize
|
||||||
|
* @returns The normalized URL without trailing slashes
|
||||||
|
*/
|
||||||
|
export function normalizeUrl(url: string | null | undefined): string | null | undefined {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace
|
||||||
|
url = url.trim();
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix double slashes (except in protocol) first
|
||||||
|
url = url.replace(/([^:]\/)\/+/g, '$1');
|
||||||
|
|
||||||
|
// Remove trailing slash, but preserve protocol
|
||||||
|
if (url.endsWith('/') && !url.match(/^https?:\/\/$/)) {
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a path pattern for custom request handlers.
|
||||||
|
* Ensures both trailing slash and non-trailing slash versions are handled.
|
||||||
|
*
|
||||||
|
* @param pattern The original pattern from customRequestHandler attribute
|
||||||
|
* @returns An array of patterns to match both with and without trailing slash
|
||||||
|
*/
|
||||||
|
export function normalizeCustomHandlerPattern(pattern: string | null | undefined): (string | null | undefined)[] {
|
||||||
|
if (!pattern || typeof pattern !== 'string') {
|
||||||
|
return [pattern];
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = pattern.trim();
|
||||||
|
|
||||||
|
if (!pattern) {
|
||||||
|
return [pattern];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pattern already ends with optional trailing slash, return as-is
|
||||||
|
if (pattern.endsWith('/?$') || pattern.endsWith('/?)')) {
|
||||||
|
return [pattern];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pattern ends with $, handle it specially
|
||||||
|
if (pattern.endsWith('$')) {
|
||||||
|
const basePattern = pattern.slice(0, -1);
|
||||||
|
|
||||||
|
// If already ends with slash, create both versions
|
||||||
|
if (basePattern.endsWith('/')) {
|
||||||
|
const withoutSlash = basePattern.slice(0, -1) + '$';
|
||||||
|
const withSlash = pattern;
|
||||||
|
return [withoutSlash, withSlash];
|
||||||
|
} else {
|
||||||
|
// Add optional trailing slash
|
||||||
|
const withSlash = basePattern + '/?$';
|
||||||
|
return [withSlash];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For patterns without $, add both versions
|
||||||
|
if (pattern.endsWith('/')) {
|
||||||
|
const withoutSlash = pattern.slice(0, -1);
|
||||||
|
return [withoutSlash, pattern];
|
||||||
|
} else {
|
||||||
|
const withSlash = pattern + '/';
|
||||||
|
return [pattern, withSlash];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
compareVersions,
|
compareVersions,
|
||||||
@ -400,6 +479,8 @@ export default {
|
|||||||
md5,
|
md5,
|
||||||
newEntityId,
|
newEntityId,
|
||||||
normalize,
|
normalize,
|
||||||
|
normalizeCustomHandlerPattern,
|
||||||
|
normalizeUrl,
|
||||||
quoteRegex,
|
quoteRegex,
|
||||||
randomSecureToken,
|
randomSecureToken,
|
||||||
randomString,
|
randomString,
|
||||||
|
@ -1,38 +1,11 @@
|
|||||||
# sv
|
# apps/website
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
Landing page for Trilium Notes powered by [Svelte](https://github.com/sveltejs/cli) and [Tailwind CSS](https://tailwindcss.com/).
|
||||||
|
|
||||||
## Creating a project
|
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# create a new project in the current directory
|
|
||||||
npx sv create
|
|
||||||
|
|
||||||
# create a new project in my-app
|
|
||||||
npx sv create my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
To run a dev server that will hot-reload changes: `pnpm nx run website:dev`
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
To create a production version of your app:
|
To create a production build: `pnpm nx run website:build`
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
|
||||||
|
|
||||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
|
||||||
|
@ -9,4 +9,6 @@ The license key is stored in the application and it enables the use of the previ
|
|||||||
|
|
||||||
## Can I opt out of these features?
|
## Can I opt out of these features?
|
||||||
|
|
||||||
At this moment there is no way to disable this features, apart from manually modifying the source code. If this is a problem, [let us know](../../Troubleshooting/Reporting%20issues.md).
|
At this moment there is no way to disable these features, apart from manually modifying the source code. If this is a problem, [let us know](../../Troubleshooting/Reporting%20issues.md).
|
||||||
|
|
||||||
|
If you have the possibility of rebuilding the source code (e.g. if a package maintainer), then modify `VITE_CKEDITOR_KEY` in `apps/client/.env` to be `GPL`.
|
@ -169,8 +169,6 @@
|
|||||||
comment = meta.description;
|
comment = meta.description;
|
||||||
desktopName = "TriliumNext Notes";
|
desktopName = "TriliumNext Notes";
|
||||||
categories = [ "Office" ];
|
categories = [ "Office" ];
|
||||||
# TODO: electron-forge build has this set to PRODUCT_NAME (forge.config.cjs)
|
|
||||||
# But the plain build doesn't set this (or the app icon).
|
|
||||||
startupWMClass = "TriliumNext Notes";
|
startupWMClass = "TriliumNext Notes";
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
25
package.json
25
package.json
@ -27,24 +27,25 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/rebuild": "4.0.1",
|
"@electron/rebuild": "4.0.1",
|
||||||
"@nx/devkit": "21.2.0",
|
"@nx/devkit": "21.2.1",
|
||||||
"@nx/esbuild": "21.2.0",
|
"@nx/esbuild": "21.2.1",
|
||||||
"@nx/eslint": "21.2.0",
|
"@nx/eslint": "21.2.1",
|
||||||
"@nx/eslint-plugin": "21.2.0",
|
"@nx/eslint-plugin": "21.2.1",
|
||||||
"@nx/express": "21.2.0",
|
"@nx/express": "21.2.1",
|
||||||
"@nx/js": "21.2.0",
|
"@nx/js": "21.2.1",
|
||||||
"@nx/node": "21.2.0",
|
"@nx/node": "21.2.1",
|
||||||
"@nx/playwright": "21.2.0",
|
"@nx/playwright": "21.2.1",
|
||||||
"@nx/vite": "21.2.0",
|
"@nx/vite": "21.2.1",
|
||||||
"@nx/web": "21.2.0",
|
"@nx/web": "21.2.1",
|
||||||
"@playwright/test": "^1.36.0",
|
"@playwright/test": "^1.36.0",
|
||||||
"@triliumnext/server": "workspace:*",
|
"@triliumnext/server": "workspace:*",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.32",
|
||||||
"@vitest/coverage-v8": "^3.0.5",
|
"@vitest/coverage-v8": "^3.0.5",
|
||||||
"@vitest/ui": "^3.0.0",
|
"@vitest/ui": "^3.0.0",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
"dpdm": "3.14.0",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "^9.8.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
@ -53,7 +54,7 @@
|
|||||||
"jiti": "2.4.2",
|
"jiti": "2.4.2",
|
||||||
"jsdom": "~26.1.0",
|
"jsdom": "~26.1.0",
|
||||||
"jsonc-eslint-parser": "^2.1.0",
|
"jsonc-eslint-parser": "^2.1.0",
|
||||||
"nx": "21.2.0",
|
"nx": "21.2.1",
|
||||||
"react-refresh": "^0.17.0",
|
"react-refresh": "^0.17.0",
|
||||||
"rollup-plugin-webpack-stats": "2.0.7",
|
"rollup-plugin-webpack-stats": "2.0.7",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
@ -11,31 +11,12 @@ export default defineConfig( {
|
|||||||
svg()
|
svg()
|
||||||
],
|
],
|
||||||
test: {
|
test: {
|
||||||
browser: {
|
environment: "happy-dom",
|
||||||
enabled: true,
|
|
||||||
name: 'chrome',
|
|
||||||
provider: 'webdriverio',
|
|
||||||
providerOptions: {},
|
|
||||||
headless: true,
|
|
||||||
ui: false
|
|
||||||
},
|
|
||||||
include: [
|
include: [
|
||||||
'tests/**/*.[jt]s'
|
'tests/**/*.[jt]s'
|
||||||
],
|
],
|
||||||
globals: true,
|
globals: true,
|
||||||
watch: false,
|
watch: false,
|
||||||
passWithNoTests: true,
|
passWithNoTests: true
|
||||||
coverage: {
|
|
||||||
thresholds: {
|
|
||||||
lines: 100,
|
|
||||||
functions: 100,
|
|
||||||
branches: 100,
|
|
||||||
statements: 100
|
|
||||||
},
|
|
||||||
provider: 'istanbul',
|
|
||||||
include: [
|
|
||||||
'src'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import "ckeditor5/ckeditor5.css";
|
import "ckeditor5/ckeditor5.css";
|
||||||
import "./theme/code_block_toolbar.css";
|
import "./theme/code_block_toolbar.css";
|
||||||
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins";
|
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS, PREMIUM_PLUGINS } from "./plugins";
|
||||||
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
|
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
|
||||||
|
import "./translation_overrides.js";
|
||||||
export { EditorWatchdog } from "ckeditor5";
|
export { EditorWatchdog } from "ckeditor5";
|
||||||
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, Node, Position, Element, WatchdogConfig } from "ckeditor5";
|
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, Node, Position, Element, WatchdogConfig } from "ckeditor5";
|
||||||
export type { TemplateDefinition } from "ckeditor5-premium-features";
|
export type { TemplateDefinition } from "ckeditor5-premium-features";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Autoformat, AutoLink, BlockQuote, BlockToolbar, Bold, CKFinderUploadAdapter, Clipboard, Code, CodeBlock, Enter, FindAndReplace, Font, FontBackgroundColor, FontColor, GeneralHtmlSupport, Heading, HeadingButtonsUI, HorizontalLine, Image, ImageCaption, ImageInline, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Alignment, Indent, IndentBlock, Italic, Link, List, ListProperties, Mention, PageBreak, Paragraph, ParagraphButtonUI, PasteFromOffice, PictureEditing, RemoveFormat, SelectAll, ShiftEnter, SpecialCharacters, SpecialCharactersEssentials, Strikethrough, Style, Subscript, Superscript, Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableSelection, TableToolbar, TextPartLanguage, TextTransformation, TodoList, Typing, Underline, Undo, Bookmark, Emoji } from "ckeditor5";
|
import { Autoformat, AutoLink, BlockQuote, BlockToolbar, Bold, CKFinderUploadAdapter, Clipboard, Code, CodeBlock, Enter, FindAndReplace, Font, FontBackgroundColor, FontColor, GeneralHtmlSupport, Heading, HeadingButtonsUI, HorizontalLine, Image, ImageCaption, ImageInline, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Alignment, Indent, IndentBlock, Italic, Link, List, ListProperties, Mention, PageBreak, Paragraph, ParagraphButtonUI, PasteFromOffice, PictureEditing, RemoveFormat, SelectAll, ShiftEnter, SpecialCharacters, SpecialCharactersEssentials, Strikethrough, Style, Subscript, Superscript, Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableSelection, TableToolbar, TextPartLanguage, TextTransformation, TodoList, Typing, Underline, Undo, Bookmark, Emoji, Notification } from "ckeditor5";
|
||||||
import { SlashCommand, Template } from "ckeditor5-premium-features";
|
import { SlashCommand, Template } from "ckeditor5-premium-features";
|
||||||
import type { Plugin } from "ckeditor5";
|
import type { Plugin } from "ckeditor5";
|
||||||
import CutToNotePlugin from "./plugins/cuttonote.js";
|
import CutToNotePlugin from "./plugins/cuttonote.js";
|
||||||
@ -148,8 +148,7 @@ export const COMMON_PLUGINS: typeof Plugin[] = [
|
|||||||
Emoji,
|
Emoji,
|
||||||
|
|
||||||
...TRILIUM_PLUGINS,
|
...TRILIUM_PLUGINS,
|
||||||
...EXTERNAL_PLUGINS,
|
...EXTERNAL_PLUGINS
|
||||||
...PREMIUM_PLUGINS
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,5 +156,5 @@ export const COMMON_PLUGINS: typeof Plugin[] = [
|
|||||||
*/
|
*/
|
||||||
export const POPUP_EDITOR_PLUGINS: typeof Plugin[] = [
|
export const POPUP_EDITOR_PLUGINS: typeof Plugin[] = [
|
||||||
...COMMON_PLUGINS,
|
...COMMON_PLUGINS,
|
||||||
BlockToolbar
|
BlockToolbar,
|
||||||
];
|
];
|
||||||
|
8
packages/ckeditor5/src/translation_overrides.ts
Normal file
8
packages/ckeditor5/src/translation_overrides.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
window.CKEDITOR_TRANSLATIONS = {
|
||||||
|
en: {
|
||||||
|
dictionary: {
|
||||||
|
"Insert template": "Insert text snippet",
|
||||||
|
"Search template": "Search text snippet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
37
packages/ckeditor5/tests/templates.ts
Normal file
37
packages/ckeditor5/tests/templates.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { it } from "vitest";
|
||||||
|
import { describe } from "vitest";
|
||||||
|
import { ClassicEditor } from "../src/index.js";
|
||||||
|
import { type BalloonEditor, type ButtonView, type Editor } from "ckeditor5";
|
||||||
|
import { beforeEach } from "vitest";
|
||||||
|
import { expect } from "vitest";
|
||||||
|
|
||||||
|
describe("Text snippets", () => {
|
||||||
|
let editorElement: HTMLDivElement;
|
||||||
|
let editor: Editor;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
editorElement = document.createElement( 'div' );
|
||||||
|
document.body.appendChild( editorElement );
|
||||||
|
|
||||||
|
console.log("Trigger each");
|
||||||
|
|
||||||
|
editor = await ClassicEditor.create(editorElement, {
|
||||||
|
licenseKey: "GPL",
|
||||||
|
toolbar: {
|
||||||
|
items: [
|
||||||
|
"insertTemplate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses correct translations", () => {
|
||||||
|
const itemsWithButtonView = Array.from(editor.ui.view.toolbar?.items)
|
||||||
|
.filter(item => "buttonView" in item)
|
||||||
|
.map(item => (item.buttonView as ButtonView).label);
|
||||||
|
|
||||||
|
expect(itemsWithButtonView).not.toContain("Insert template");
|
||||||
|
expect(itemsWithButtonView).toContain("Insert text snippet");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -23,9 +23,9 @@
|
|||||||
"@codemirror/lang-css": "6.3.1",
|
"@codemirror/lang-css": "6.3.1",
|
||||||
"@codemirror/lang-html": "6.4.9",
|
"@codemirror/lang-html": "6.4.9",
|
||||||
"@codemirror/lang-javascript": "6.2.4",
|
"@codemirror/lang-javascript": "6.2.4",
|
||||||
"@codemirror/lang-json": "6.0.1",
|
"@codemirror/lang-json": "6.0.2",
|
||||||
"@codemirror/lang-markdown": "6.3.3",
|
"@codemirror/lang-markdown": "6.3.3",
|
||||||
"@codemirror/lang-php": "6.0.1",
|
"@codemirror/lang-php": "6.0.2",
|
||||||
"@codemirror/lang-vue": "0.1.3",
|
"@codemirror/lang-vue": "0.1.3",
|
||||||
"@codemirror/lang-xml": "6.1.0",
|
"@codemirror/lang-xml": "6.1.0",
|
||||||
"@codemirror/legacy-modes": "6.5.1",
|
"@codemirror/legacy-modes": "6.5.1",
|
||||||
|
986
pnpm-lock.yaml
generated
986
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user