mirror of
https://github.com/standardnotes/app.git
synced 2026-01-11 19:56:41 +00:00
feat: add snjs package
This commit is contained in:
parent
321a055bae
commit
0e40469e2f
296 changed files with 46109 additions and 187 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -26,6 +26,7 @@ packages/api/dist
|
||||||
packages/responses/dist
|
packages/responses/dist
|
||||||
packages/sncrypto-common/dist
|
packages/sncrypto-common/dist
|
||||||
packages/sncrypto-web/dist
|
packages/sncrypto-web/dist
|
||||||
|
packages/snjs/dist
|
||||||
|
|
||||||
**/.pnp.*
|
**/.pnp.*
|
||||||
**/.yarn/*
|
**/.yarn/*
|
||||||
|
|
|
||||||
BIN
.yarn/cache/@mrmlnc-readdir-enhanced-npm-2.2.1-5286808663-d3b82b2936.zip
vendored
Normal file
BIN
.yarn/cache/@mrmlnc-readdir-enhanced-npm-2.2.1-5286808663-d3b82b2936.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@nodelib-fs.stat-npm-1.1.3-95bc1892a0-318deab369.zip
vendored
Normal file
BIN
.yarn/cache/@nodelib-fs.stat-npm-1.1.3-95bc1892a0-318deab369.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@sinonjs-samsam-npm-6.1.1-06e18132d0-a09b0914bf.zip
vendored
Normal file
BIN
.yarn/cache/@sinonjs-samsam-npm-6.1.1-06e18132d0-a09b0914bf.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@sinonjs-text-encoding-npm-0.7.1-865b0079b5-130de0bb56.zip
vendored
Normal file
BIN
.yarn/cache/@sinonjs-text-encoding-npm-0.7.1-865b0079b5-130de0bb56.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-domain-events-npm-2.39.0-ee14411bb8-7a37b281ef.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-domain-events-npm-2.39.0-ee14411bb8-7a37b281ef.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@standardnotes-predicates-npm-1.2.0-8a42210bdb-779a3fcf93.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-predicates-npm-1.2.0-8a42210bdb-779a3fcf93.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-settings-npm-1.17.0-fc942a7080-b376015038.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-settings-npm-1.17.0-fc942a7080-b376015038.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@types-crypto-js-npm-4.1.1-1c4ce3312a-ea3d6a67b6.zip
vendored
Normal file
BIN
.yarn/cache/@types-crypto-js-npm-4.1.1-1c4ce3312a-ea3d6a67b6.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@ungap-promise-all-settled-npm-1.1.2-c0f42e147b-08d37fdfa2.zip
vendored
Normal file
BIN
.yarn/cache/@ungap-promise-all-settled-npm-1.1.2-c0f42e147b-08d37fdfa2.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/ansi-colors-npm-4.1.1-97ad42f223-138d04a510.zip
vendored
Normal file
BIN
.yarn/cache/ansi-colors-npm-4.1.1-97ad42f223-138d04a510.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/args-npm-5.0.3-ec59f35e6d-ac39e65609.zip
vendored
Normal file
BIN
.yarn/cache/args-npm-5.0.3-ec59f35e6d-ac39e65609.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/browser-stdout-npm-1.3.1-6b2376bf3f-b717b19b25.zip
vendored
Normal file
BIN
.yarn/cache/browser-stdout-npm-1.3.1-6b2376bf3f-b717b19b25.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/call-me-maybe-npm-1.0.1-d07e74bc9c-d19e9d6ac2.zip
vendored
Normal file
BIN
.yarn/cache/call-me-maybe-npm-1.0.1-d07e74bc9c-d19e9d6ac2.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/camelcase-npm-5.0.0-c808398846-8bfe920e04.zip
vendored
Normal file
BIN
.yarn/cache/camelcase-npm-5.0.0-c808398846-8bfe920e04.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/chai-as-promised-npm-7.1.1-cdc17e4612-7262868a5b.zip
vendored
Normal file
BIN
.yarn/cache/chai-as-promised-npm-7.1.1-cdc17e4612-7262868a5b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/chai-subset-npm-1.6.0-3cee47a65d-c85a64b42d.zip
vendored
Normal file
BIN
.yarn/cache/chai-subset-npm-1.6.0-3cee47a65d-c85a64b42d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/crypto-js-npm-4.1.1-38a3b8c19d-b3747c12ee.zip
vendored
Normal file
BIN
.yarn/cache/crypto-js-npm-4.1.1-38a3b8c19d-b3747c12ee.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/debug-npm-4.3.3-710fd4cc7f-14472d56fe.zip
vendored
Normal file
BIN
.yarn/cache/debug-npm-4.3.3-710fd4cc7f-14472d56fe.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/devtools-protocol-npm-0.0.981744-4636a91b0c-609901bff5.zip
vendored
Normal file
BIN
.yarn/cache/devtools-protocol-npm-0.0.981744-4636a91b0c-609901bff5.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/diff-npm-5.0.0-ad6900db18-f19fe29284.zip
vendored
Normal file
BIN
.yarn/cache/diff-npm-5.0.0-ad6900db18-f19fe29284.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/docdash-npm-1.2.0-1aba121ac3-0c1ca8a2d8.zip
vendored
Normal file
BIN
.yarn/cache/docdash-npm-1.2.0-1aba121ac3-0c1ca8a2d8.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/dom-storage-npm-2.1.0-637aad78a8-b17f9f9a13.zip
vendored
Normal file
BIN
.yarn/cache/dom-storage-npm-2.1.0-637aad78a8-b17f9f9a13.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/exports-loader-npm-3.1.0-083dec0b38-1bf210433b.zip
vendored
Normal file
BIN
.yarn/cache/exports-loader-npm-3.1.0-083dec0b38-1bf210433b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/extract-zip-npm-2.0.1-92a28e392b-8cbda9debd.zip
vendored
Normal file
BIN
.yarn/cache/extract-zip-npm-2.0.1-92a28e392b-8cbda9debd.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/fast-glob-npm-2.2.7-f211fb26f4-304ccff1d4.zip
vendored
Normal file
BIN
.yarn/cache/fast-glob-npm-2.2.7-f211fb26f4-304ccff1d4.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/glob-npm-7.2.0-bb4644d239-78a8ea9423.zip
vendored
Normal file
BIN
.yarn/cache/glob-npm-7.2.0-bb4644d239-78a8ea9423.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/glob-to-regexp-npm-0.3.0-4f55888857-d34b3219d8.zip
vendored
Normal file
BIN
.yarn/cache/glob-to-regexp-npm-0.3.0-4f55888857-d34b3219d8.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/globby-npm-9.2.0-686548dc5f-9b4cb70aa0.zip
vendored
Normal file
BIN
.yarn/cache/globby-npm-9.2.0-686548dc5f-9b4cb70aa0.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/growl-npm-1.10.5-2d1da54198-4b86685de6.zip
vendored
Normal file
BIN
.yarn/cache/growl-npm-1.10.5-2d1da54198-4b86685de6.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/jest-environment-jsdom-npm-28.1.2-225ffc028f-73388b5cde.zip
vendored
Normal file
BIN
.yarn/cache/jest-environment-jsdom-npm-28.1.2-225ffc028f-73388b5cde.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/jsdom-npm-19.0.0-f0768fdc93-94b693bf4a.zip
vendored
Normal file
BIN
.yarn/cache/jsdom-npm-19.0.0-f0768fdc93-94b693bf4a.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/just-extend-npm-4.2.1-ccc4201277-ff9fdede24.zip
vendored
Normal file
BIN
.yarn/cache/just-extend-npm-4.2.1-ccc4201277-ff9fdede24.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/leven-npm-2.1.0-19f0a16606-f7b4a01b15.zip
vendored
Normal file
BIN
.yarn/cache/leven-npm-2.1.0-19f0a16606-f7b4a01b15.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/lodash.get-npm-4.4.2-7bda64ed87-e403047ddb.zip
vendored
Normal file
BIN
.yarn/cache/lodash.get-npm-4.4.2-7bda64ed87-e403047ddb.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/minimatch-npm-4.2.1-558ec7f418-2b1514e3d0.zip
vendored
Normal file
BIN
.yarn/cache/minimatch-npm-4.2.1-558ec7f418-2b1514e3d0.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/mocha-headless-chrome-npm-4.0.0-b66897187f-ab802a342d.zip
vendored
Normal file
BIN
.yarn/cache/mocha-headless-chrome-npm-4.0.0-b66897187f-ab802a342d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/mocha-npm-9.2.2-f7735febb8-4d5ca4ce33.zip
vendored
Normal file
BIN
.yarn/cache/mocha-npm-9.2.2-f7735febb8-4d5ca4ce33.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/mri-npm-1.1.4-d22a399f26-e65b9aed3b.zip
vendored
Normal file
BIN
.yarn/cache/mri-npm-1.1.4-d22a399f26-e65b9aed3b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/nanoid-npm-3.3.1-bdd760bee0-4ef0969e1b.zip
vendored
Normal file
BIN
.yarn/cache/nanoid-npm-3.3.1-bdd760bee0-4ef0969e1b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/nise-npm-5.1.1-210b3fdf40-d8be29e84a.zip
vendored
Normal file
BIN
.yarn/cache/nise-npm-5.1.1-210b3fdf40-d8be29e84a.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/nock-npm-13.2.8-6b3bcf0f50-656f696d3c.zip
vendored
Normal file
BIN
.yarn/cache/nock-npm-13.2.8-6b3bcf0f50-656f696d3c.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/propagate-npm-2.0.1-2074bf76d3-c4febaee2b.zip
vendored
Normal file
BIN
.yarn/cache/propagate-npm-2.0.1-2074bf76d3-c4febaee2b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/proxy-from-env-npm-1.1.0-c13d07f26b-ed7fcc2ba0.zip
vendored
Normal file
BIN
.yarn/cache/proxy-from-env-npm-1.1.0-c13d07f26b-ed7fcc2ba0.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/puppeteer-npm-13.7.0-e6812428d2-4062b3ac33.zip
vendored
Normal file
BIN
.yarn/cache/puppeteer-npm-13.7.0-e6812428d2-4062b3ac33.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/raw-loader-npm-0.5.1-842d4ead25-8051ec0b80.zip
vendored
Normal file
BIN
.yarn/cache/raw-loader-npm-0.5.1-842d4ead25-8051ec0b80.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/script-loader-npm-0.7.2-cfe1d20d30-e01b3fb3e5.zip
vendored
Normal file
BIN
.yarn/cache/script-loader-npm-0.7.2-cfe1d20d30-e01b3fb3e5.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/sinon-npm-13.0.2-8544b59862-237f21c8c4.zip
vendored
Normal file
BIN
.yarn/cache/sinon-npm-13.0.2-8544b59862-237f21c8c4.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/tscpaths-npm-0.0.9-90fb6cfbb0-384fc9f22d.zip
vendored
Normal file
BIN
.yarn/cache/tscpaths-npm-0.0.9-90fb6cfbb0-384fc9f22d.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/unbzip2-stream-npm-1.4.3-c5582d6a9f-0e67c4a91f.zip
vendored
Normal file
BIN
.yarn/cache/unbzip2-stream-npm-1.4.3-c5582d6a9f-0e67c4a91f.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/whatwg-url-npm-10.0.0-769b9530cc-a21ec309c5.zip
vendored
Normal file
BIN
.yarn/cache/whatwg-url-npm-10.0.0-769b9530cc-a21ec309c5.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/workerpool-npm-6.2.0-d2a722f6bb-3493b4f0ef.zip
vendored
Normal file
BIN
.yarn/cache/workerpool-npm-6.2.0-d2a722f6bb-3493b4f0ef.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/ws-npm-8.5.0-8e99728c84-76f2f90e40.zip
vendored
Normal file
BIN
.yarn/cache/ws-npm-8.5.0-8e99728c84-76f2f90e40.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/yargs-parser-npm-20.2.4-1de20916a6-d251998a37.zip
vendored
Normal file
BIN
.yarn/cache/yargs-parser-npm-20.2.4-1de20916a6-d251998a37.zip
vendored
Normal file
Binary file not shown.
|
|
@ -28,6 +28,7 @@
|
||||||
"build:desktop": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/desktop --exclude @standardnotes/components-meta run build",
|
"build:desktop": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/desktop --exclude @standardnotes/components-meta run build",
|
||||||
"build:mobile": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/mobile --exclude @standardnotes/components-meta run build",
|
"build:mobile": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/mobile --exclude @standardnotes/components-meta run build",
|
||||||
"build:web-server": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/web-server --exclude @standardnotes/components-meta run build",
|
"build:web-server": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/web-server --exclude @standardnotes/components-meta run build",
|
||||||
|
"build:snjs": "yarn workspaces foreach -pt --verbose -R --from @standardnotes/snjs --exclude @standardnotes/components-meta run build",
|
||||||
"start:server:web": "lerna run start --scope=@standardnotes/web-server",
|
"start:server:web": "lerna run start --scope=@standardnotes/web-server",
|
||||||
"start:server:web:localhost": "lerna run start:no-binding --scope=@standardnotes/web-server",
|
"start:server:web:localhost": "lerna run start:no-binding --scope=@standardnotes/web-server",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standardnotes/sncrypto-web": "workspace:*",
|
"@standardnotes/sncrypto-web": "workspace:*",
|
||||||
"@standardnotes/snjs": "^2.61.3",
|
"@standardnotes/snjs": "workspace:*",
|
||||||
"regenerator-runtime": "^0.13.9"
|
"regenerator-runtime": "^0.13.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ module.exports = (async () => {
|
||||||
'../services',
|
'../services',
|
||||||
'../files',
|
'../files',
|
||||||
'../utils',
|
'../utils',
|
||||||
'../sncrypto-common'
|
'../sncrypto-common',
|
||||||
|
'../snjs',
|
||||||
],
|
],
|
||||||
transformer: {
|
transformer: {
|
||||||
getTransformOptions: async () => ({
|
getTransformOptions: async () => ({
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"@standardnotes/react-native-textview": "1.1.0",
|
"@standardnotes/react-native-textview": "1.1.0",
|
||||||
"@standardnotes/react-native-utils": "1.0.1",
|
"@standardnotes/react-native-utils": "1.0.1",
|
||||||
"@standardnotes/sncrypto-common": "workspace:*",
|
"@standardnotes/sncrypto-common": "workspace:*",
|
||||||
"@standardnotes/snjs": "^2.118.3",
|
"@standardnotes/snjs": "workspace:*",
|
||||||
"@standardnotes/stylekit": "5.29.3",
|
"@standardnotes/stylekit": "5.29.3",
|
||||||
"@standardnotes/utils": "workspace:*",
|
"@standardnotes/utils": "workspace:*",
|
||||||
"@standardnotes/web": "workspace:*",
|
"@standardnotes/web": "workspace:*",
|
||||||
|
|
|
||||||
7
packages/snjs/.browserslistrc
Normal file
7
packages/snjs/.browserslistrc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Edge 16
|
||||||
|
Firefox 53
|
||||||
|
Chrome 57
|
||||||
|
Safari 11
|
||||||
|
Opera 44
|
||||||
|
ios 11
|
||||||
|
ChromeAndroid 84
|
||||||
5
packages/snjs/.eslintignore
Normal file
5
packages/snjs/.eslintignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
test
|
||||||
|
*.config.js
|
||||||
|
mocha/**/*
|
||||||
9
packages/snjs/.eslintrc
Normal file
9
packages/snjs/.eslintrc
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../.eslintrc",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./linter.tsconfig.json"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-explicit-any": ["warn", { "ignoreRestArgs": true }]
|
||||||
|
}
|
||||||
|
}
|
||||||
2181
packages/snjs/CHANGELOG.md
Normal file
2181
packages/snjs/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
7
packages/snjs/babel.config.js
Normal file
7
packages/snjs/babel.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = function (api) {
|
||||||
|
api.cache.forever();
|
||||||
|
|
||||||
|
return {
|
||||||
|
presets: ['@babel/preset-env'],
|
||||||
|
};
|
||||||
|
};
|
||||||
2
packages/snjs/jest-global.ts
Normal file
2
packages/snjs/jest-global.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
//@ts-ignore
|
||||||
|
global['__VERSION__'] = global['SnjsVersion'] = require('./package.json').version
|
||||||
37
packages/snjs/jest.config.js
Normal file
37
packages/snjs/jest.config.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const base = require('../../node_modules/@standardnotes/config/src/jest.json');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...base,
|
||||||
|
moduleNameMapper: {
|
||||||
|
'@Lib/(.*)': '<rootDir>/lib/$1',
|
||||||
|
'@Services/(.*)': '<rootDir>/lib/Services/$1',
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: '<rootDir>/lib/tsconfig.json',
|
||||||
|
isolatedModules: true,
|
||||||
|
babelConfig: 'babel.config.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clearMocks: true,
|
||||||
|
collectCoverageFrom: ['lib/**/{!(index),}.ts'],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
coverageReporters: ['json', 'text', 'html'],
|
||||||
|
resetMocks: true,
|
||||||
|
resetModules: true,
|
||||||
|
roots: ['<rootDir>/lib'],
|
||||||
|
setupFiles: ['<rootDir>/jest-global.ts'],
|
||||||
|
setupFilesAfterEnv: [],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|js)?$': 'ts-jest',
|
||||||
|
},
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 13,
|
||||||
|
functions: 22,
|
||||||
|
lines: 27,
|
||||||
|
statements: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
22
packages/snjs/jsdoc.json
Normal file
22
packages/snjs/jsdoc.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"includePattern": ".+\\.js(doc|x)?$",
|
||||||
|
"include": ["lib"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
},
|
||||||
|
"recurseDepth": 10,
|
||||||
|
"opts": {
|
||||||
|
"destination": "./docs/",
|
||||||
|
"recurse": true,
|
||||||
|
"template": "node_modules/docdash"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"allowUnknownTags": true,
|
||||||
|
"dictionaries": ["jsdoc", "closure"]
|
||||||
|
},
|
||||||
|
"docdash": {
|
||||||
|
"meta": {
|
||||||
|
"title": "SNJS Documentation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
packages/snjs/lib/Application/Application.spec.ts
Normal file
151
packages/snjs/lib/Application/Application.spec.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { SNLog } from './../Log'
|
||||||
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
|
import {
|
||||||
|
AlertService,
|
||||||
|
DeviceInterface,
|
||||||
|
Environment,
|
||||||
|
namespacedKey,
|
||||||
|
Platform,
|
||||||
|
RawStorageKey,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
import { SNApplication } from './Application'
|
||||||
|
|
||||||
|
describe('application', () => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
SNLog.onLog = console.log
|
||||||
|
SNLog.onError = console.error
|
||||||
|
|
||||||
|
let application: SNApplication
|
||||||
|
let device: DeviceInterface
|
||||||
|
let crypto: PureCryptoInterface
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const identifier = '123'
|
||||||
|
|
||||||
|
crypto = {} as jest.Mocked<PureCryptoInterface>
|
||||||
|
crypto.initialize = jest.fn()
|
||||||
|
|
||||||
|
device = {} as jest.Mocked<DeviceInterface>
|
||||||
|
device.openDatabase = jest.fn().mockResolvedValue(true)
|
||||||
|
device.getAllRawDatabasePayloads = jest.fn().mockReturnValue([])
|
||||||
|
device.setRawStorageValue = jest.fn()
|
||||||
|
device.getRawStorageValue = jest.fn().mockImplementation((key) => {
|
||||||
|
if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) {
|
||||||
|
return '10.0.0'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
device.getDatabaseKeys = async () => {
|
||||||
|
return Promise.resolve(['1', '2', '3'])
|
||||||
|
}
|
||||||
|
|
||||||
|
application = new SNApplication({
|
||||||
|
environment: Environment.Mobile,
|
||||||
|
platform: Platform.Ios,
|
||||||
|
deviceInterface: device,
|
||||||
|
crypto: crypto,
|
||||||
|
alertService: {} as jest.Mocked<AlertService>,
|
||||||
|
identifier: identifier,
|
||||||
|
defaultHost: 'localhost',
|
||||||
|
appVersion: '1.0',
|
||||||
|
})
|
||||||
|
|
||||||
|
await application.prepareForLaunch({ receiveChallenge: jest.fn() })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('diagnostics', async () => {
|
||||||
|
const diagnostics = await application.getDiagnostics()
|
||||||
|
|
||||||
|
expect(diagnostics).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
application: expect.objectContaining({
|
||||||
|
appVersion: '1.0',
|
||||||
|
environment: 3,
|
||||||
|
platform: 1,
|
||||||
|
}),
|
||||||
|
payloads: {
|
||||||
|
integrityPayloads: [],
|
||||||
|
nonDeletedItemCount: 0,
|
||||||
|
invalidPayloadsCount: 0,
|
||||||
|
},
|
||||||
|
items: { allIds: [] },
|
||||||
|
storage: {
|
||||||
|
storagePersistable: false,
|
||||||
|
persistencePolicy: 'Default',
|
||||||
|
encryptionPolicy: 'Default',
|
||||||
|
needsPersist: false,
|
||||||
|
currentPersistPromise: false,
|
||||||
|
isStorageWrapped: false,
|
||||||
|
allRawPayloadsCount: 0,
|
||||||
|
databaseKeys: ['1', '2', '3'],
|
||||||
|
},
|
||||||
|
encryption: expect.objectContaining({
|
||||||
|
getLatestVersion: '004',
|
||||||
|
hasAccount: false,
|
||||||
|
getUserVersion: undefined,
|
||||||
|
upgradeAvailable: false,
|
||||||
|
accountUpgradeAvailable: false,
|
||||||
|
passcodeUpgradeAvailable: false,
|
||||||
|
hasPasscode: false,
|
||||||
|
isPasscodeLocked: false,
|
||||||
|
itemsEncryption: expect.objectContaining({
|
||||||
|
itemsKeysIds: [],
|
||||||
|
}),
|
||||||
|
rootKeyEncryption: expect.objectContaining({
|
||||||
|
hasRootKey: false,
|
||||||
|
keyMode: 'RootKeyNone',
|
||||||
|
hasRootKeyWrapper: false,
|
||||||
|
hasAccount: false,
|
||||||
|
hasPasscode: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
api: {
|
||||||
|
hasSession: false,
|
||||||
|
user: undefined,
|
||||||
|
registering: false,
|
||||||
|
authenticating: false,
|
||||||
|
changing: false,
|
||||||
|
refreshingSession: false,
|
||||||
|
filesHost: undefined,
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
isSessionRenewChallengePresented: false,
|
||||||
|
online: false,
|
||||||
|
offline: true,
|
||||||
|
isSignedIn: false,
|
||||||
|
isSignedIntoFirstPartyServer: false,
|
||||||
|
},
|
||||||
|
sync: {
|
||||||
|
syncToken: undefined,
|
||||||
|
cursorToken: undefined,
|
||||||
|
lastSyncDate: undefined,
|
||||||
|
outOfSync: false,
|
||||||
|
completedOnlineDownloadFirstSync: false,
|
||||||
|
clientLocked: false,
|
||||||
|
databaseLoaded: false,
|
||||||
|
syncLock: false,
|
||||||
|
dealloced: false,
|
||||||
|
itemsNeedingSync: [],
|
||||||
|
itemsNeedingSyncCount: 0,
|
||||||
|
pendingRequestCount: 0,
|
||||||
|
},
|
||||||
|
protections: expect.objectContaining({
|
||||||
|
getLastSessionLength: undefined,
|
||||||
|
hasProtectionSources: false,
|
||||||
|
hasUnprotectedAccessSession: true,
|
||||||
|
hasBiometricsEnabled: false,
|
||||||
|
}),
|
||||||
|
keyRecovery: { queueLength: 0, isProcessingQueue: false },
|
||||||
|
features: {
|
||||||
|
roles: [],
|
||||||
|
features: [],
|
||||||
|
enabledExperimentalFeatures: [],
|
||||||
|
needsInitialFeaturesUpdate: true,
|
||||||
|
completedSuccessfulFeaturesRetrieval: false,
|
||||||
|
},
|
||||||
|
migrations: { activeMigrations: [] },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
1568
packages/snjs/lib/Application/Application.ts
Normal file
1568
packages/snjs/lib/Application/Application.ts
Normal file
File diff suppressed because it is too large
Load diff
90
packages/snjs/lib/Application/Event.ts
Normal file
90
packages/snjs/lib/Application/Event.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { SyncEvent } from '@standardnotes/services'
|
||||||
|
export { SyncEvent }
|
||||||
|
|
||||||
|
export enum ApplicationEvent {
|
||||||
|
SignedIn = 2,
|
||||||
|
SignedOut = 3,
|
||||||
|
|
||||||
|
/** When a full, potentially multi-page sync completes */
|
||||||
|
CompletedFullSync = 5,
|
||||||
|
|
||||||
|
FailedSync = 6,
|
||||||
|
HighLatencySync = 7,
|
||||||
|
EnteredOutOfSync = 8,
|
||||||
|
ExitedOutOfSync = 9,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application has finished it `prepareForLaunch` state and is now ready for unlock
|
||||||
|
* Called when the application has initialized and is ready for launch, but before
|
||||||
|
* the application has been unlocked, if applicable. Use this to do pre-launch
|
||||||
|
* configuration, but do not attempt to access user data like notes or tags.
|
||||||
|
*/
|
||||||
|
Started = 10,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The applicaiton is fully unlocked and ready for i/o
|
||||||
|
* Called when the application has been fully decrypted and unlocked. Use this to
|
||||||
|
* to begin streaming data like notes and tags.
|
||||||
|
*/
|
||||||
|
Launched = 11,
|
||||||
|
LocalDataLoaded = 12,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the root key or root key wrapper changes. Includes events like account state
|
||||||
|
* changes (registering, signing in, changing pw, logging out) and passcode state
|
||||||
|
* changes (adding, removing, changing).
|
||||||
|
*/
|
||||||
|
KeyStatusChanged = 13,
|
||||||
|
|
||||||
|
MajorDataChange = 14,
|
||||||
|
CompletedRestart = 15,
|
||||||
|
LocalDataIncrementalLoad = 16,
|
||||||
|
SyncStatusChanged = 17,
|
||||||
|
WillSync = 18,
|
||||||
|
InvalidSyncSession = 19,
|
||||||
|
LocalDatabaseReadError = 20,
|
||||||
|
LocalDatabaseWriteError = 21,
|
||||||
|
|
||||||
|
/** When a single roundtrip completes with sync, in a potentially multi-page sync request.
|
||||||
|
* If just a single roundtrip, this event will be triggered, along with CompletedFullSync */
|
||||||
|
CompletedIncrementalSync = 22,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application has loaded all pending migrations (but not run any, except for the base one),
|
||||||
|
* and consumers may now call `hasPendingMigrations`
|
||||||
|
*/
|
||||||
|
MigrationsLoaded = 23,
|
||||||
|
|
||||||
|
/** When StorageService is ready to start servicing read/write requests */
|
||||||
|
StorageReady = 24,
|
||||||
|
|
||||||
|
PreferencesChanged = 25,
|
||||||
|
UnprotectedSessionBegan = 26,
|
||||||
|
UserRolesChanged = 27,
|
||||||
|
FeaturesUpdated = 28,
|
||||||
|
UnprotectedSessionExpired = 29,
|
||||||
|
/** Called when the app first launches and after first sync request made after sign in */
|
||||||
|
CompletedInitialSync = 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applicationEventForSyncEvent(syncEvent: SyncEvent) {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
[SyncEvent.SyncCompletedWithAllItemsUploaded]: ApplicationEvent.CompletedFullSync,
|
||||||
|
[SyncEvent.SingleRoundTripSyncCompleted]: ApplicationEvent.CompletedIncrementalSync,
|
||||||
|
[SyncEvent.SyncError]: ApplicationEvent.FailedSync,
|
||||||
|
[SyncEvent.SyncTakingTooLong]: ApplicationEvent.HighLatencySync,
|
||||||
|
[SyncEvent.EnterOutOfSync]: ApplicationEvent.EnteredOutOfSync,
|
||||||
|
[SyncEvent.ExitOutOfSync]: ApplicationEvent.ExitedOutOfSync,
|
||||||
|
[SyncEvent.LocalDataLoaded]: ApplicationEvent.LocalDataLoaded,
|
||||||
|
[SyncEvent.MajorDataChange]: ApplicationEvent.MajorDataChange,
|
||||||
|
[SyncEvent.LocalDataIncrementalLoad]: ApplicationEvent.LocalDataIncrementalLoad,
|
||||||
|
[SyncEvent.StatusChanged]: ApplicationEvent.SyncStatusChanged,
|
||||||
|
[SyncEvent.SyncWillBegin]: ApplicationEvent.WillSync,
|
||||||
|
[SyncEvent.InvalidSession]: ApplicationEvent.InvalidSyncSession,
|
||||||
|
[SyncEvent.DatabaseReadError]: ApplicationEvent.LocalDatabaseReadError,
|
||||||
|
[SyncEvent.DatabaseWriteError]: ApplicationEvent.LocalDatabaseWriteError,
|
||||||
|
[SyncEvent.DownloadFirstSyncCompleted]: ApplicationEvent.CompletedInitialSync,
|
||||||
|
} as any
|
||||||
|
)[syncEvent]
|
||||||
|
}
|
||||||
34
packages/snjs/lib/Application/LiveItem.ts
Normal file
34
packages/snjs/lib/Application/LiveItem.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { DecryptedItemInterface } from '@standardnotes/models'
|
||||||
|
import { SNApplication } from './Application'
|
||||||
|
|
||||||
|
/** Keeps an item reference up to date with changes */
|
||||||
|
export class LiveItem<T extends DecryptedItemInterface> {
|
||||||
|
public item: T
|
||||||
|
private removeObserver: () => void
|
||||||
|
|
||||||
|
constructor(uuid: string, application: SNApplication, onChange?: (item: T) => void) {
|
||||||
|
this.item = application.items.findSureItem(uuid)
|
||||||
|
|
||||||
|
onChange && onChange(this.item)
|
||||||
|
|
||||||
|
this.removeObserver = application.streamItems(this.item.content_type, ({ changed, inserted }) => {
|
||||||
|
const matchingItem = [...changed, ...inserted].find((item) => {
|
||||||
|
return item.uuid === uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matchingItem) {
|
||||||
|
this.item = matchingItem as T
|
||||||
|
onChange && onChange(this.item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public deinit() {
|
||||||
|
if (!this.removeObserver) {
|
||||||
|
console.error('A LiveItem is attempting to be deinited more than once.')
|
||||||
|
} else {
|
||||||
|
this.removeObserver()
|
||||||
|
;(this.removeObserver as unknown) = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/snjs/lib/Application/Options/ApplicationOptions.ts
Normal file
16
packages/snjs/lib/Application/Options/ApplicationOptions.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { ApplicationOptionsWhichHaveDefaults } from './Defaults'
|
||||||
|
import {
|
||||||
|
ApplicationDisplayOptions,
|
||||||
|
ApplicationOptionalConfiguratioOptions,
|
||||||
|
ApplicationSyncOptions,
|
||||||
|
} from './OptionalOptions'
|
||||||
|
import { RequiredApplicationOptions } from './RequiredOptions'
|
||||||
|
|
||||||
|
export type ApplicationConstructorOptions = RequiredApplicationOptions &
|
||||||
|
Partial<ApplicationSyncOptions & ApplicationDisplayOptions & ApplicationOptionalConfiguratioOptions>
|
||||||
|
|
||||||
|
export type FullyResolvedApplicationOptions = RequiredApplicationOptions &
|
||||||
|
ApplicationSyncOptions &
|
||||||
|
ApplicationDisplayOptions &
|
||||||
|
ApplicationOptionalConfiguratioOptions &
|
||||||
|
ApplicationOptionsWhichHaveDefaults
|
||||||
11
packages/snjs/lib/Application/Options/Defaults.ts
Normal file
11
packages/snjs/lib/Application/Options/Defaults.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { ApplicationDisplayOptions, ApplicationSyncOptions } from './OptionalOptions'
|
||||||
|
|
||||||
|
export interface ApplicationOptionsWhichHaveDefaults {
|
||||||
|
loadBatchSize: ApplicationSyncOptions['loadBatchSize']
|
||||||
|
supportsFileNavigation: ApplicationDisplayOptions['supportsFileNavigation']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApplicationOptionsDefaults: ApplicationOptionsWhichHaveDefaults = {
|
||||||
|
loadBatchSize: 700,
|
||||||
|
supportsFileNavigation: false,
|
||||||
|
}
|
||||||
25
packages/snjs/lib/Application/Options/OptionalOptions.ts
Normal file
25
packages/snjs/lib/Application/Options/OptionalOptions.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
export interface ApplicationSyncOptions {
|
||||||
|
/**
|
||||||
|
* The size of the item batch to decrypt and render upon application load.
|
||||||
|
*/
|
||||||
|
loadBatchSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDisplayOptions {
|
||||||
|
supportsFileNavigation: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationOptionalConfiguratioOptions {
|
||||||
|
/**
|
||||||
|
* Gives consumers the ability to provide their own custom
|
||||||
|
* subclass for a service. swapClasses should be an array of key/value pairs
|
||||||
|
* consisting of keys 'swap' and 'with'. 'swap' is the base class you wish to replace,
|
||||||
|
* and 'with' is the custom subclass to use.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
swapClasses?: { swap: any; with: any }[]
|
||||||
|
/**
|
||||||
|
* URL for WebSocket providing permissions and roles information.
|
||||||
|
*/
|
||||||
|
webSocketUrl?: string
|
||||||
|
}
|
||||||
42
packages/snjs/lib/Application/Options/RequiredOptions.ts
Normal file
42
packages/snjs/lib/Application/Options/RequiredOptions.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||||
|
import { AlertService, DeviceInterface, Environment, Platform } from '@standardnotes/services'
|
||||||
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
|
|
||||||
|
export interface RequiredApplicationOptions {
|
||||||
|
/**
|
||||||
|
* The Environment that identifies your application.
|
||||||
|
*/
|
||||||
|
environment: Environment
|
||||||
|
/**
|
||||||
|
* The Platform that identifies your application.
|
||||||
|
*/
|
||||||
|
platform: Platform
|
||||||
|
/**
|
||||||
|
* The device interface that provides platform specific
|
||||||
|
* utilities that are used to read/write raw values from/to the database or value storage.
|
||||||
|
*/
|
||||||
|
deviceInterface: DeviceInterface
|
||||||
|
/**
|
||||||
|
* The platform-dependent implementation of SNPureCrypto to use.
|
||||||
|
* Web uses SNWebCrypto, mobile uses SNReactNativeCrypto.
|
||||||
|
*/
|
||||||
|
crypto: PureCryptoInterface
|
||||||
|
/**
|
||||||
|
* The platform-dependent implementation of alert service.
|
||||||
|
*/
|
||||||
|
alertService: AlertService
|
||||||
|
/**
|
||||||
|
* A unique persistent identifier to namespace storage and other
|
||||||
|
* persistent properties. For an ephemeral runtime identifier, use ephemeralIdentifier.
|
||||||
|
*/
|
||||||
|
identifier: ApplicationIdentifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default host to use in ApiService.
|
||||||
|
*/
|
||||||
|
defaultHost: string
|
||||||
|
/**
|
||||||
|
* Version of client application.
|
||||||
|
*/
|
||||||
|
appVersion: string
|
||||||
|
}
|
||||||
55
packages/snjs/lib/Application/Platforms.ts
Normal file
55
packages/snjs/lib/Application/Platforms.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Environment, Platform } from '@standardnotes/services'
|
||||||
|
|
||||||
|
export function platformFromString(string: string) {
|
||||||
|
const map: Record<string, Platform> = {
|
||||||
|
'mac-web': Platform.MacWeb,
|
||||||
|
'mac-desktop': Platform.MacDesktop,
|
||||||
|
'linux-web': Platform.LinuxWeb,
|
||||||
|
'linux-desktop': Platform.LinuxDesktop,
|
||||||
|
'windows-web': Platform.WindowsWeb,
|
||||||
|
'windows-desktop': Platform.WindowsDesktop,
|
||||||
|
ios: Platform.Ios,
|
||||||
|
android: Platform.Android,
|
||||||
|
}
|
||||||
|
return map[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function platformToString(platform: Platform) {
|
||||||
|
const map = {
|
||||||
|
[Platform.MacWeb]: 'mac-web',
|
||||||
|
[Platform.MacDesktop]: 'mac-desktop',
|
||||||
|
[Platform.LinuxWeb]: 'linux-web',
|
||||||
|
[Platform.LinuxDesktop]: 'linux-desktop',
|
||||||
|
[Platform.WindowsWeb]: 'windows-web',
|
||||||
|
[Platform.WindowsDesktop]: 'windows-desktop',
|
||||||
|
[Platform.Ios]: 'ios',
|
||||||
|
[Platform.Android]: 'android',
|
||||||
|
}
|
||||||
|
return map[platform]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function environmentFromString(string: string) {
|
||||||
|
const map: Record<string, Environment> = {
|
||||||
|
web: Environment.Web,
|
||||||
|
desktop: Environment.Desktop,
|
||||||
|
mobile: Environment.Mobile,
|
||||||
|
}
|
||||||
|
return map[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function environmentToString(environment: Environment) {
|
||||||
|
const map = {
|
||||||
|
[Environment.Web]: 'web',
|
||||||
|
[Environment.Desktop]: 'desktop',
|
||||||
|
[Environment.Mobile]: 'mobile',
|
||||||
|
}
|
||||||
|
return map[environment]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEnvironmentWebOrDesktop(environment: Environment) {
|
||||||
|
return environment === Environment.Web || environment === Environment.Desktop
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEnvironmentMobile(environment: Environment) {
|
||||||
|
return environment === Environment.Mobile
|
||||||
|
}
|
||||||
4
packages/snjs/lib/Application/index.ts
Normal file
4
packages/snjs/lib/Application/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './Application'
|
||||||
|
export * from './Event'
|
||||||
|
export * from './LiveItem'
|
||||||
|
export * from './Platforms'
|
||||||
6
packages/snjs/lib/ApplicationGroup/AppGroupCallback.ts
Normal file
6
packages/snjs/lib/ApplicationGroup/AppGroupCallback.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { AppGroupManagedApplication, DeviceInterface } from '@standardnotes/services'
|
||||||
|
import { ApplicationDescriptor } from './ApplicationDescriptor'
|
||||||
|
|
||||||
|
export type AppGroupCallback<D extends DeviceInterface = DeviceInterface> = {
|
||||||
|
applicationCreator: (descriptor: ApplicationDescriptor, deviceInterface: D) => Promise<AppGroupManagedApplication>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||||
|
|
||||||
|
export type ApplicationDescriptor = {
|
||||||
|
identifier: ApplicationIdentifier
|
||||||
|
label: string
|
||||||
|
primary: boolean
|
||||||
|
}
|
||||||
247
packages/snjs/lib/ApplicationGroup/ApplicationGroup.ts
Normal file
247
packages/snjs/lib/ApplicationGroup/ApplicationGroup.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
import {
|
||||||
|
AbstractService,
|
||||||
|
AppGroupManagedApplication,
|
||||||
|
DeinitSource,
|
||||||
|
DeinitCallback,
|
||||||
|
DeviceInterface,
|
||||||
|
DeinitMode,
|
||||||
|
InternalEventBus,
|
||||||
|
InternalEventBusInterface,
|
||||||
|
RawStorageKey,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
import { UuidGenerator } from '@standardnotes/utils'
|
||||||
|
import { AppGroupCallback } from './AppGroupCallback'
|
||||||
|
import { ApplicationGroupEvent, ApplicationGroupEventData } from './ApplicationGroupEvent'
|
||||||
|
import { DescriptorRecord } from './DescriptorRecord'
|
||||||
|
import { ApplicationDescriptor } from './ApplicationDescriptor'
|
||||||
|
|
||||||
|
export class SNApplicationGroup<D extends DeviceInterface = DeviceInterface> extends AbstractService<
|
||||||
|
ApplicationGroupEvent,
|
||||||
|
| ApplicationGroupEventData[ApplicationGroupEvent.PrimaryApplicationSet]
|
||||||
|
| ApplicationGroupEventData[ApplicationGroupEvent.DeviceWillRestart]
|
||||||
|
| ApplicationGroupEventData[ApplicationGroupEvent.DescriptorsDataChanged]
|
||||||
|
> {
|
||||||
|
public primaryApplication!: AppGroupManagedApplication
|
||||||
|
private descriptorRecord!: DescriptorRecord
|
||||||
|
callback!: AppGroupCallback<D>
|
||||||
|
|
||||||
|
constructor(public device: D, internalEventBus?: InternalEventBusInterface) {
|
||||||
|
if (internalEventBus === undefined) {
|
||||||
|
internalEventBus = new InternalEventBus()
|
||||||
|
}
|
||||||
|
|
||||||
|
super(internalEventBus)
|
||||||
|
}
|
||||||
|
|
||||||
|
override deinit() {
|
||||||
|
super.deinit()
|
||||||
|
|
||||||
|
this.device.deinit()
|
||||||
|
;(this.device as unknown) = undefined
|
||||||
|
;(this.callback as unknown) = undefined
|
||||||
|
;(this.primaryApplication as unknown) = undefined
|
||||||
|
;(this.onApplicationDeinit as unknown) = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(callback: AppGroupCallback<D>): Promise<void> {
|
||||||
|
if (this.device.isDeviceDestroyed()) {
|
||||||
|
throw 'Attempting to initialize new application while device is destroyed.'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callback = callback
|
||||||
|
|
||||||
|
this.descriptorRecord = (await this.device.getJsonParsedRawStorageValue(
|
||||||
|
RawStorageKey.DescriptorRecord,
|
||||||
|
)) as DescriptorRecord
|
||||||
|
|
||||||
|
if (!this.descriptorRecord) {
|
||||||
|
await this.createNewDescriptorRecord()
|
||||||
|
}
|
||||||
|
|
||||||
|
let primaryDescriptor = this.findPrimaryDescriptor()
|
||||||
|
if (!primaryDescriptor) {
|
||||||
|
console.error('No primary application descriptor found. Ensure migrations have been run.')
|
||||||
|
primaryDescriptor = this.getDescriptors()[0]
|
||||||
|
|
||||||
|
this.setDescriptorAsPrimary(primaryDescriptor)
|
||||||
|
|
||||||
|
await this.persistDescriptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
const application = await this.buildApplication(primaryDescriptor)
|
||||||
|
|
||||||
|
this.primaryApplication = application
|
||||||
|
|
||||||
|
await this.notifyEvent(ApplicationGroupEvent.PrimaryApplicationSet, { application: application })
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNewDescriptorRecord() {
|
||||||
|
/**
|
||||||
|
* The identifier 'standardnotes' is used because this was the
|
||||||
|
* database name of Standard Notes web/desktop
|
||||||
|
* */
|
||||||
|
const identifier = 'standardnotes'
|
||||||
|
const descriptorRecord: DescriptorRecord = {
|
||||||
|
[identifier]: {
|
||||||
|
identifier: identifier,
|
||||||
|
label: 'Main Workspace',
|
||||||
|
primary: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.device.setRawStorageValue(RawStorageKey.DescriptorRecord, JSON.stringify(descriptorRecord))
|
||||||
|
|
||||||
|
this.descriptorRecord = descriptorRecord
|
||||||
|
|
||||||
|
await this.persistDescriptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDescriptors() {
|
||||||
|
return Object.values(this.descriptorRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
private findPrimaryDescriptor() {
|
||||||
|
for (const descriptor of this.getDescriptors()) {
|
||||||
|
if (descriptor.primary) {
|
||||||
|
return descriptor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async signOutAllWorkspaces() {
|
||||||
|
await this.primaryApplication.user.signOut(false, DeinitSource.SignOutAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
onApplicationDeinit: DeinitCallback = (
|
||||||
|
application: AppGroupManagedApplication,
|
||||||
|
mode: DeinitMode,
|
||||||
|
source: DeinitSource,
|
||||||
|
) => {
|
||||||
|
if (this.primaryApplication === application) {
|
||||||
|
;(this.primaryApplication as unknown) = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const performSyncronously = async () => {
|
||||||
|
if (source === DeinitSource.SignOut) {
|
||||||
|
void this.removeDescriptor(this.descriptorForApplication(application))
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptors = this.getDescriptors()
|
||||||
|
|
||||||
|
if (descriptors.length === 0 || source === DeinitSource.SignOutAll) {
|
||||||
|
const identifiers = descriptors.map((d) => d.identifier)
|
||||||
|
|
||||||
|
this.descriptorRecord = {}
|
||||||
|
|
||||||
|
const { killsApplication } = await this.device.clearAllDataFromDevice(identifiers)
|
||||||
|
|
||||||
|
if (killsApplication) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = this.device
|
||||||
|
|
||||||
|
void this.notifyEvent(ApplicationGroupEvent.DeviceWillRestart, { source, mode })
|
||||||
|
|
||||||
|
this.deinit()
|
||||||
|
|
||||||
|
if (mode === DeinitMode.Hard) {
|
||||||
|
device.performHardReset()
|
||||||
|
} else {
|
||||||
|
device.performSoftReset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void performSyncronously()
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDescriptorAsPrimary(primaryDescriptor: ApplicationDescriptor) {
|
||||||
|
for (const descriptor of this.getDescriptors()) {
|
||||||
|
descriptor.primary = descriptor === primaryDescriptor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistDescriptors() {
|
||||||
|
await this.device.setRawStorageValue(RawStorageKey.DescriptorRecord, JSON.stringify(this.descriptorRecord))
|
||||||
|
|
||||||
|
void this.notifyEvent(ApplicationGroupEvent.DescriptorsDataChanged, { descriptors: this.descriptorRecord })
|
||||||
|
}
|
||||||
|
|
||||||
|
public renameDescriptor(descriptor: ApplicationDescriptor, label: string) {
|
||||||
|
descriptor.label = label
|
||||||
|
|
||||||
|
void this.persistDescriptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeDescriptor(descriptor: ApplicationDescriptor) {
|
||||||
|
delete this.descriptorRecord[descriptor.identifier]
|
||||||
|
|
||||||
|
const descriptors = this.getDescriptors()
|
||||||
|
if (descriptor.primary && descriptors.length > 0) {
|
||||||
|
this.setDescriptorAsPrimary(descriptors[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.persistDescriptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeAllDescriptors() {
|
||||||
|
this.descriptorRecord = {}
|
||||||
|
|
||||||
|
return this.persistDescriptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private descriptorForApplication(application: AppGroupManagedApplication) {
|
||||||
|
return this.descriptorRecord[application.identifier]
|
||||||
|
}
|
||||||
|
|
||||||
|
private createNewApplicationDescriptor(label?: string) {
|
||||||
|
const identifier = UuidGenerator.GenerateUuid()
|
||||||
|
const index = this.getDescriptors().length + 1
|
||||||
|
|
||||||
|
const descriptor: ApplicationDescriptor = {
|
||||||
|
identifier: identifier,
|
||||||
|
label: label || `Workspace ${index}`,
|
||||||
|
primary: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNewPrimaryDescriptor(label?: string): Promise<void> {
|
||||||
|
const descriptor = this.createNewApplicationDescriptor(label)
|
||||||
|
|
||||||
|
this.descriptorRecord[descriptor.identifier] = descriptor
|
||||||
|
|
||||||
|
this.setDescriptorAsPrimary(descriptor)
|
||||||
|
|
||||||
|
await this.persistDescriptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unloadCurrentAndCreateNewDescriptor(label?: string): Promise<void> {
|
||||||
|
await this.createNewPrimaryDescriptor(label)
|
||||||
|
|
||||||
|
if (this.primaryApplication) {
|
||||||
|
this.primaryApplication.deinit(this.primaryApplication.getDeinitMode(), DeinitSource.SwitchWorkspace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unloadCurrentAndActivateDescriptor(descriptor: ApplicationDescriptor) {
|
||||||
|
this.setDescriptorAsPrimary(descriptor)
|
||||||
|
|
||||||
|
await this.persistDescriptors()
|
||||||
|
|
||||||
|
if (this.primaryApplication) {
|
||||||
|
this.primaryApplication.deinit(this.primaryApplication.getDeinitMode(), DeinitSource.SwitchWorkspace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildApplication(descriptor: ApplicationDescriptor) {
|
||||||
|
const application = await this.callback.applicationCreator(descriptor, this.device)
|
||||||
|
|
||||||
|
application.setOnDeinit(this.onApplicationDeinit)
|
||||||
|
|
||||||
|
return application
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/snjs/lib/ApplicationGroup/ApplicationGroupEvent.ts
Normal file
21
packages/snjs/lib/ApplicationGroup/ApplicationGroupEvent.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ApplicationInterface, DeinitMode, DeinitSource } from '@standardnotes/services'
|
||||||
|
import { DescriptorRecord } from './DescriptorRecord'
|
||||||
|
|
||||||
|
export enum ApplicationGroupEvent {
|
||||||
|
PrimaryApplicationSet = 'PrimaryApplicationSet',
|
||||||
|
DescriptorsDataChanged = 'DescriptorsDataChanged',
|
||||||
|
DeviceWillRestart = 'DeviceWillRestart',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationGroupEventData {
|
||||||
|
[ApplicationGroupEvent.PrimaryApplicationSet]: {
|
||||||
|
application: ApplicationInterface
|
||||||
|
}
|
||||||
|
[ApplicationGroupEvent.DeviceWillRestart]: {
|
||||||
|
source: DeinitSource
|
||||||
|
mode: DeinitMode
|
||||||
|
}
|
||||||
|
[ApplicationGroupEvent.DescriptorsDataChanged]: {
|
||||||
|
descriptors: DescriptorRecord
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/snjs/lib/ApplicationGroup/DeinitCallback.ts
Normal file
3
packages/snjs/lib/ApplicationGroup/DeinitCallback.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { AppGroupManagedApplication, DeinitSource, DeinitMode } from '@standardnotes/services'
|
||||||
|
|
||||||
|
export type DeinitCallback = (application: AppGroupManagedApplication, mode: DeinitMode, source: DeinitSource) => void
|
||||||
3
packages/snjs/lib/ApplicationGroup/DescriptorRecord.ts
Normal file
3
packages/snjs/lib/ApplicationGroup/DescriptorRecord.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { ApplicationDescriptor } from './ApplicationDescriptor'
|
||||||
|
|
||||||
|
export type DescriptorRecord = Record<string, ApplicationDescriptor>
|
||||||
5
packages/snjs/lib/ApplicationGroup/index.ts
Normal file
5
packages/snjs/lib/ApplicationGroup/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './AppGroupCallback'
|
||||||
|
export * from './ApplicationDescriptor'
|
||||||
|
export * from './ApplicationGroup'
|
||||||
|
export * from './ApplicationGroupEvent'
|
||||||
|
export * from './DescriptorRecord'
|
||||||
41
packages/snjs/lib/Client/FileViewController.ts
Normal file
41
packages/snjs/lib/Client/FileViewController.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { FileItem } from '@standardnotes/models'
|
||||||
|
import { ContentType } from '@standardnotes/common'
|
||||||
|
import { SNApplication } from '../Application/Application'
|
||||||
|
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
||||||
|
|
||||||
|
export class FileViewController implements ItemViewControllerInterface {
|
||||||
|
public dealloced = false
|
||||||
|
private removeStreamObserver?: () => void
|
||||||
|
|
||||||
|
constructor(private application: SNApplication, public item: FileItem) {}
|
||||||
|
|
||||||
|
deinit() {
|
||||||
|
this.dealloced = true
|
||||||
|
this.removeStreamObserver?.()
|
||||||
|
;(this.removeStreamObserver as unknown) = undefined
|
||||||
|
;(this.application as unknown) = undefined
|
||||||
|
;(this.item as unknown) = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.streamItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private streamItems() {
|
||||||
|
this.removeStreamObserver = this.application.streamItems<FileItem>(ContentType.File, ({ changed, inserted }) => {
|
||||||
|
if (this.dealloced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = changed.concat(inserted)
|
||||||
|
|
||||||
|
const matchingFile = files.find((item) => {
|
||||||
|
return item.uuid === this.item.uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matchingFile) {
|
||||||
|
this.item = matchingFile
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
69
packages/snjs/lib/Client/IconsController.spec.ts
Normal file
69
packages/snjs/lib/Client/IconsController.spec.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { IconsController } from './IconsController'
|
||||||
|
|
||||||
|
describe('IconsController', () => {
|
||||||
|
let iconsController: IconsController
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
iconsController = new IconsController()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getIconForFileType', () => {
|
||||||
|
it('should return correct icon type for supported mimetypes', () => {
|
||||||
|
const iconTypeForPdf = iconsController.getIconForFileType('application/pdf')
|
||||||
|
expect(iconTypeForPdf).toBe('file-pdf')
|
||||||
|
|
||||||
|
const iconTypeForDoc = iconsController.getIconForFileType('application/msword')
|
||||||
|
const iconTypeForDocx = iconsController.getIconForFileType(
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
)
|
||||||
|
expect(iconTypeForDoc).toBe('file-doc')
|
||||||
|
expect(iconTypeForDocx).toBe('file-doc')
|
||||||
|
|
||||||
|
const iconTypeForPpt = iconsController.getIconForFileType('application/vnd.ms-powerpoint')
|
||||||
|
const iconTypeForPptx = iconsController.getIconForFileType(
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
)
|
||||||
|
expect(iconTypeForPpt).toBe('file-ppt')
|
||||||
|
expect(iconTypeForPptx).toBe('file-ppt')
|
||||||
|
|
||||||
|
const iconTypeForXls = iconsController.getIconForFileType('application/vnd.ms-excel')
|
||||||
|
const iconTypeForXlsx = iconsController.getIconForFileType(
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.spreadsheet',
|
||||||
|
)
|
||||||
|
expect(iconTypeForXls).toBe('file-xls')
|
||||||
|
expect(iconTypeForXlsx).toBe('file-xls')
|
||||||
|
|
||||||
|
const iconTypeForJpg = iconsController.getIconForFileType('image/jpeg')
|
||||||
|
const iconTypeForPng = iconsController.getIconForFileType('image/png')
|
||||||
|
expect(iconTypeForJpg).toBe('file-image')
|
||||||
|
expect(iconTypeForPng).toBe('file-image')
|
||||||
|
|
||||||
|
const iconTypeForMpeg = iconsController.getIconForFileType('video/mpeg')
|
||||||
|
const iconTypeForMp4 = iconsController.getIconForFileType('video/mp4')
|
||||||
|
expect(iconTypeForMpeg).toBe('file-mov')
|
||||||
|
expect(iconTypeForMp4).toBe('file-mov')
|
||||||
|
|
||||||
|
const iconTypeForWav = iconsController.getIconForFileType('audio/wav')
|
||||||
|
const iconTypeForMp3 = iconsController.getIconForFileType('audio/mp3')
|
||||||
|
expect(iconTypeForWav).toBe('file-music')
|
||||||
|
expect(iconTypeForMp3).toBe('file-music')
|
||||||
|
|
||||||
|
const iconTypeForZip = iconsController.getIconForFileType('application/zip')
|
||||||
|
const iconTypeForRar = iconsController.getIconForFileType('application/vnd.rar')
|
||||||
|
const iconTypeForTar = iconsController.getIconForFileType('application/x-tar')
|
||||||
|
const iconTypeFor7z = iconsController.getIconForFileType('application/x-7z-compressed')
|
||||||
|
expect(iconTypeForZip).toBe('file-zip')
|
||||||
|
expect(iconTypeForRar).toBe('file-zip')
|
||||||
|
expect(iconTypeForTar).toBe('file-zip')
|
||||||
|
expect(iconTypeFor7z).toBe('file-zip')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return fallback icon type for unsupported mimetypes', () => {
|
||||||
|
const iconForBin = iconsController.getIconForFileType('application/octet-stream')
|
||||||
|
expect(iconForBin).toBe('file-other')
|
||||||
|
|
||||||
|
const iconForNoType = iconsController.getIconForFileType('')
|
||||||
|
expect(iconForNoType).toBe('file-other')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
61
packages/snjs/lib/Client/IconsController.ts
Normal file
61
packages/snjs/lib/Client/IconsController.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { NoteType } from '@standardnotes/features'
|
||||||
|
import { IconType } from '@Lib/Types/IconType'
|
||||||
|
|
||||||
|
export class IconsController {
|
||||||
|
getIconForFileType(type: string): IconType {
|
||||||
|
let iconType: IconType = 'file-other'
|
||||||
|
|
||||||
|
if (type === 'application/pdf') {
|
||||||
|
iconType = 'file-pdf'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/word/.test(type)) {
|
||||||
|
iconType = 'file-doc'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/powerpoint|presentation/.test(type)) {
|
||||||
|
iconType = 'file-ppt'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/excel|spreadsheet/.test(type)) {
|
||||||
|
iconType = 'file-xls'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^image\//.test(type)) {
|
||||||
|
iconType = 'file-image'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^video\//.test(type)) {
|
||||||
|
iconType = 'file-mov'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^audio\//.test(type)) {
|
||||||
|
iconType = 'file-music'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/(zip)|([tr]ar)|(7z)/.test(type)) {
|
||||||
|
iconType = 'file-zip'
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconType
|
||||||
|
}
|
||||||
|
|
||||||
|
getIconAndTintForNoteType(noteType?: NoteType): [IconType, number] {
|
||||||
|
switch (noteType) {
|
||||||
|
case NoteType.RichText:
|
||||||
|
return ['rich-text', 1]
|
||||||
|
case NoteType.Markdown:
|
||||||
|
return ['markdown', 2]
|
||||||
|
case NoteType.Authentication:
|
||||||
|
return ['authenticator', 6]
|
||||||
|
case NoteType.Spreadsheet:
|
||||||
|
return ['spreadsheets', 5]
|
||||||
|
case NoteType.Task:
|
||||||
|
return ['tasks', 3]
|
||||||
|
case NoteType.Code:
|
||||||
|
return ['code', 4]
|
||||||
|
default:
|
||||||
|
return ['plain-text', 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
packages/snjs/lib/Client/ItemGroupController.ts
Normal file
125
packages/snjs/lib/Client/ItemGroupController.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { ApplicationEvent } from '../Application/Event'
|
||||||
|
import { FileItem, PrefKey, SNNote } from '@standardnotes/models'
|
||||||
|
import { removeFromArray } from '@standardnotes/utils'
|
||||||
|
import { SNApplication } from '../Application/Application'
|
||||||
|
import { NoteViewController } from './NoteViewController'
|
||||||
|
import { FileViewController } from './FileViewController'
|
||||||
|
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
||||||
|
|
||||||
|
type ItemControllerGroupChangeCallback = (activeController: NoteViewController | FileViewController | undefined) => void
|
||||||
|
|
||||||
|
type CreateItemControllerOptions = FileItem | SNNote | TemplateNoteViewControllerOptions
|
||||||
|
|
||||||
|
export class ItemGroupController {
|
||||||
|
public itemControllers: (NoteViewController | FileViewController)[] = []
|
||||||
|
private addTagHierarchy: boolean
|
||||||
|
changeObservers: ItemControllerGroupChangeCallback[] = []
|
||||||
|
eventObservers: (() => void)[] = []
|
||||||
|
|
||||||
|
constructor(private application: SNApplication) {
|
||||||
|
this.addTagHierarchy = application.getPreference(PrefKey.NoteAddToParentFolders, true)
|
||||||
|
|
||||||
|
this.eventObservers.push(
|
||||||
|
application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
||||||
|
this.addTagHierarchy = application.getPreference(PrefKey.NoteAddToParentFolders, true)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public deinit(): void {
|
||||||
|
;(this.application as unknown) = undefined
|
||||||
|
|
||||||
|
this.eventObservers.forEach((removeObserver) => {
|
||||||
|
removeObserver()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.changeObservers.length = 0
|
||||||
|
|
||||||
|
for (const controller of this.itemControllers) {
|
||||||
|
this.closeItemController(controller, { notify: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.itemControllers.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async createItemController(options: CreateItemControllerOptions): Promise<NoteViewController | FileViewController> {
|
||||||
|
if (this.activeItemViewController) {
|
||||||
|
this.closeItemController(this.activeItemViewController, { notify: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller!: NoteViewController | FileViewController
|
||||||
|
|
||||||
|
if (options instanceof FileItem) {
|
||||||
|
const file = options
|
||||||
|
controller = new FileViewController(this.application, file)
|
||||||
|
} else if (options instanceof SNNote) {
|
||||||
|
const note = options
|
||||||
|
controller = new NoteViewController(this.application, note)
|
||||||
|
} else {
|
||||||
|
controller = new NoteViewController(this.application, undefined, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.itemControllers.push(controller)
|
||||||
|
|
||||||
|
await controller.initialize(this.addTagHierarchy)
|
||||||
|
|
||||||
|
this.notifyObservers()
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeItemController(
|
||||||
|
controller: NoteViewController | FileViewController,
|
||||||
|
{ notify = true }: { notify: boolean } = { notify: true },
|
||||||
|
): void {
|
||||||
|
controller.deinit()
|
||||||
|
|
||||||
|
removeFromArray(this.itemControllers, controller)
|
||||||
|
|
||||||
|
if (notify) {
|
||||||
|
this.notifyObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeActiveItemController(): void {
|
||||||
|
const activeController = this.activeItemViewController
|
||||||
|
|
||||||
|
if (activeController) {
|
||||||
|
this.closeItemController(activeController, { notify: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllItemControllers(): void {
|
||||||
|
for (const controller of this.itemControllers) {
|
||||||
|
this.closeItemController(controller, { notify: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeItemViewController(): NoteViewController | FileViewController | undefined {
|
||||||
|
return this.itemControllers[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies observer when the active controller has changed.
|
||||||
|
*/
|
||||||
|
public addActiveControllerChangeObserver(callback: ItemControllerGroupChangeCallback): () => void {
|
||||||
|
this.changeObservers.push(callback)
|
||||||
|
|
||||||
|
if (this.activeItemViewController) {
|
||||||
|
callback(this.activeItemViewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
const thislessChangeObservers = this.changeObservers
|
||||||
|
return () => {
|
||||||
|
removeFromArray(thislessChangeObservers, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyObservers(): void {
|
||||||
|
for (const observer of this.changeObservers) {
|
||||||
|
observer(this.activeItemViewController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/snjs/lib/Client/ItemViewControllerInterface.ts
Normal file
8
packages/snjs/lib/Client/ItemViewControllerInterface.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { SNNote, FileItem } from '@standardnotes/models'
|
||||||
|
|
||||||
|
export interface ItemViewControllerInterface {
|
||||||
|
item: SNNote | FileItem
|
||||||
|
|
||||||
|
deinit: () => void
|
||||||
|
initialize(addTagHierarchy?: boolean): Promise<void>
|
||||||
|
}
|
||||||
208
packages/snjs/lib/Client/NoteViewController.ts
Normal file
208
packages/snjs/lib/Client/NoteViewController.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
import {
|
||||||
|
NoteMutator,
|
||||||
|
SNNote,
|
||||||
|
SNTag,
|
||||||
|
NoteContent,
|
||||||
|
DecryptedItemInterface,
|
||||||
|
PayloadEmitSource,
|
||||||
|
} from '@standardnotes/models'
|
||||||
|
import { removeFromArray } from '@standardnotes/utils'
|
||||||
|
import { ContentType } from '@standardnotes/common'
|
||||||
|
import { UuidString } from '@Lib/Types/UuidString'
|
||||||
|
import { SNApplication } from '../Application/Application'
|
||||||
|
import {
|
||||||
|
STRING_SAVING_WHILE_DOCUMENT_HIDDEN,
|
||||||
|
STRING_INVALID_NOTE,
|
||||||
|
NOTE_PREVIEW_CHAR_LIMIT,
|
||||||
|
STRING_ELLIPSES,
|
||||||
|
SAVE_TIMEOUT_NO_DEBOUNCE,
|
||||||
|
SAVE_TIMEOUT_DEBOUNCE,
|
||||||
|
} from './Types'
|
||||||
|
import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
||||||
|
import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions'
|
||||||
|
|
||||||
|
export type EditorValues = {
|
||||||
|
title: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NoteViewController implements ItemViewControllerInterface {
|
||||||
|
public item!: SNNote
|
||||||
|
public dealloced = false
|
||||||
|
private innerValueChangeObservers: ((note: SNNote, source: PayloadEmitSource) => void)[] = []
|
||||||
|
private removeStreamObserver?: () => void
|
||||||
|
public isTemplateNote = false
|
||||||
|
private saveTimeout?: ReturnType<typeof setTimeout>
|
||||||
|
private defaultTitle: string | undefined
|
||||||
|
private defaultTag: UuidString | undefined
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private application: SNApplication,
|
||||||
|
item?: SNNote,
|
||||||
|
templateNoteOptions?: TemplateNoteViewControllerOptions,
|
||||||
|
) {
|
||||||
|
if (item) {
|
||||||
|
this.item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateNoteOptions) {
|
||||||
|
this.defaultTitle = templateNoteOptions.title
|
||||||
|
this.defaultTag = templateNoteOptions.tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit(): void {
|
||||||
|
this.dealloced = true
|
||||||
|
this.removeStreamObserver?.()
|
||||||
|
;(this.removeStreamObserver as unknown) = undefined
|
||||||
|
;(this.application as unknown) = undefined
|
||||||
|
;(this.item as unknown) = undefined
|
||||||
|
|
||||||
|
this.innerValueChangeObservers.length = 0
|
||||||
|
|
||||||
|
this.saveTimeout = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(addTagHierarchy: boolean): Promise<void> {
|
||||||
|
if (!this.item) {
|
||||||
|
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(ContentType.Note, {
|
||||||
|
text: '',
|
||||||
|
title: this.defaultTitle || '',
|
||||||
|
references: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isTemplateNote = true
|
||||||
|
this.item = note
|
||||||
|
|
||||||
|
if (this.defaultTag) {
|
||||||
|
const tag = this.application.items.findItem(this.defaultTag) as SNTag
|
||||||
|
await this.application.items.addTagToNote(note, tag, addTagHierarchy)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyObservers(this.item, PayloadEmitSource.InitialObserverRegistrationPush)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.streamItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyObservers(note: SNNote, source: PayloadEmitSource): void {
|
||||||
|
for (const observer of this.innerValueChangeObservers) {
|
||||||
|
observer(note, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private streamItems() {
|
||||||
|
this.removeStreamObserver = this.application.streamItems<SNNote>(
|
||||||
|
ContentType.Note,
|
||||||
|
({ changed, inserted, source }) => {
|
||||||
|
if (this.dealloced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const notes = changed.concat(inserted)
|
||||||
|
|
||||||
|
const matchingNote = notes.find((item) => {
|
||||||
|
return item.uuid === this.item.uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matchingNote) {
|
||||||
|
this.isTemplateNote = false
|
||||||
|
this.item = matchingNote
|
||||||
|
this.notifyObservers(matchingNote, source)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public insertTemplatedNote(): Promise<DecryptedItemInterface> {
|
||||||
|
this.isTemplateNote = false
|
||||||
|
return this.application.mutator.insertItem(this.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register to be notified when the controller's note's inner values change
|
||||||
|
* (and thus a new object reference is created)
|
||||||
|
*/
|
||||||
|
public addNoteInnerValueChangeObserver(callback: (note: SNNote, source: PayloadEmitSource) => void): () => void {
|
||||||
|
this.innerValueChangeObservers.push(callback)
|
||||||
|
|
||||||
|
if (this.item) {
|
||||||
|
callback(this.item, PayloadEmitSource.InitialObserverRegistrationPush)
|
||||||
|
}
|
||||||
|
|
||||||
|
const thislessChangeObservers = this.innerValueChangeObservers
|
||||||
|
return () => {
|
||||||
|
removeFromArray(thislessChangeObservers, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bypassDebouncer Calling save will debounce by default. You can pass true to save
|
||||||
|
* immediately.
|
||||||
|
* @param isUserModified This field determines if the item will be saved as a user
|
||||||
|
* modification, thus updating the user modified date displayed in the UI
|
||||||
|
* @param dontUpdatePreviews Whether this change should update the note's plain and HTML
|
||||||
|
* preview.
|
||||||
|
* @param customMutate A custom mutator function.
|
||||||
|
*/
|
||||||
|
public async save(dto: {
|
||||||
|
editorValues: EditorValues
|
||||||
|
bypassDebouncer?: boolean
|
||||||
|
isUserModified?: boolean
|
||||||
|
dontUpdatePreviews?: boolean
|
||||||
|
customMutate?: (mutator: NoteMutator) => void
|
||||||
|
}): Promise<void> {
|
||||||
|
const title = dto.editorValues.title
|
||||||
|
const text = dto.editorValues.text
|
||||||
|
const isTemplate = this.isTemplateNote
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined' && document.hidden) {
|
||||||
|
void this.application.alertService.alert(STRING_SAVING_WHILE_DOCUMENT_HIDDEN)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTemplate) {
|
||||||
|
await this.insertTemplatedNote()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.application.items.findItem(this.item.uuid)) {
|
||||||
|
void this.application.alertService.alert(STRING_INVALID_NOTE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.application.mutator.changeItem(
|
||||||
|
this.item,
|
||||||
|
(mutator) => {
|
||||||
|
const noteMutator = mutator as NoteMutator
|
||||||
|
if (dto.customMutate) {
|
||||||
|
dto.customMutate(noteMutator)
|
||||||
|
}
|
||||||
|
noteMutator.title = title
|
||||||
|
noteMutator.text = text
|
||||||
|
|
||||||
|
if (!dto.dontUpdatePreviews) {
|
||||||
|
const noteText = text || ''
|
||||||
|
const truncate = noteText.length > NOTE_PREVIEW_CHAR_LIMIT
|
||||||
|
const substring = noteText.substring(0, NOTE_PREVIEW_CHAR_LIMIT)
|
||||||
|
const previewPlain = substring + (truncate ? STRING_ELLIPSES : '')
|
||||||
|
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
noteMutator.preview_plain = previewPlain
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
noteMutator.preview_html = undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dto.isUserModified,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (this.saveTimeout) {
|
||||||
|
clearTimeout(this.saveTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const noDebounce = dto.bypassDebouncer || this.application.noAccount()
|
||||||
|
const syncDebouceMs = noDebounce ? SAVE_TIMEOUT_NO_DEBOUNCE : SAVE_TIMEOUT_DEBOUNCE
|
||||||
|
this.saveTimeout = setTimeout(() => {
|
||||||
|
void this.application.sync.sync()
|
||||||
|
}, syncDebouceMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { UuidString } from '@Lib/Types/UuidString'
|
||||||
|
|
||||||
|
export type TemplateNoteViewControllerOptions = {
|
||||||
|
title?: string
|
||||||
|
tag?: UuidString
|
||||||
|
}
|
||||||
8
packages/snjs/lib/Client/Types.ts
Normal file
8
packages/snjs/lib/Client/Types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN =
|
||||||
|
'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.'
|
||||||
|
export const STRING_INVALID_NOTE =
|
||||||
|
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."
|
||||||
|
export const STRING_ELLIPSES = '...'
|
||||||
|
export const NOTE_PREVIEW_CHAR_LIMIT = 80
|
||||||
|
export const SAVE_TIMEOUT_DEBOUNCE = 350
|
||||||
|
export const SAVE_TIMEOUT_NO_DEBOUNCE = 100
|
||||||
4
packages/snjs/lib/Client/index.ts
Normal file
4
packages/snjs/lib/Client/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './IconsController'
|
||||||
|
export * from './NoteViewController'
|
||||||
|
export * from './FileViewController'
|
||||||
|
export * from './ItemGroupController'
|
||||||
40
packages/snjs/lib/Hosts.ts
Normal file
40
packages/snjs/lib/Hosts.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
export const APPLICATION_DEFAULT_HOSTS = [
|
||||||
|
'api.standardnotes.com',
|
||||||
|
'api-dev.standardnotes.com',
|
||||||
|
'sync.standardnotes.org',
|
||||||
|
'syncing-server-demo.standardnotes.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com', 'files-dev.standardnotes.com']
|
||||||
|
|
||||||
|
export const TRUSTED_FEATURE_HOSTS = [
|
||||||
|
'api-dev.standardnotes.com',
|
||||||
|
'api.standardnotes.com',
|
||||||
|
'extensions.standardnotes.com',
|
||||||
|
'extensions.standardnotes.org',
|
||||||
|
'extensions-server-dev.standardnotes.org',
|
||||||
|
'extensions-server-dev.standardnotes.com',
|
||||||
|
'features.standardnotes.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
export enum ExtensionsServerURL {
|
||||||
|
Dev = 'https://extensions-server-dev.standardnotes.org',
|
||||||
|
Prod = 'https://extensions.standardnotes.org',
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocalHost = 'localhost'
|
||||||
|
|
||||||
|
export function isUrlFirstParty(url: string): boolean {
|
||||||
|
try {
|
||||||
|
const { host } = new URL(url)
|
||||||
|
return host.startsWith(LocalHost) || APPLICATION_DEFAULT_HOSTS.includes(host) || FILES_DEFAULT_HOSTS.includes(host)
|
||||||
|
} catch (_err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PROD_OFFLINE_FEATURES_URL = 'https://api.standardnotes.com/v1/offline/features'
|
||||||
|
|
||||||
|
export const LEGACY_PROD_EXT_ORIGIN = 'https://extensions.standardnotes.org'
|
||||||
|
|
||||||
|
export const TRUSTED_CUSTOM_EXTENSIONS_HOSTS = ['listed.to']
|
||||||
13
packages/snjs/lib/Log.ts
Normal file
13
packages/snjs/lib/Log.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
export class SNLog {
|
||||||
|
static log(...message: any): void {
|
||||||
|
this.onLog(...message)
|
||||||
|
}
|
||||||
|
static error<T extends Error>(error: T): T {
|
||||||
|
this.onError(error)
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
static onLog: (...message: any) => void
|
||||||
|
static onError: (error: Error) => void
|
||||||
|
}
|
||||||
124
packages/snjs/lib/Migrations/Applicators/TagsToFolders.spec.ts
Normal file
124
packages/snjs/lib/Migrations/Applicators/TagsToFolders.spec.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { ItemManager } from '@Lib/Services'
|
||||||
|
import { TagsToFoldersMigrationApplicator } from './TagsToFolders'
|
||||||
|
|
||||||
|
const itemManagerMock = (tagTitles: string[]) => {
|
||||||
|
const mockTag = (title: string) => ({
|
||||||
|
title,
|
||||||
|
uuid: title,
|
||||||
|
parentId: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mock = {
|
||||||
|
getItems: jest.fn().mockReturnValue(tagTitles.map(mockTag)),
|
||||||
|
findOrCreateTagParentChain: jest.fn(),
|
||||||
|
changeItem: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('folders component to hierarchy', () => {
|
||||||
|
it('should produce a valid hierarchy in the simple case', async () => {
|
||||||
|
const titles = ['a', 'a.b', 'a.b.c']
|
||||||
|
|
||||||
|
const itemManager = itemManagerMock(titles)
|
||||||
|
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||||
|
|
||||||
|
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||||
|
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||||
|
|
||||||
|
expect(findOrCreateTagParentChainCalls.length).toEqual(2)
|
||||||
|
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a'])
|
||||||
|
expect(findOrCreateTagParentChainCalls[1][0]).toEqual(['a', 'b'])
|
||||||
|
|
||||||
|
expect(changeItemCalls.length).toEqual(2)
|
||||||
|
expect(changeItemCalls[0][0].uuid).toEqual('a.b')
|
||||||
|
expect(changeItemCalls[1][0].uuid).toEqual('a.b.c')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not touch flat hierarchies', async () => {
|
||||||
|
const titles = ['a', 'x', 'y', 'z']
|
||||||
|
|
||||||
|
const itemManager = itemManagerMock(titles)
|
||||||
|
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||||
|
|
||||||
|
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||||
|
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||||
|
|
||||||
|
expect(findOrCreateTagParentChainCalls.length).toEqual(0)
|
||||||
|
|
||||||
|
expect(changeItemCalls.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work despite cloned tags', async () => {
|
||||||
|
const titles = ['a.b', 'c', 'a.b']
|
||||||
|
|
||||||
|
const itemManager = itemManagerMock(titles)
|
||||||
|
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||||
|
|
||||||
|
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||||
|
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||||
|
|
||||||
|
expect(findOrCreateTagParentChainCalls.length).toEqual(2)
|
||||||
|
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a'])
|
||||||
|
expect(findOrCreateTagParentChainCalls[1][0]).toEqual(['a'])
|
||||||
|
|
||||||
|
expect(changeItemCalls.length).toEqual(2)
|
||||||
|
expect(changeItemCalls[0][0].uuid).toEqual('a.b')
|
||||||
|
expect(changeItemCalls[0][0].uuid).toEqual('a.b')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should produce a valid hierarchy cases with missing intermediate tags or unordered', async () => {
|
||||||
|
const titles = ['y.2', 'w.3', 'y']
|
||||||
|
|
||||||
|
const itemManager = itemManagerMock(titles)
|
||||||
|
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||||
|
|
||||||
|
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||||
|
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||||
|
|
||||||
|
expect(findOrCreateTagParentChainCalls.length).toEqual(2)
|
||||||
|
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['w'])
|
||||||
|
expect(findOrCreateTagParentChainCalls[1][0]).toEqual(['y'])
|
||||||
|
|
||||||
|
expect(changeItemCalls.length).toEqual(2)
|
||||||
|
expect(changeItemCalls[0][0].uuid).toEqual('w.3')
|
||||||
|
expect(changeItemCalls[1][0].uuid).toEqual('y.2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skip prefixed names', async () => {
|
||||||
|
const titles = ['.something', '.something...something']
|
||||||
|
|
||||||
|
const itemManager = itemManagerMock(titles)
|
||||||
|
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||||
|
|
||||||
|
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||||
|
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||||
|
|
||||||
|
expect(findOrCreateTagParentChainCalls.length).toEqual(0)
|
||||||
|
expect(changeItemCalls.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skip not-supported names', async () => {
|
||||||
|
const titles = [
|
||||||
|
'something.',
|
||||||
|
'something..',
|
||||||
|
'something..another.thing',
|
||||||
|
'a.b.c',
|
||||||
|
'a',
|
||||||
|
'something..another.thing..anyway',
|
||||||
|
]
|
||||||
|
|
||||||
|
const itemManager = itemManagerMock(titles)
|
||||||
|
await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager)
|
||||||
|
|
||||||
|
const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls
|
||||||
|
const changeItemCalls = itemManager.changeItem.mock.calls
|
||||||
|
|
||||||
|
expect(findOrCreateTagParentChainCalls.length).toEqual(1)
|
||||||
|
expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a', 'b'])
|
||||||
|
|
||||||
|
expect(changeItemCalls.length).toEqual(1)
|
||||||
|
expect(changeItemCalls[0][0].uuid).toEqual('a.b.c')
|
||||||
|
})
|
||||||
|
})
|
||||||
50
packages/snjs/lib/Migrations/Applicators/TagsToFolders.ts
Normal file
50
packages/snjs/lib/Migrations/Applicators/TagsToFolders.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { SNTag, TagMutator, TagFolderDelimitter } from '@standardnotes/models'
|
||||||
|
import { ItemManager } from '@Lib/Services'
|
||||||
|
import { lastElement, sortByKey, withoutLastElement } from '@standardnotes/utils'
|
||||||
|
import { ContentType } from '@standardnotes/common'
|
||||||
|
|
||||||
|
export class TagsToFoldersMigrationApplicator {
|
||||||
|
public static isApplicableToCurrentData(itemManager: ItemManager): boolean {
|
||||||
|
const tags = itemManager.getItems<SNTag>(ContentType.Tag)
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (tag.title.includes(TagFolderDelimitter) && !tag.parentId) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async run(itemManager: ItemManager): Promise<void> {
|
||||||
|
const tags = itemManager.getItems(ContentType.Tag) as SNTag[]
|
||||||
|
const sortedTags = sortByKey(tags, 'title')
|
||||||
|
|
||||||
|
for (const tag of sortedTags) {
|
||||||
|
const hierarchy = tag.title.split(TagFolderDelimitter)
|
||||||
|
const hasSimpleTitle = hierarchy.length === 1
|
||||||
|
const hasParent = !!tag.parentId
|
||||||
|
const hasUnsupportedTitle = hierarchy.some((title) => title.length === 0)
|
||||||
|
|
||||||
|
if (hasParent || hasSimpleTitle || hasUnsupportedTitle) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parents = withoutLastElement(hierarchy)
|
||||||
|
const newTitle = lastElement(hierarchy)
|
||||||
|
|
||||||
|
if (!newTitle) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = await itemManager.findOrCreateTagParentChain(parents)
|
||||||
|
|
||||||
|
await itemManager.changeItem(tag, (mutator: TagMutator) => {
|
||||||
|
mutator.title = newTitle
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
mutator.makeChildOf(parent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue