mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-22 18:30:09 +00:00
Merge branch 'master' into feat/operation-logs
Resolve merge conflicts: - package.json: use @types/node@^22.19.5 - sync-form.const.ts: keep eslint-disable for naming convention - global-config.effects.ts: remove extra blank line - unlink-all-tasks-on-provider-deletion.effects.ts: keep deprecation notice - auto-fix-typia-errors.ts: keep better-typed getValueByPath - undo-task-delete.meta-reducer.spec.ts: keep getLastDeletePayload import - Accept deletions for files refactored into operation-logs architecture
This commit is contained in:
commit
ce91c8b1dc
104 changed files with 6881 additions and 4596 deletions
|
|
@ -1 +0,0 @@
|
||||||
docs/**/*.md
|
|
||||||
142
.eslintrc.json
142
.eslintrc.json
|
|
@ -1,142 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"ignorePatterns": [
|
|
||||||
"app-builds/**/*",
|
|
||||||
"dist/**",
|
|
||||||
"node_modules/**/*",
|
|
||||||
"src/app/t.const.ts",
|
|
||||||
"src/assets/bundled-plugins/**/*",
|
|
||||||
"src/app/config/env.generated.ts"
|
|
||||||
],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"excludedFiles": [],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2020,
|
|
||||||
"sourceType": "module",
|
|
||||||
"createDefaultProgram": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@angular-eslint/recommended",
|
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
|
||||||
"plugins": ["@typescript-eslint", "prettier", "prefer-arrow", "local-rules"],
|
|
||||||
"rules": {
|
|
||||||
"local-rules/require-hydration-guard": "error",
|
|
||||||
"local-rules/require-entity-registry": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
|
||||||
"@typescript-eslint/ban-ts-comment": 0,
|
|
||||||
"@typescript-eslint/no-empty-function": 0,
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"@angular-eslint/component-selector": ["off"],
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"args": "none"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"allowExpressions": true,
|
|
||||||
"allowTypedFunctionExpressions": true,
|
|
||||||
"allowHigherOrderFunctions": true,
|
|
||||||
"allowDirectConstAssertionInArrowFunctions": true,
|
|
||||||
"allowConciseArrowFunctionExpressionsStartingWithVoid": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": [
|
|
||||||
"off",
|
|
||||||
{
|
|
||||||
"accessibility": "explicit"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"arrow-parens": ["off", "always"],
|
|
||||||
"import/order": "off",
|
|
||||||
"@typescript-eslint/member-ordering": "off",
|
|
||||||
"@typescript-eslint/no-inferrable-types": "off",
|
|
||||||
"no-underscore-dangle": "off",
|
|
||||||
"arrow-body-style": "off",
|
|
||||||
"@angular-eslint/no-input-rename": "off",
|
|
||||||
"@typescript-eslint/naming-convention": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"selector": "default",
|
|
||||||
"format": ["camelCase", "snake_case", "UPPER_CASE", "PascalCase"],
|
|
||||||
"leadingUnderscore": "allowSingleOrDouble",
|
|
||||||
"trailingUnderscore": "allow",
|
|
||||||
"filter": {
|
|
||||||
"regex": "(should)|@tags",
|
|
||||||
"match": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "variable",
|
|
||||||
"format": ["camelCase", "snake_case", "UPPER_CASE", "PascalCase"],
|
|
||||||
"leadingUnderscore": "allowSingleOrDouble",
|
|
||||||
"trailingUnderscore": "allow"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "enum",
|
|
||||||
"format": ["PascalCase", "UPPER_CASE"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selector": "typeLike",
|
|
||||||
"format": ["PascalCase"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prefer-const": "error",
|
|
||||||
"@typescript-eslint/no-unused-expressions": "error",
|
|
||||||
"@typescript-eslint/no-empty-interface": "error",
|
|
||||||
"max-len": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"ignorePattern": "^import \\{.+;$",
|
|
||||||
"ignoreRegExpLiterals": true,
|
|
||||||
"ignoreStrings": true,
|
|
||||||
"ignoreUrls": true,
|
|
||||||
"code": 150
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id-blacklist": "error",
|
|
||||||
"@typescript-eslint/member-delimiter-style": "error",
|
|
||||||
"no-shadow": "off",
|
|
||||||
"@typescript-eslint/no-shadow": ["error"],
|
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
|
||||||
"no-mixed-operators": "error",
|
|
||||||
"prefer-arrow/prefer-arrow-functions": "error",
|
|
||||||
"@angular-eslint/directive-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "attribute",
|
|
||||||
"prefix": "",
|
|
||||||
"style": "camelCase"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/ban-types": "error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.html"],
|
|
||||||
"extends": [
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@angular-eslint/template/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2020,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"processor": "@angular-eslint/template/extract-inline-html",
|
|
||||||
"plugins": ["prettier"],
|
|
||||||
"rules": {
|
|
||||||
"@angular-eslint/template/no-negated-async": "off",
|
|
||||||
"prettier/prettier": "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
5
.github/workflows/publish-to-hub-docker.yml
vendored
5
.github/workflows/publish-to-hub-docker.yml
vendored
|
|
@ -28,11 +28,14 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
# Note: Docker Hub image is under johannesjo/ (personal account) while
|
||||||
|
# the GitHub repo is under super-productivity/ org. This is intentional
|
||||||
|
# to maintain existing Docker image references for users.
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||||
with:
|
with:
|
||||||
images: super-productivity/super-productivity
|
images: johannesjo/super-productivity
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
|
||||||
12
angular.json
12
angular.json
|
|
@ -42,7 +42,17 @@
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
"stylePreprocessorOptions": {
|
"stylePreprocessorOptions": {
|
||||||
"includePaths": [".", "src", "src/styles"]
|
"includePaths": [".", "src", "src/styles"]
|
||||||
}
|
},
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"typia",
|
||||||
|
"electron-log",
|
||||||
|
"stacktrace-js",
|
||||||
|
"spark-md5",
|
||||||
|
"chrono-node",
|
||||||
|
"dayjs",
|
||||||
|
"jira2md",
|
||||||
|
"query-string"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ const _invoke: (channel: IPCEventValue, ...args: unknown[]) => Promise<unknown>
|
||||||
...args
|
...args
|
||||||
) => ipcRenderer.invoke(channel, ...args);
|
) => ipcRenderer.invoke(channel, ...args);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const ea: ElectronAPI = {
|
const ea: ElectronAPI = {
|
||||||
on: (
|
on: (
|
||||||
channel: string,
|
channel: string,
|
||||||
|
|
|
||||||
127
eslint.config.js
Normal file
127
eslint.config.js
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
// @ts-check
|
||||||
|
const tseslint = require('typescript-eslint');
|
||||||
|
const angular = require('angular-eslint');
|
||||||
|
const prettierRecommended = require('eslint-plugin-prettier/recommended');
|
||||||
|
const preferArrow = require('eslint-plugin-prefer-arrow');
|
||||||
|
|
||||||
|
module.exports = tseslint.config(
|
||||||
|
// Global ignores
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'app-builds/**/*',
|
||||||
|
'dist/**',
|
||||||
|
'node_modules/**/*',
|
||||||
|
'src/app/t.const.ts',
|
||||||
|
'src/assets/bundled-plugins/**/*',
|
||||||
|
'src/app/config/env.generated.ts',
|
||||||
|
'.tmp/**/*',
|
||||||
|
'packages/**/*',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// TypeScript files
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
extends: [
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...angular.configs.tsRecommended,
|
||||||
|
prettierRecommended,
|
||||||
|
],
|
||||||
|
processor: angular.processInlineTemplates,
|
||||||
|
plugins: {
|
||||||
|
'prefer-arrow': preferArrow,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Disabled rules
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@angular-eslint/component-selector': 'off',
|
||||||
|
'@angular-eslint/no-input-rename': 'off',
|
||||||
|
'@typescript-eslint/no-inferrable-types': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'arrow-body-style': 'off',
|
||||||
|
'@typescript-eslint/member-ordering': 'off',
|
||||||
|
'import/order': 'off',
|
||||||
|
'arrow-parens': 'off',
|
||||||
|
'@typescript-eslint/explicit-member-accessibility': 'off',
|
||||||
|
|
||||||
|
// Enabled rules
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ args: 'none', caughtErrors: 'none' },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowExpressions: true,
|
||||||
|
allowTypedFunctionExpressions: true,
|
||||||
|
allowHigherOrderFunctions: true,
|
||||||
|
allowDirectConstAssertionInArrowFunctions: true,
|
||||||
|
allowConciseArrowFunctionExpressionsStartingWithVoid: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/naming-convention': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector: 'default',
|
||||||
|
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
|
||||||
|
leadingUnderscore: 'allowSingleOrDouble',
|
||||||
|
trailingUnderscore: 'allow',
|
||||||
|
filter: { regex: '(should)|@tags', match: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'variable',
|
||||||
|
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
|
||||||
|
leadingUnderscore: 'allowSingleOrDouble',
|
||||||
|
trailingUnderscore: 'allow',
|
||||||
|
},
|
||||||
|
{ selector: 'enum', format: ['PascalCase', 'UPPER_CASE'] },
|
||||||
|
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||||
|
],
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'@typescript-eslint/no-unused-expressions': 'error',
|
||||||
|
'@typescript-eslint/no-empty-object-type': 'error',
|
||||||
|
'max-len': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignorePattern: '^import \\{.+;$',
|
||||||
|
ignoreRegExpLiterals: true,
|
||||||
|
ignoreStrings: true,
|
||||||
|
ignoreUrls: true,
|
||||||
|
code: 150,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'id-blacklist': 'error',
|
||||||
|
// @typescript-eslint/member-delimiter-style removed in v8 - Prettier handles this
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
|
'no-mixed-operators': 'error',
|
||||||
|
'prefer-arrow/prefer-arrow-functions': 'error',
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{ type: 'attribute', prefix: '', style: 'camelCase' },
|
||||||
|
],
|
||||||
|
// @typescript-eslint/ban-types replaced by specific rules in v8
|
||||||
|
'@typescript-eslint/no-unsafe-function-type': 'error',
|
||||||
|
'@typescript-eslint/no-wrapper-object-types': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// HTML files
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
extends: [...angular.configs.templateRecommended, prettierRecommended],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/template/no-negated-async': 'off',
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
10024
package-lock.json
generated
10024
package-lock.json
generated
File diff suppressed because it is too large
Load diff
117
package.json
117
package.json
|
|
@ -139,34 +139,32 @@
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"fs-extra": "^11.3.2",
|
|
||||||
"hash-wasm": "^4.12.0",
|
"hash-wasm": "^4.12.0",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"uuidv7": "^1.1.0"
|
"uuidv7": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^20.0.0",
|
"@angular-devkit/build-angular": "^21.0.5",
|
||||||
"@angular-devkit/build-angular": "^20.3.13",
|
"@angular-eslint/builder": "^21.1.0",
|
||||||
"@angular-eslint/builder": "^20.5.0",
|
"@angular-eslint/eslint-plugin": "^21.1.0",
|
||||||
"@angular-eslint/eslint-plugin": "^20.5.0",
|
"@angular-eslint/eslint-plugin-template": "^21.1.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "^20.5.0",
|
"@angular-eslint/schematics": "^21.1.0",
|
||||||
"@angular-eslint/schematics": "^20.5.0",
|
"@angular-eslint/template-parser": "^21.1.0",
|
||||||
"@angular-eslint/template-parser": "^20.5.0",
|
"@angular/animations": "^21.0.8",
|
||||||
"@angular/animations": "^20.3.15",
|
"@angular/cdk": "^21.0.6",
|
||||||
"@angular/cdk": "^20.2.10",
|
"@angular/cli": "^21.0.5",
|
||||||
"@angular/cli": "^20.3.13",
|
"@angular/common": "^21.0.8",
|
||||||
"@angular/common": "^20.3.15",
|
"@angular/compiler": "^21.0.8",
|
||||||
"@angular/compiler": "^20.3.15",
|
"@angular/compiler-cli": "^21.0.8",
|
||||||
"@angular/compiler-cli": "^20.3.15",
|
"@angular/core": "^21.0.8",
|
||||||
"@angular/core": "^20.3.15",
|
"@angular/forms": "^21.0.8",
|
||||||
"@angular/forms": "^20.3.15",
|
"@angular/language-service": "^21.0.8",
|
||||||
"@angular/language-service": "^20.3.15",
|
"@angular/material": "^21.0.6",
|
||||||
"@angular/material": "^20.2.10",
|
"@angular/platform-browser": "^21.0.8",
|
||||||
"@angular/platform-browser": "^20.3.15",
|
"@angular/platform-browser-dynamic": "^21.0.8",
|
||||||
"@angular/platform-browser-dynamic": "^20.3.15",
|
"@angular/platform-server": "^21.0.8",
|
||||||
"@angular/platform-server": "^20.3.15",
|
"@angular/router": "^21.0.8",
|
||||||
"@angular/router": "^20.3.15",
|
"@angular/service-worker": "^21.0.8",
|
||||||
"@angular/service-worker": "^20.3.15",
|
|
||||||
"@capacitor/android": "^7.4.4",
|
"@capacitor/android": "^7.4.4",
|
||||||
"@capacitor/app": "^7.1.0",
|
"@capacitor/app": "^7.1.0",
|
||||||
"@capacitor/cli": "^7.4.4",
|
"@capacitor/cli": "^7.4.4",
|
||||||
|
|
@ -179,58 +177,59 @@
|
||||||
"@csstools/stylelint-formatter-github": "^1.0.0",
|
"@csstools/stylelint-formatter-github": "^1.0.0",
|
||||||
"@dotenv-run/cli": "^1.3.6",
|
"@dotenv-run/cli": "^1.3.6",
|
||||||
"@electron/notarize": "^3.1.1",
|
"@electron/notarize": "^3.1.1",
|
||||||
"@fontsource/open-sans": "^5.2.6",
|
"@eslint/js": "^9.39.2",
|
||||||
|
"@fontsource/open-sans": "^5.2.7",
|
||||||
"@nextcloud/cdav-library": "^1.5.3",
|
"@nextcloud/cdav-library": "^1.5.3",
|
||||||
"@ngrx/effects": "^20.0.0",
|
"@ngrx/effects": "^21.0.1",
|
||||||
"@ngrx/entity": "^20.0.0",
|
"@ngrx/entity": "^21.0.1",
|
||||||
"@ngrx/schematics": "^20.1.0",
|
"@ngrx/schematics": "^21.0.1",
|
||||||
"@ngrx/store": "20.0.0",
|
"@ngrx/store": "21.0.1",
|
||||||
"@ngrx/store-devtools": "^20.0.0",
|
"@ngrx/store-devtools": "^21.0.1",
|
||||||
"@ngx-formly/core": "7.0.0",
|
"@ngx-formly/core": "^7.0.1",
|
||||||
"@ngx-formly/material": "7.0.0",
|
"@ngx-formly/material": "^7.0.1",
|
||||||
"@ngx-translate/core": "^17.0.0",
|
"@ngx-translate/core": "^17.0.0",
|
||||||
"@ngx-translate/http-loader": "^17.0.0",
|
"@ngx-translate/http-loader": "^17.0.0",
|
||||||
"@playwright/test": "^1.56.1",
|
"@playwright/test": "^1.57.0",
|
||||||
"@schematics/angular": "^20.1.4",
|
"@schematics/angular": "^21.0.0",
|
||||||
"@types/electron": "^1.4.38",
|
"@types/electron": "^1.4.38",
|
||||||
"@types/electron-localshortcut": "^3.1.3",
|
"@types/electron-localshortcut": "^3.1.3",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@types/jasmine": "^3.10.2",
|
"@types/jasmine": "^5.1.14",
|
||||||
"@types/jasminewd2": "~2.0.13",
|
"@types/jasminewd2": "~2.0.13",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^22.19.5",
|
||||||
"@types/node-fetch": "^2.6.6",
|
"@types/node-fetch": "^2.6.6",
|
||||||
"@types/object-path": "^0.11.4",
|
"@types/object-path": "^0.11.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
||||||
"@typescript-eslint/parser": "7.18.0",
|
|
||||||
"@typescript-eslint/types": "^8.17.0",
|
"@typescript-eslint/types": "^8.17.0",
|
||||||
"@typescript-eslint/utils": "^8.51.0",
|
"@typescript-eslint/utils": "^8.51.0",
|
||||||
|
"angular-eslint": "^21.1.0",
|
||||||
"angular-material-css-vars": "^9.1.1",
|
"angular-material-css-vars": "^9.1.1",
|
||||||
"baseline-browser-mapping": "^2.9.11",
|
"baseline-browser-mapping": "^2.9.14",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"chai": "^5.1.2",
|
"chai": "^5.1.2",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.5.1",
|
||||||
"chrono-node": "^2.8.3",
|
"chrono-node": "^2.9.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"conventional-changelog-cli": "^5.0.0",
|
"conventional-changelog-cli": "^5.0.0",
|
||||||
"core-js": "^3.39.0",
|
"core-js": "^3.47.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"detect-it": "^4.0.1",
|
"detect-it": "^4.0.1",
|
||||||
"electron": "37.10.3",
|
"electron": "37.10.3",
|
||||||
"electron-builder": "^26.3.3",
|
"electron-builder": "^26.4.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsdoc": "61.4.1",
|
"eslint-plugin-jsdoc": "61.4.1",
|
||||||
"eslint-plugin-local-rules": "^3.0.2",
|
"eslint-plugin-local-rules": "^3.0.2",
|
||||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"fs-extra": "^11.3.3",
|
||||||
"glob": "^9.3.5",
|
"glob": "^9.3.5",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"ical.js": "^2.1.0",
|
"ical.js": "^2.2.1",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"jasmine-core": "^5.10.0",
|
"jasmine-core": "^5.13.0",
|
||||||
"jasmine-marbles": "^0.9.2",
|
"jasmine-marbles": "^0.9.2",
|
||||||
"jasmine-spec-reporter": "~7.0.0",
|
"jasmine-spec-reporter": "~7.0.0",
|
||||||
"jira2md": "git+https://github.com/johannesjo/J2M.git",
|
"jira2md": "git+https://github.com/johannesjo/J2M.git",
|
||||||
|
|
@ -241,32 +240,30 @@
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "^1.6.0",
|
"karma-jasmine-html-reporter": "^1.6.0",
|
||||||
"karma-spec-reporter": "^0.0.36",
|
"karma-spec-reporter": "^0.0.36",
|
||||||
"marked": "^12.0.2",
|
"marked": "^17.0.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"new-github-issue-url": "^1.1.0",
|
"new-github-issue-url": "^1.1.0",
|
||||||
"ng2-charts": "^8.0.0",
|
"ng2-charts": "^8.0.0",
|
||||||
"ngx-markdown": "^20.0.0",
|
"ngx-markdown": "^21.0.0",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.57.0",
|
||||||
"prettier": "^3.5.1",
|
"prettier": "^3.7.4",
|
||||||
"pretty-quick": "^4.1.1",
|
"pretty-quick": "^4.2.2",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"shepherd.js": "^11.2.0",
|
"shepherd.js": "^11.2.0",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2",
|
||||||
"start-server-and-test": "^2.0.9",
|
"start-server-and-test": "^2.1.3",
|
||||||
"stylelint": "^16.18.0",
|
"stylelint": "^16.26.1",
|
||||||
"stylelint-config-recommended-scss": "^14.1.0",
|
"stylelint-config-recommended-scss": "^14.1.0",
|
||||||
"ts-node": "~10.9.2",
|
"ts-node": "~10.9.2",
|
||||||
"ts-patch": "^3.3.0",
|
"ts-patch": "^3.3.0",
|
||||||
"tslib": "^2.7.0",
|
"tslib": "^2.7.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.9.3",
|
||||||
"typia": "^9.1.0"
|
"typescript-eslint": "^8.52.0",
|
||||||
|
"typia": "^11.0.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"ngx-markdown": {
|
|
||||||
"marked": "12.0.2"
|
|
||||||
},
|
|
||||||
"@conventional-changelog/git-client": "^2.5.1"
|
"@conventional-changelog/git-client": "^2.5.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["solid", "@typescript-eslint", "prettier"],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:solid/typescript",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn"
|
|
||||||
},
|
|
||||||
"ignorePatterns": ["dist", "node_modules"]
|
|
||||||
}
|
|
||||||
16
packages/plugin-dev/automations/eslint.config.js
Normal file
16
packages/plugin-dev/automations/eslint.config.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// @ts-check
|
||||||
|
const tseslint = require('typescript-eslint');
|
||||||
|
const solid = require('eslint-plugin-solid/configs/typescript');
|
||||||
|
const prettierRecommended = require('eslint-plugin-prettier/recommended');
|
||||||
|
|
||||||
|
module.exports = tseslint.config(
|
||||||
|
{ ignores: ['dist', 'node_modules'] },
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
extends: [...tseslint.configs.recommended, solid, prettierRecommended],
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["solid", "@typescript-eslint", "prettier"],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:solid/typescript",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn"
|
|
||||||
},
|
|
||||||
"ignorePatterns": ["dist", "node_modules"]
|
|
||||||
}
|
|
||||||
16
packages/plugin-dev/boilerplate-solid-js/eslint.config.js
Normal file
16
packages/plugin-dev/boilerplate-solid-js/eslint.config.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// @ts-check
|
||||||
|
const tseslint = require('typescript-eslint');
|
||||||
|
const solid = require('eslint-plugin-solid/configs/typescript');
|
||||||
|
const prettierRecommended = require('eslint-plugin-prettier/recommended');
|
||||||
|
|
||||||
|
module.exports = tseslint.config(
|
||||||
|
{ ignores: ['dist', 'node_modules'] },
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
extends: [...tseslint.configs.recommended, solid, prettierRecommended],
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../../.eslintrc.json",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
packages/plugin-dev/sync-md/eslint.config.js
Normal file
11
packages/plugin-dev/sync-md/eslint.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// @ts-check
|
||||||
|
const tseslint = require('typescript-eslint');
|
||||||
|
const rootConfig = require('../../../eslint.config.js');
|
||||||
|
|
||||||
|
module.exports = tseslint.config(...rootConfig, {
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -28,7 +28,6 @@ export class GlobalErrorHandler implements ErrorHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
let simpleStack = '';
|
let simpleStack = '';
|
||||||
if (
|
if (
|
||||||
err &&
|
err &&
|
||||||
|
|
|
||||||
|
|
@ -265,9 +265,8 @@ export class ShareService {
|
||||||
private async _showShareDialog(payload: SharePayload): Promise<ShareResult> {
|
private async _showShareDialog(payload: SharePayload): Promise<ShareResult> {
|
||||||
try {
|
try {
|
||||||
// Import dialog component dynamically to avoid circular dependencies
|
// Import dialog component dynamically to avoid circular dependencies
|
||||||
const { DialogShareComponent } = await import(
|
const { DialogShareComponent } =
|
||||||
'./dialog-share/dialog-share.component'
|
await import('./dialog-share/dialog-share.component');
|
||||||
);
|
|
||||||
|
|
||||||
const dialogRef = this._matDialog.open(DialogShareComponent, {
|
const dialogRef = this._matDialog.open(DialogShareComponent, {
|
||||||
width: '500px',
|
width: '500px',
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { T } from '../../t.const';
|
import { T } from '../../t.const';
|
||||||
import { DEFAULT_PANEL_CFG } from './boards.const';
|
import { DEFAULT_PANEL_CFG } from './boards.const';
|
||||||
import { FormlyFieldConfig } from '@ngx-formly/core/lib/models/fieldconfig';
|
import { FormlyFieldConfig } from '@ngx-formly/core';
|
||||||
|
|
||||||
const getNewPanel = (): BoardPanelCfg => ({
|
const getNewPanel = (): BoardPanelCfg => ({
|
||||||
...DEFAULT_PANEL_CFG,
|
...DEFAULT_PANEL_CFG,
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
|
||||||
isTimedFullScreenBlocker: false,
|
isTimedFullScreenBlocker: false,
|
||||||
timedFullScreenBlockerDuration: 8000,
|
timedFullScreenBlockerDuration: 8000,
|
||||||
isFocusWindow: false,
|
isFocusWindow: false,
|
||||||
/* eslint-disable-next-line */
|
|
||||||
takeABreakMessage:
|
takeABreakMessage:
|
||||||
'You have been working for ${duration} without one. Go away from the computer! Take a short walk! Makes you more productive in the long run!',
|
'You have been working for ${duration} without one. Go away from the computer! Take a short walk! Makes you more productive in the long run!',
|
||||||
takeABreakMinWorkingTime: 60 * minute,
|
takeABreakMinWorkingTime: 60 * minute,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, LocalBackupConfig } from '../global-config.model';
|
import { ConfigFormSection, LocalBackupConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, DominaModeConfig } from '../global-config.model';
|
import { ConfigFormSection, DominaModeConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
import { speak } from '../../../util/speak';
|
import { speak } from '../../../util/speak';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, EvaluationConfig } from '../global-config.model';
|
import { ConfigFormSection, EvaluationConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, IdleConfig } from '../global-config.model';
|
import { ConfigFormSection, IdleConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
import { HelperClasses } from '../../../app.constants';
|
import { HelperClasses } from '../../../app.constants';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection } from '../global-config.model';
|
import { ConfigFormSection } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, LimitedFormlyFieldConfig } from '../global-config.model';
|
import { ConfigFormSection, LimitedFormlyFieldConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
import { IS_ELECTRON } from '../../../app.constants';
|
import { IS_ELECTRON } from '../../../app.constants';
|
||||||
|
|
@ -392,4 +391,3 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection<KeyboardConfig> = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
/* eslint-enable max-len */
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import {
|
import {
|
||||||
ConfigFormSection,
|
ConfigFormSection,
|
||||||
LimitedFormlyFieldConfig,
|
LimitedFormlyFieldConfig,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection } from '../global-config.model';
|
import { ConfigFormSection } from '../global-config.model';
|
||||||
import {
|
import {
|
||||||
SimpleCounterConfig,
|
SimpleCounterConfig,
|
||||||
|
|
@ -7,7 +6,7 @@ import {
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
import { EMPTY_SIMPLE_COUNTER } from '../../simple-counter/simple-counter.const';
|
import { EMPTY_SIMPLE_COUNTER } from '../../simple-counter/simple-counter.const';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { FormlyFieldConfig } from '@ngx-formly/core/lib/models/fieldconfig';
|
import { FormlyFieldConfig } from '@ngx-formly/core';
|
||||||
|
|
||||||
export const SIMPLE_COUNTER_FORM: ConfigFormSection<SimpleCounterConfig> = {
|
export const SIMPLE_COUNTER_FORM: ConfigFormSection<SimpleCounterConfig> = {
|
||||||
title: T.F.SIMPLE_COUNTER.FORM.TITLE,
|
title: T.F.SIMPLE_COUNTER.FORM.TITLE,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, SoundConfig } from '../global-config.model';
|
import { ConfigFormSection, SoundConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
import { playDoneSound } from '../../tasks/util/play-done-sound';
|
import { playDoneSound } from '../../tasks/util/play-done-sound';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable max-len, @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
import { ConfigFormSection, SyncConfig } from '../global-config.model';
|
import { ConfigFormSection, SyncConfig } from '../global-config.model';
|
||||||
import { LegacySyncProvider } from '../../../imex/sync/legacy-sync-provider.model';
|
import { LegacySyncProvider } from '../../../imex/sync/legacy-sync-provider.model';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, TakeABreakConfig } from '../global-config.model';
|
import { ConfigFormSection, TakeABreakConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable max-len */
|
|
||||||
import { ConfigFormSection, TimeTrackingConfig } from '../global-config.model';
|
import { ConfigFormSection, TimeTrackingConfig } from '../global-config.model';
|
||||||
import { T } from '../../../t.const';
|
import { T } from '../../../t.const';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,8 +245,10 @@ export type GlobalSectionConfig =
|
||||||
| SyncConfig;
|
| SyncConfig;
|
||||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||||
|
|
||||||
export interface LimitedFormlyFieldConfig<FormModel>
|
export interface LimitedFormlyFieldConfig<FormModel> extends Omit<
|
||||||
extends Omit<FormlyFieldConfig, 'key'> {
|
FormlyFieldConfig,
|
||||||
|
'key'
|
||||||
|
> {
|
||||||
key?: keyof FormModel;
|
key?: keyof FormModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,8 +271,10 @@ export interface ConfigFormSection<FormModel> {
|
||||||
isHideForAndroidApp?: boolean;
|
isHideForAndroidApp?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericConfigFormSection
|
export interface GenericConfigFormSection extends Omit<
|
||||||
extends Omit<ConfigFormSection<unknown>, 'items'> {
|
ConfigFormSection<unknown>,
|
||||||
|
'items'
|
||||||
|
> {
|
||||||
items?: FormlyFieldConfig[];
|
items?: FormlyFieldConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ export class GlobalConfigEffects {
|
||||||
sectionCfg && typeof (sectionCfg as MiscConfig).startOfNextDay === 'number',
|
sectionCfg && typeof (sectionCfg as MiscConfig).startOfNextDay === 'number',
|
||||||
),
|
),
|
||||||
tap(({ sectionKey, sectionCfg }) => {
|
tap(({ sectionKey, sectionCfg }) => {
|
||||||
// eslint-disable-next-line
|
|
||||||
this._dateService.setStartOfNextDayDiff((sectionCfg as any)['startOfNextDay']);
|
this._dateService.setStartOfNextDayDiff((sectionCfg as any)['startOfNextDay']);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -163,9 +163,9 @@ export class CaldavClientService {
|
||||||
}
|
}
|
||||||
for (i = 0; i < etag.length; i++) {
|
for (i = 0; i < etag.length; i++) {
|
||||||
chr = etag.charCodeAt(i);
|
chr = etag.charCodeAt(i);
|
||||||
hash = (hash << 5) - hash + chr; //eslint-disable-line no-bitwise
|
hash = (hash << 5) - hash + chr;
|
||||||
// Convert to 32bit integer
|
// Convert to 32bit integer
|
||||||
hash |= 0; //eslint-disable-line no-bitwise
|
hash |= 0;
|
||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +295,7 @@ export class CaldavClientService {
|
||||||
const oldOpen = xhr.open;
|
const oldOpen = xhr.open;
|
||||||
|
|
||||||
// override open() method to add headers
|
// override open() method to add headers
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
||||||
xhr.open = function (): void {
|
xhr.open = function (): void {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export type GithubOriginalMileStone = Readonly<{
|
||||||
labels_url: string;
|
labels_url: string;
|
||||||
id: number;
|
id: number;
|
||||||
node_id: string;
|
node_id: string;
|
||||||
// eslint-disable-next-line id-blacklist
|
|
||||||
number: number;
|
number: number;
|
||||||
state: GithubOriginalState;
|
state: GithubOriginalState;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -150,7 +150,7 @@ export type GithubOriginalIssue = Readonly<{
|
||||||
comments_url: string;
|
comments_url: string;
|
||||||
events_url: string;
|
events_url: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
// eslint-disable-next-line id-blacklist
|
|
||||||
number: number;
|
number: number;
|
||||||
state: GithubOriginalState;
|
state: GithubOriginalState;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ export const GITHUB_CONFIG_FORM: LimitedFormlyFieldConfig<IssueProviderGithub>[]
|
||||||
key: 'filterUsernameForIssueUpdates',
|
key: 'filterUsernameForIssueUpdates',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
expressions: {
|
expressions: {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
// 'props.disabled': '!model.filterUsername',
|
// 'props.disabled': '!model.filterUsername',
|
||||||
hide: '!model.isAutoPoll',
|
hide: '!model.isAutoPoll',
|
||||||
},
|
},
|
||||||
|
|
@ -75,7 +74,6 @@ export const GITHUB_CONFIG_FORM: LimitedFormlyFieldConfig<IssueProviderGithub>[]
|
||||||
key: 'backlogQuery',
|
key: 'backlogQuery',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
expressions: {
|
expressions: {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
// 'props.disabled': '!model.filterUsername',
|
// 'props.disabled': '!model.filterUsername',
|
||||||
hide: '!model.isAutoAddToBacklog || !model.defaultProjectId',
|
hide: '!model.isAutoAddToBacklog || !model.defaultProjectId',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export const mapGithubIssue = (issue: GithubOriginalIssue): GithubIssue => {
|
||||||
comments_url: issue.comments_url,
|
comments_url: issue.comments_url,
|
||||||
events_url: issue.events_url,
|
events_url: issue.events_url,
|
||||||
html_url: issue.html_url,
|
html_url: issue.html_url,
|
||||||
// eslint-disable-next-line id-blacklist
|
|
||||||
number: issue.number,
|
number: issue.number,
|
||||||
state: issue.state,
|
state: issue.state,
|
||||||
title: issue.title,
|
title: issue.title,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ export type GithubPullRequest = GithubOriginalPullRequest;
|
||||||
export type GithubComment = GithubOriginalComment;
|
export type GithubComment = GithubOriginalComment;
|
||||||
|
|
||||||
export type GithubIssueReduced = Readonly<{
|
export type GithubIssueReduced = Readonly<{
|
||||||
// eslint-disable-next-line id-blacklist
|
|
||||||
state: GithubState;
|
state: GithubState;
|
||||||
title: string;
|
title: string;
|
||||||
// to make it consistent with non reduced issue
|
// to make it consistent with non reduced issue
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export const mapGitlabIssue = (
|
||||||
): GitlabIssue => {
|
): GitlabIssue => {
|
||||||
return {
|
return {
|
||||||
html_url: issue.web_url,
|
html_url: issue.web_url,
|
||||||
// eslint-disable-next-line id-blacklist
|
|
||||||
number: issue.iid,
|
number: issue.iid,
|
||||||
// iid: issue.iid,
|
// iid: issue.iid,
|
||||||
state: issue.state,
|
state: issue.state,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export type GitlabIssue = Readonly<{
|
||||||
// comments_url: string;
|
// comments_url: string;
|
||||||
// events_url: string;
|
// events_url: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
// eslint-disable-next-line id-blacklist
|
|
||||||
number: number;
|
number: number;
|
||||||
state: GitlabState;
|
state: GitlabState;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ import { JiraIssue, JiraIssueReduced } from './jira-issue.model';
|
||||||
import { BannerService } from '../../../../core/banner/banner.service';
|
import { BannerService } from '../../../../core/banner/banner.service';
|
||||||
import { BannerId } from '../../../../core/banner/banner.model';
|
import { BannerId } from '../../../../core/banner/banner.model';
|
||||||
import { T } from '../../../../t.const';
|
import { T } from '../../../../t.const';
|
||||||
import { stringify } from 'query-string';
|
|
||||||
import { getErrorTxt } from '../../../../util/get-error-text';
|
import { getErrorTxt } from '../../../../util/get-error-text';
|
||||||
import { isOnline } from '../../../../util/is-online';
|
import { isOnline } from '../../../../util/is-online';
|
||||||
import { GlobalProgressBarService } from '../../../../core-ui/global-progress-bar/global-progress-bar.service';
|
import { GlobalProgressBarService } from '../../../../core-ui/global-progress-bar/global-progress-bar.service';
|
||||||
|
|
@ -453,7 +452,7 @@ export class JiraApiService {
|
||||||
const requestInit = this._makeRequestInit(jiraReqCfg, cfg);
|
const requestInit = this._makeRequestInit(jiraReqCfg, cfg);
|
||||||
|
|
||||||
const queryStr = jiraReqCfg.query
|
const queryStr = jiraReqCfg.query
|
||||||
? `?${stringify(jiraReqCfg.query, { arrayFormat: 'comma' })}`
|
? `?${stringifyQueryParams(jiraReqCfg.query)}`
|
||||||
: '';
|
: '';
|
||||||
const base = `${stripTrailing(cfg.host || 'null', '/')}/rest/api/${API_VERSION}`;
|
const base = `${stripTrailing(cfg.host || 'null', '/')}/rest/api/${API_VERSION}`;
|
||||||
const url = `${base}/${jiraReqCfg.pathname}${queryStr}`.trim();
|
const url = `${base}/${jiraReqCfg.pathname}${queryStr}`.trim();
|
||||||
|
|
@ -747,3 +746,19 @@ async function streamToJsonIfPossible(stream: ReadableStream): Promise<any> {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
||||||
|
function stringifyQueryParams(
|
||||||
|
params: Record<string, string | boolean | number | string[]>,
|
||||||
|
): string {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// arrayFormat: 'comma' - join array values with comma
|
||||||
|
searchParams.set(key, value.join(','));
|
||||||
|
} else if (value !== undefined && value !== null) {
|
||||||
|
searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return searchParams.toString();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export const JIRA_CONFIG_FORM_SECTION: ConfigFormSection<IssueProviderJira> = {
|
||||||
type: 'input',
|
type: 'input',
|
||||||
templateOptions: {
|
templateOptions: {
|
||||||
type: 'url',
|
type: 'url',
|
||||||
/* eslint-disable-next-line */
|
|
||||||
pattern:
|
pattern:
|
||||||
/^(http(s)?:\/\/)?(localhost|[\w.\-]+(?:\.[\w\.\-]+)+)(:\d+)?(\/[^\s]*)?$/i,
|
/^(http(s)?:\/\/)?(localhost|[\w.\-]+(?:\.[\w\.\-]+)+)(:\d+)?(\/[^\s]*)?$/i,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ export class NoteComponent implements OnChanges {
|
||||||
private readonly _projectService = inject(ProjectService);
|
private readonly _projectService = inject(ProjectService);
|
||||||
private readonly _workContextService = inject(WorkContextService);
|
private readonly _workContextService = inject(WorkContextService);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
note!: Note;
|
note!: Note;
|
||||||
|
|
||||||
// TODO: Skipped for migration because:
|
// TODO: Skipped for migration because:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export class PlannerCalendarEventComponent {
|
||||||
return this.isBeingSubmitted;
|
return this.isBeingSubmitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('click', ['$event'])
|
@HostListener('click')
|
||||||
async onClick(): Promise<void> {
|
async onClick(): Promise<void> {
|
||||||
if (this.isBeingSubmitted) {
|
if (this.isBeingSubmitted) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export class PlannerTaskComponent extends BaseComponent implements OnInit, OnDes
|
||||||
return this.task.id === this._taskService.currentTaskId();
|
return this.task.id === this._taskService.currentTaskId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('click', ['$event'])
|
@HostListener('click')
|
||||||
async clickHandler(): Promise<void> {
|
async clickHandler(): Promise<void> {
|
||||||
if (this.task) {
|
if (this.task) {
|
||||||
// Use bottom panel on mobile, dialog on desktop
|
// Use bottom panel on mobile, dialog on desktop
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ export const CREATE_PROJECT_BASIC_CONFIG_FORM_CONFIG: ConfigFormSection<Project>
|
||||||
// TODO translate
|
// TODO translate
|
||||||
title: 'Project Settings & Theme',
|
title: 'Project Settings & Theme',
|
||||||
key: 'basic',
|
key: 'basic',
|
||||||
/* eslint-disable */
|
|
||||||
help: `Very basic settings for your project.`,
|
help: `Very basic settings for your project.`,
|
||||||
/* eslint-enable */
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ export interface ProjectBasicCfg {
|
||||||
|
|
||||||
// Omit conflicting properties from PluginProject when extending
|
// Omit conflicting properties from PluginProject when extending
|
||||||
export interface ProjectCopy
|
export interface ProjectCopy
|
||||||
extends Omit<PluginProject, 'advancedCfg' | 'theme'>,
|
extends
|
||||||
|
Omit<PluginProject, 'advancedCfg' | 'theme'>,
|
||||||
ProjectBasicCfg,
|
ProjectBasicCfg,
|
||||||
WorkContextCommon {
|
WorkContextCommon {
|
||||||
// Additional app-specific fields
|
// Additional app-specific fields
|
||||||
|
|
|
||||||
|
|
@ -348,7 +348,6 @@ const convertVEventToCalendarIntegrationEvent = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const getAllPossibleEventsAfterStartFromIcal = (
|
const getAllPossibleEventsAfterStartFromIcal = (
|
||||||
ICAL: any,
|
ICAL: any,
|
||||||
icalData: string,
|
icalData: string,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
* The ical.js library is ~76KB and only needed for calendar integration.
|
* The ical.js library is ~76KB and only needed for calendar integration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let icalModule: any = null;
|
let icalModule: any = null;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let loadingPromise: Promise<any> | null = null;
|
let loadingPromise: Promise<any> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,7 +12,7 @@ let loadingPromise: Promise<any> | null = null;
|
||||||
* Subsequent calls return the cached module.
|
* Subsequent calls return the cached module.
|
||||||
* Concurrent calls share the same loading promise to prevent race conditions.
|
* Concurrent calls share the same loading promise to prevent race conditions.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const loadIcalModule = async (): Promise<any> => {
|
export const loadIcalModule = async (): Promise<any> => {
|
||||||
if (icalModule) {
|
if (icalModule) {
|
||||||
return icalModule;
|
return icalModule;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { createBlockedBlocksByDayMap } from './create-blocked-blocks-by-day-map';
|
import { createBlockedBlocksByDayMap } from './create-blocked-blocks-by-day-map';
|
||||||
import { TaskCopy, TaskWithDueTime } from '../../tasks/task.model';
|
import { TaskCopy, TaskWithDueTime } from '../../tasks/task.model';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
|
|
||||||
const NDS = '1970-01-01';
|
const NDS = '1970-01-01';
|
||||||
const H = 60 * 60 * 1000;
|
const H = 60 * 60 * 1000;
|
||||||
const TZ_OFFSET = new Date(NDS).getTimezoneOffset() * 60000;
|
const TZ_OFFSET = new Date(NDS).getTimezoneOffset() * 60000;
|
||||||
|
|
@ -16,7 +14,7 @@ const FAKE_TASK: Partial<TaskCopy> = {
|
||||||
|
|
||||||
const h = (hr: number): number => hr * 60 * 1000 * 60;
|
const h = (hr: number): number => hr * 60 * 1000 * 60;
|
||||||
// const hTz = (hr: number): number => h(hr) + TZ_OFFSET;
|
// const hTz = (hr: number): number => h(hr) + TZ_OFFSET;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-mixed-operators
|
// eslint-disable-next-line no-mixed-operators
|
||||||
const dh = (d: number = 0, hr: number): number => hr * H + d * h(24);
|
const dh = (d: number = 0, hr: number): number => hr * H + d * h(24);
|
||||||
const dhTz = (d: number = 0, hr: number): number => dh(d, hr) + TZ_OFFSET;
|
const dhTz = (d: number = 0, hr: number): number => dh(d, hr) + TZ_OFFSET;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ export const mapScheduleDaysToScheduleEvents = (
|
||||||
|
|
||||||
days.forEach((day, dayIndex) => {
|
days.forEach((day, dayIndex) => {
|
||||||
beyondBudgetDays[dayIndex] = day.beyondBudgetTasks.map((taskPlannedForDay) => {
|
beyondBudgetDays[dayIndex] = day.beyondBudgetTasks.map((taskPlannedForDay) => {
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
const timeLeft = getTimeLeftForTask(taskPlannedForDay);
|
const timeLeft = getTimeLeftForTask(taskPlannedForDay);
|
||||||
const timeLeftInHours = timeLeft / 1000 / 60 / 60;
|
const timeLeftInHours = timeLeft / 1000 / 60 / 60;
|
||||||
const rowSpan = Math.max(Math.round(timeLeftInHours * FH), 1);
|
const rowSpan = Math.max(Math.round(timeLeftInHours * FH), 1);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { mapToScheduleDays } from './map-to-schedule-days';
|
import { mapToScheduleDays } from './map-to-schedule-days';
|
||||||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
|
|
||||||
// Helper function to conditionally skip tests that are timezone-dependent
|
// Helper function to conditionally skip tests that are timezone-dependent
|
||||||
// These tests were written with hardcoded expectations for Europe/Berlin timezone
|
// These tests were written with hardcoded expectations for Europe/Berlin timezone
|
||||||
const TZ_OFFSET = new Date('1970-01-01').getTimezoneOffset() * 60000;
|
const TZ_OFFSET = new Date('1970-01-01').getTimezoneOffset() * 60000;
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,10 @@ const FAKE_TASK: Partial<TaskCopy> = {
|
||||||
|
|
||||||
const h = (hr: number): number => hr * 60 * 1000 * 60;
|
const h = (hr: number): number => hr * 60 * 1000 * 60;
|
||||||
const hTz = (hr: number): number => h(hr) + TZ_OFFSET;
|
const hTz = (hr: number): number => h(hr) + TZ_OFFSET;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-mixed-operators
|
// eslint-disable-next-line no-mixed-operators
|
||||||
const dh = (d: number = 0, hr: number): number => hr * H + d * h(24);
|
const dh = (d: number = 0, hr: number): number => hr * H + d * h(24);
|
||||||
const dhTz = (d: number = 0, hr: number): number => dh(d, hr) + TZ_OFFSET;
|
const dhTz = (d: number = 0, hr: number): number => dh(d, hr) + TZ_OFFSET;
|
||||||
|
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
const minAfterNow = (min: number): Date => new Date(1970, 0, 1, 0, min, 0, 0);
|
const minAfterNow = (min: number): Date => new Date(1970, 0, 1, 0, min, 0, 0);
|
||||||
|
|
||||||
const minAfterNowTs = (min: number): number => minAfterNow(min).getTime();
|
const minAfterNowTs = (min: number): number => minAfterNow(min).getTime();
|
||||||
|
|
@ -404,7 +403,7 @@ describe('mapToScheduleDays()', () => {
|
||||||
id: 'N1',
|
id: 'N1',
|
||||||
// NOTE: the 24h stuff only works if we count from 0 of the current timezone
|
// NOTE: the 24h stuff only works if we count from 0 of the current timezone
|
||||||
start: h(0),
|
start: h(0),
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
duration: h(23) - 60000,
|
duration: h(23) - 60000,
|
||||||
type: 'Task',
|
type: 'Task',
|
||||||
},
|
},
|
||||||
|
|
@ -418,7 +417,7 @@ describe('mapToScheduleDays()', () => {
|
||||||
{
|
{
|
||||||
data: jasmine.any(Object),
|
data: jasmine.any(Object),
|
||||||
id: 'N2',
|
id: 'N2',
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
start: hTz(24),
|
start: hTz(24),
|
||||||
duration: h(1),
|
duration: h(1),
|
||||||
type: 'SplitTask',
|
type: 'SplitTask',
|
||||||
|
|
@ -426,7 +425,7 @@ describe('mapToScheduleDays()', () => {
|
||||||
{
|
{
|
||||||
data: jasmine.any(Object),
|
data: jasmine.any(Object),
|
||||||
id: 'R1_1970-01-02',
|
id: 'R1_1970-01-02',
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
start: hTz(25),
|
start: hTz(25),
|
||||||
duration: h(1),
|
duration: h(1),
|
||||||
type: 'ScheduledRepeatProjection',
|
type: 'ScheduledRepeatProjection',
|
||||||
|
|
@ -434,7 +433,7 @@ describe('mapToScheduleDays()', () => {
|
||||||
{
|
{
|
||||||
data: jasmine.any(Object),
|
data: jasmine.any(Object),
|
||||||
id: 'N2_1970-01-02_0',
|
id: 'N2_1970-01-02_0',
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
start: hTz(26),
|
start: hTz(26),
|
||||||
duration: h(1),
|
duration: h(1),
|
||||||
type: 'SplitTaskContinuedLast',
|
type: 'SplitTaskContinuedLast',
|
||||||
|
|
@ -482,7 +481,7 @@ describe('mapToScheduleDays()', () => {
|
||||||
data: jasmine.any(Object),
|
data: jasmine.any(Object),
|
||||||
id: 'N1',
|
id: 'N1',
|
||||||
start: hTz(0),
|
start: hTz(0),
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
duration: h(23) - 60000,
|
duration: h(23) - 60000,
|
||||||
type: 'Task',
|
type: 'Task',
|
||||||
},
|
},
|
||||||
|
|
@ -496,7 +495,7 @@ describe('mapToScheduleDays()', () => {
|
||||||
{
|
{
|
||||||
data: jasmine.any(Object),
|
data: jasmine.any(Object),
|
||||||
id: 'R1_1970-01-02',
|
id: 'R1_1970-01-02',
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
start: hTz(24),
|
start: hTz(24),
|
||||||
duration: h(2),
|
duration: h(2),
|
||||||
type: 'RepeatProjection',
|
type: 'RepeatProjection',
|
||||||
|
|
@ -550,7 +549,7 @@ describe('mapToScheduleDays()', () => {
|
||||||
data: jasmine.any(Object),
|
data: jasmine.any(Object),
|
||||||
id: 'N1',
|
id: 'N1',
|
||||||
start: dhTz(0, 9),
|
start: dhTz(0, 9),
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
duration: h(8),
|
duration: h(8),
|
||||||
type: 'SplitTask',
|
type: 'SplitTask',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,10 @@ export interface ScheduleFromCalendarEvent extends CalendarIntegrationEvent {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleCustomEvent
|
export interface ScheduleCustomEvent extends Omit<
|
||||||
extends Omit<ScheduleFromCalendarEvent, 'calProviderId'> {
|
ScheduleFromCalendarEvent,
|
||||||
|
'calProviderId'
|
||||||
|
> {
|
||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export class ScheduleComponent {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const hours = now.getHours();
|
const hours = now.getHours();
|
||||||
const minutes = now.getMinutes();
|
const minutes = now.getMinutes();
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
const hoursToday = hours + minutes / 60;
|
const hoursToday = hours + minutes / 60;
|
||||||
return Math.round(hoursToday * FH);
|
return Math.round(hoursToday * FH);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -739,7 +739,7 @@ export const SHEPHERD_STEPS = (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Moving around',
|
title: 'Moving around',
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
text: `<p>When a task is focused you can navigate to other tasks by pressing the arrow keys <kbd>↑</kbd> and <kbd>↓</kbd>.</p>`,
|
text: `<p>When a task is focused you can navigate to other tasks by pressing the arrow keys <kbd>↑</kbd> and <kbd>↓</kbd>.</p>`,
|
||||||
when: {
|
when: {
|
||||||
show: () => taskService.focusFirstTaskIfVisible(),
|
show: () => taskService.focusFirstTaskIfVisible(),
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ describe('getSimpleCounterStreakDuration()', () => {
|
||||||
countOnDay: { [getDbDateStr()]: 1 },
|
countOnDay: { [getDbDateStr()]: 1 },
|
||||||
isTrackStreaks: true,
|
isTrackStreaks: true,
|
||||||
streakMinValue: 2,
|
streakMinValue: 2,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
streakWeekDays: { 0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true },
|
streakWeekDays: { 0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ export const isEqualSimpleCounterCfg = (
|
||||||
}
|
}
|
||||||
for (let i = 0; i < a.length; ++i) {
|
for (let i = 0; i < a.length; ++i) {
|
||||||
if (a[i] !== b[i]) {
|
if (a[i] !== b[i]) {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
||||||
for (let j = 0; j < FIELDS_TO_COMPARE.length; j++) {
|
for (let j = 0; j < FIELDS_TO_COMPARE.length; j++) {
|
||||||
const field = FIELDS_TO_COMPARE[j];
|
const field = FIELDS_TO_COMPARE[j];
|
||||||
if (a[field] !== b[field]) {
|
if (a[field] !== b[field]) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { DEFAULT_SIMPLE_COUNTERS } from '../simple-counter.const';
|
||||||
import { arrayToDictionary } from '../../../util/array-to-dictionary';
|
import { arrayToDictionary } from '../../../util/array-to-dictionary';
|
||||||
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
|
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
|
||||||
import { updateAllInDictionary } from '../../../util/update-all-in-dictionary';
|
import { updateAllInDictionary } from '../../../util/update-all-in-dictionary';
|
||||||
import { Update } from '@ngrx/entity/src/models';
|
import { Update } from '@ngrx/entity';
|
||||||
import {
|
import {
|
||||||
addSimpleCounter,
|
addSimpleCounter,
|
||||||
decreaseSimpleCounterCounterToday,
|
decreaseSimpleCounterCounterToday,
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import { Tag as PluginTag } from '@super-productivity/plugin-api';
|
||||||
|
|
||||||
// Omit conflicting properties from PluginTag when extending
|
// Omit conflicting properties from PluginTag when extending
|
||||||
export interface TagCopy
|
export interface TagCopy
|
||||||
extends Omit<PluginTag, 'advancedCfg' | 'theme'>,
|
extends Omit<PluginTag, 'advancedCfg' | 'theme'>, WorkContextCommon {
|
||||||
WorkContextCommon {
|
|
||||||
// All fields already included in PluginTag
|
// All fields already included in PluginTag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import { getNextRepeatOccurrence } from './get-next-repeat-occurrence.util';
|
||||||
import { DEFAULT_TASK_REPEAT_CFG, TaskRepeatCfg } from '../task-repeat-cfg.model';
|
import { DEFAULT_TASK_REPEAT_CFG, TaskRepeatCfg } from '../task-repeat-cfg.model';
|
||||||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||||
|
|
||||||
/* eslint-disable no-mixed-operators */
|
|
||||||
|
|
||||||
const FAKE_MONDAY_THE_10TH = new Date(2022, 0, 10).getTime();
|
const FAKE_MONDAY_THE_10TH = new Date(2022, 0, 10).getTime();
|
||||||
|
|
||||||
const DUMMY_REPEATABLE_TASK: TaskRepeatCfg = {
|
const DUMMY_REPEATABLE_TASK: TaskRepeatCfg = {
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ describe('selectTaskRepeatCfgsDueOnDay', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
FAKE_MONDAY_THE_10TH + DAY,
|
FAKE_MONDAY_THE_10TH + DAY,
|
||||||
// eslint-disable-next-line no-mixed-operators
|
// eslint-disable-next-line no-mixed-operators
|
||||||
FAKE_MONDAY_THE_10TH + DAY * 11,
|
FAKE_MONDAY_THE_10TH + DAY * 11,
|
||||||
|
|
@ -150,7 +149,6 @@ describe('selectTaskRepeatCfgsDueOnDay', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
FAKE_MONDAY_THE_10TH,
|
FAKE_MONDAY_THE_10TH,
|
||||||
// eslint-disable-next-line no-mixed-operators
|
// eslint-disable-next-line no-mixed-operators
|
||||||
FAKE_MONDAY_THE_10TH + DAY * 2,
|
FAKE_MONDAY_THE_10TH + DAY * 2,
|
||||||
|
|
@ -539,7 +537,6 @@ describe('selectAllUnprocessedTaskRepeatCfgs', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
// eslint-disable-next-line no-mixed-operators
|
|
||||||
FAKE_MONDAY_THE_10TH,
|
FAKE_MONDAY_THE_10TH,
|
||||||
// eslint-disable-next-line no-mixed-operators
|
// eslint-disable-next-line no-mixed-operators
|
||||||
FAKE_MONDAY_THE_10TH + DAY * 2,
|
FAKE_MONDAY_THE_10TH + DAY * 2,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ export enum SORT_ORDER {
|
||||||
|
|
||||||
// === GROUP ===
|
// === GROUP ===
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
export type GroupOption = BaseOption<GROUP_OPTION_TYPE>;
|
||||||
export interface GroupOption extends BaseOption<GROUP_OPTION_TYPE> {}
|
|
||||||
|
|
||||||
export enum GROUP_OPTION_TYPE {
|
export enum GROUP_OPTION_TYPE {
|
||||||
tag = 'tag',
|
tag = 'tag',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
import { Task, TaskState } from '../task.model';
|
import { Task, TaskState } from '../task.model';
|
||||||
import { initialTaskState, taskReducer } from './task.reducer';
|
import { initialTaskState, taskReducer } from './task.reducer';
|
||||||
import * as fromActions from './task.actions';
|
import * as fromActions from './task.actions';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention,@typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { Task, TaskState } from '../task.model';
|
import { Task, TaskState } from '../task.model';
|
||||||
import { initialTaskState, taskReducer } from './task.reducer';
|
import { initialTaskState, taskReducer } from './task.reducer';
|
||||||
import { PlannerActions } from '../../planner/store/planner.actions';
|
import { PlannerActions } from '../../planner/store/planner.actions';
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@ export interface IssueFieldsForTask {
|
||||||
// Extend the plugin Task type with app-specific fields
|
// Extend the plugin Task type with app-specific fields
|
||||||
// Omit issue fields from PluginTask to avoid conflict with IssueFieldsForTask
|
// Omit issue fields from PluginTask to avoid conflict with IssueFieldsForTask
|
||||||
export interface TaskCopy
|
export interface TaskCopy
|
||||||
extends Omit<
|
extends
|
||||||
|
Omit<
|
||||||
PluginTask,
|
PluginTask,
|
||||||
| 'issueId'
|
| 'issueId'
|
||||||
| 'issueProviderId'
|
| 'issueProviderId'
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { WorkContextCommon, WorkContextThemeCfg } from './work-context.model';
|
||||||
import { WorklogExportSettings, WorklogGrouping } from '../worklog/worklog.model';
|
import { WorklogExportSettings, WorklogGrouping } from '../worklog/worklog.model';
|
||||||
import { ConfigFormSection } from '../config/global-config.model';
|
import { ConfigFormSection } from '../config/global-config.model';
|
||||||
import { T } from '../../t.const';
|
import { T } from '../../t.const';
|
||||||
import { FormlyFieldConfig } from '@ngx-formly/core/lib/models/fieldconfig';
|
import { FormlyFieldConfig } from '@ngx-formly/core';
|
||||||
|
|
||||||
export const WORKLOG_EXPORT_DEFAULTS: WorklogExportSettings = {
|
export const WORKLOG_EXPORT_DEFAULTS: WorklogExportSettings = {
|
||||||
cols: ['DATE', 'START', 'END', 'TIME_CLOCK', 'TITLES_INCLUDING_SUB'],
|
cols: ['DATE', 'START', 'END', 'TIME_CLOCK', 'TITLES_INCLUDING_SUB'],
|
||||||
|
|
|
||||||
|
|
@ -190,10 +190,8 @@
|
||||||
>
|
>
|
||||||
@if (isShowAsText) {
|
@if (isShowAsText) {
|
||||||
<button
|
<button
|
||||||
|
(click)="copyToClipboard()"
|
||||||
color="primary"
|
color="primary"
|
||||||
data-clipboard-action="copy"
|
|
||||||
data-clipboard-target="#task-textarea"
|
|
||||||
id="clipboard-btn"
|
|
||||||
mat-button
|
mat-button
|
||||||
>
|
>
|
||||||
<mat-icon>content_paste</mat-icon>
|
<mat-icon>content_paste</mat-icon>
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import {
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { combineLatest, from, Subscription } from 'rxjs';
|
import { combineLatest, from, Subscription } from 'rxjs';
|
||||||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||||
// @ts-ignore
|
|
||||||
import Clipboard from 'clipboard';
|
|
||||||
import { SnackService } from '../../../core/snack/snack.service';
|
import { SnackService } from '../../../core/snack/snack.service';
|
||||||
import { WorklogService } from '../worklog.service';
|
import { WorklogService } from '../worklog.service';
|
||||||
import {
|
import {
|
||||||
|
|
@ -234,16 +232,6 @@ export class WorklogExportComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// dirty but good enough for now
|
|
||||||
const clipboard = new Clipboard('#clipboard-btn');
|
|
||||||
clipboard.on('success', (e: any) => {
|
|
||||||
this._snackService.open({
|
|
||||||
msg: T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD,
|
|
||||||
type: 'SUCCESS',
|
|
||||||
});
|
|
||||||
e.clearSelection();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
|
@ -262,4 +250,20 @@ export class WorklogExportComponent implements OnInit, OnDestroy {
|
||||||
addCol(colOpt: WorklogColTypes): void {
|
addCol(colOpt: WorklogColTypes): void {
|
||||||
this.options.cols.push(colOpt);
|
this.options.cols.push(colOpt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async copyToClipboard(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(this.txt);
|
||||||
|
this._snackService.open({
|
||||||
|
msg: T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD,
|
||||||
|
type: 'SUCCESS',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._snackService.open({
|
||||||
|
msg: 'Failed to copy to clipboard',
|
||||||
|
type: 'ERROR',
|
||||||
|
isSkipTranslate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable one-var */
|
|
||||||
import { WorkStartEnd } from 'src/app/features/work-context/work-context.model';
|
import { WorkStartEnd } from 'src/app/features/work-context/work-context.model';
|
||||||
import { WorklogGrouping } from '../worklog.model';
|
import { WorklogGrouping } from '../worklog.model';
|
||||||
import { createRows } from './worklog-export.util';
|
import { createRows } from './worklog-export.util';
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ const maskString = (key: string, val: string, counter: number): string => {
|
||||||
const recurse = (obj: unknown): void => {
|
const recurse = (obj: unknown): void => {
|
||||||
if (typeof obj !== 'object' || obj === null) return;
|
if (typeof obj !== 'object' || obj === null) return;
|
||||||
|
|
||||||
// eslint-disable-next-line guard-for-in
|
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
const val = (obj as Record<string, unknown>)[key];
|
const val = (obj as Record<string, unknown>)[key];
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,11 @@ export interface LegacyAppDataForProjects {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LegacyAppDataCompleteOptionalSyncModelChange
|
export interface LegacyAppDataCompleteOptionalSyncModelChange
|
||||||
extends LegacyAppBaseData,
|
extends LegacyAppBaseData, LegacyAppDataForProjects {
|
||||||
LegacyAppDataForProjects {
|
|
||||||
lastLocalSyncModelChange?: number | null;
|
lastLocalSyncModelChange?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LegacyAppDataComplete
|
export interface LegacyAppDataComplete
|
||||||
extends LegacyAppBaseData,
|
extends LegacyAppBaseData, LegacyAppDataForProjects {
|
||||||
LegacyAppDataForProjects {
|
|
||||||
lastLocalSyncModelChange: number | null;
|
lastLocalSyncModelChange: number | null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,7 @@ export interface AppArchiveFileData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppBaseData
|
export interface AppBaseData
|
||||||
extends AppBaseWithoutLastSyncModelChange,
|
extends AppBaseWithoutLastSyncModelChange, AppArchiveFileData {}
|
||||||
AppArchiveFileData {}
|
|
||||||
|
|
||||||
export interface LocalSyncMetaForProvider {
|
export interface LocalSyncMetaForProvider {
|
||||||
lastSync: number;
|
lastSync: number;
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,13 @@ const _deriveKeyArgon = async (
|
||||||
outputType: 'binary',
|
outputType: 'binary',
|
||||||
});
|
});
|
||||||
|
|
||||||
return window.crypto.subtle.importKey('raw', derivedBytes, { name: ALGORITHM }, false, [
|
return window.crypto.subtle.importKey(
|
||||||
'encrypt',
|
'raw',
|
||||||
'decrypt',
|
derivedBytes.buffer as ArrayBuffer,
|
||||||
]);
|
{ name: ALGORITHM },
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt'],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptArgon = async (data: string, password: string): Promise<string> => {
|
const decryptArgon = async (data: string, password: string): Promise<string> => {
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,7 @@ import { md5HashPromise } from '../../../../util/md5-hash';
|
||||||
import { PFLog } from '../../../../core/log';
|
import { PFLog } from '../../../../core/log';
|
||||||
import { PrivateCfgByProviderId } from '../../../core/types/sync.types';
|
import { PrivateCfgByProviderId } from '../../../core/types/sync.types';
|
||||||
|
|
||||||
export abstract class LocalFileSyncBase
|
export abstract class LocalFileSyncBase implements SyncProviderServiceInterface<SyncProviderId.LocalFile> {
|
||||||
implements SyncProviderServiceInterface<SyncProviderId.LocalFile>
|
|
||||||
{
|
|
||||||
private static readonly LB = 'LocalFileSyncBase';
|
private static readonly LB = 'LocalFileSyncBase';
|
||||||
|
|
||||||
readonly id = SyncProviderId.LocalFile;
|
readonly id = SyncProviderId.LocalFile;
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ import {
|
||||||
} from '../../../core/errors/sync-errors';
|
} from '../../../core/errors/sync-errors';
|
||||||
import { WebDavHttpHeader, WebDavHttpMethod, WebDavHttpStatus } from './webdav.const';
|
import { WebDavHttpHeader, WebDavHttpMethod, WebDavHttpStatus } from './webdav.const';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
|
|
||||||
export class WebdavApi {
|
export class WebdavApi {
|
||||||
private static readonly L = 'WebdavApi';
|
private static readonly L = 'WebdavApi';
|
||||||
private xmlParser: WebdavXmlParser;
|
private xmlParser: WebdavXmlParser;
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import { SyncLog } from '../../../../core/log';
|
||||||
* Base class for WebDAV-based sync providers.
|
* Base class for WebDAV-based sync providers.
|
||||||
* Provides common functionality for uploading, downloading, and managing files via WebDAV.
|
* Provides common functionality for uploading, downloading, and managing files via WebDAV.
|
||||||
*/
|
*/
|
||||||
export abstract class WebdavBaseProvider<T extends SyncProviderId.WebDAV>
|
export abstract class WebdavBaseProvider<
|
||||||
implements SyncProviderServiceInterface<T>
|
T extends SyncProviderId.WebDAV,
|
||||||
{
|
> implements SyncProviderServiceInterface<T> {
|
||||||
abstract readonly id: T;
|
abstract readonly id: T;
|
||||||
readonly isUploadForcePossible = false;
|
readonly isUploadForcePossible = false;
|
||||||
readonly maxConcurrentRequests = 10;
|
readonly maxConcurrentRequests = 10;
|
||||||
|
|
|
||||||
|
|
@ -1157,9 +1157,8 @@ describe('SuperSyncProvider', () => {
|
||||||
jsonPayload: string,
|
jsonPayload: string,
|
||||||
): Promise<{ base64Gzip: string; headers: Record<string, string>; url: string }> {
|
): Promise<{ base64Gzip: string; headers: Record<string, string>; url: string }> {
|
||||||
// Instead of actually calling CapacitorHttp, return what would be sent
|
// Instead of actually calling CapacitorHttp, return what would be sent
|
||||||
const { compressWithGzipToString } = await import(
|
const { compressWithGzipToString } =
|
||||||
'../../encryption/compression-handler'
|
await import('../../encryption/compression-handler');
|
||||||
);
|
|
||||||
const base64Gzip = await compressWithGzipToString(jsonPayload);
|
const base64Gzip = await compressWithGzipToString(jsonPayload);
|
||||||
const baseUrl = cfg.baseUrl.replace(/\/$/, '');
|
const baseUrl = cfg.baseUrl.replace(/\/$/, '');
|
||||||
const url = `${baseUrl}${path}`;
|
const url = `${baseUrl}${path}`;
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ describe('PluginBridgeService.setCounter()', () => {
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalled();
|
expect(store.dispatch).toHaveBeenCalled();
|
||||||
const call = store.dispatch.calls.mostRecent();
|
const call = store.dispatch.calls.mostRecent();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const action = call.args[0] as any;
|
const action = call.args[0] as any;
|
||||||
expect(action.type).toBe('[SimpleCounter] Upsert SimpleCounter');
|
expect(action.type).toBe('[SimpleCounter] Upsert SimpleCounter');
|
||||||
expect(action.simpleCounter.id).toBe('new-counter');
|
expect(action.simpleCounter.id).toBe('new-counter');
|
||||||
|
|
@ -166,7 +166,7 @@ describe('PluginBridgeService.setCounter()', () => {
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalled();
|
expect(store.dispatch).toHaveBeenCalled();
|
||||||
const call = store.dispatch.calls.mostRecent();
|
const call = store.dispatch.calls.mostRecent();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const action = call.args[0] as any;
|
const action = call.args[0] as any;
|
||||||
expect(action.type).toBe('[SimpleCounter] Upsert SimpleCounter');
|
expect(action.type).toBe('[SimpleCounter] Upsert SimpleCounter');
|
||||||
expect(action.simpleCounter.id).toBe('zero-counter');
|
expect(action.simpleCounter.id).toBe('zero-counter');
|
||||||
|
|
@ -177,7 +177,7 @@ describe('PluginBridgeService.setCounter()', () => {
|
||||||
await service.setCounter('full-counter', 5);
|
await service.setCounter('full-counter', 5);
|
||||||
|
|
||||||
const call = store.dispatch.calls.mostRecent();
|
const call = store.dispatch.calls.mostRecent();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const action = call.args[0] as any;
|
const action = call.args[0] as any;
|
||||||
const counter = action.simpleCounter;
|
const counter = action.simpleCounter;
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ describe('PluginBridgeService.setCounter()', () => {
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalled();
|
expect(store.dispatch).toHaveBeenCalled();
|
||||||
const call = store.dispatch.calls.mostRecent();
|
const call = store.dispatch.calls.mostRecent();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const action = call.args[0] as any;
|
const action = call.args[0] as any;
|
||||||
expect(action.type).toBe('[SimpleCounter] Update SimpleCounter');
|
expect(action.type).toBe('[SimpleCounter] Update SimpleCounter');
|
||||||
expect(action.simpleCounter.id).toBe('existing-counter');
|
expect(action.simpleCounter.id).toBe('existing-counter');
|
||||||
|
|
@ -223,7 +223,7 @@ describe('PluginBridgeService.setCounter()', () => {
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalled();
|
expect(store.dispatch).toHaveBeenCalled();
|
||||||
const call = store.dispatch.calls.mostRecent();
|
const call = store.dispatch.calls.mostRecent();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const action = call.args[0] as any;
|
const action = call.args[0] as any;
|
||||||
const countOnDay = action.simpleCounter.changes.countOnDay;
|
const countOnDay = action.simpleCounter.changes.countOnDay;
|
||||||
expect(countOnDay['2024-01-01']).toBe(5);
|
expect(countOnDay['2024-01-01']).toBe(5);
|
||||||
|
|
@ -241,7 +241,7 @@ describe('PluginBridgeService.setCounter()', () => {
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalled();
|
expect(store.dispatch).toHaveBeenCalled();
|
||||||
const call = store.dispatch.calls.mostRecent();
|
const call = store.dispatch.calls.mostRecent();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const action = call.args[0] as any;
|
const action = call.args[0] as any;
|
||||||
expect(action.simpleCounter.changes.countOnDay[today]).toBe(100);
|
expect(action.simpleCounter.changes.countOnDay[today]).toBe(100);
|
||||||
});
|
});
|
||||||
|
|
@ -253,7 +253,7 @@ describe('PluginBridgeService.setCounter()', () => {
|
||||||
await service.setCounter('check-action', 1);
|
await service.setCounter('check-action', 1);
|
||||||
|
|
||||||
const call = store.dispatch.calls.mostRecent();
|
const call = store.dispatch.calls.mostRecent();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const action = call.args[0] as any;
|
const action = call.args[0] as any;
|
||||||
expect(action.type).toBe('[SimpleCounter] Update SimpleCounter');
|
expect(action.type).toBe('[SimpleCounter] Update SimpleCounter');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// TODO: Fix plugin tests after stabilizing task model changes
|
// TODO: Fix plugin tests after stabilizing task model changes
|
||||||
/* eslint-disable */
|
|
||||||
/*
|
/*
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// TODO: Fix plugin tests after stabilizing task model changes
|
// TODO: Fix plugin tests after stabilizing task model changes
|
||||||
/* eslint-disable */
|
|
||||||
// TODO: These tests are disabled due to module resolution issues with @super-productivity/plugin-api
|
// TODO: These tests are disabled due to module resolution issues with @super-productivity/plugin-api
|
||||||
describe('PluginService', () => {
|
describe('PluginService', () => {
|
||||||
it('should pass placeholder test', () => {
|
it('should pass placeholder test', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { plannerSharedMetaReducer } from './planner-shared.reducer';
|
import { plannerSharedMetaReducer } from './planner-shared.reducer';
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
import { Task } from '../../../features/tasks/task.model';
|
import { Task } from '../../../features/tasks/task.model';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
|
||||||
import { tagSharedMetaReducer } from './tag-shared.reducer';
|
import { tagSharedMetaReducer } from './tag-shared.reducer';
|
||||||
import { TaskSharedActions } from '../task-shared.actions';
|
import { TaskSharedActions } from '../task-shared.actions';
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { taskBatchUpdateMetaReducer } from './task-batch-update.reducer';
|
import { taskBatchUpdateMetaReducer } from './task-batch-update.reducer';
|
||||||
import { TaskSharedActions } from '../task-shared.actions';
|
import { TaskSharedActions } from '../task-shared.actions';
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
|
||||||
import { taskBatchUpdateMetaReducer } from './task-batch-update.reducer';
|
import { taskBatchUpdateMetaReducer } from './task-batch-update.reducer';
|
||||||
import { TaskSharedActions } from '../task-shared.actions';
|
import { TaskSharedActions } from '../task-shared.actions';
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { taskBatchUpdateMetaReducer } from './task-batch-update.reducer';
|
import { taskBatchUpdateMetaReducer } from './task-batch-update.reducer';
|
||||||
import { TaskSharedActions } from '../task-shared.actions';
|
import { TaskSharedActions } from '../task-shared.actions';
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { taskSharedLifecycleMetaReducer } from './task-shared-lifecycle.reducer';
|
import { taskSharedLifecycleMetaReducer } from './task-shared-lifecycle.reducer';
|
||||||
import { TaskSharedActions } from '../task-shared.actions';
|
import { TaskSharedActions } from '../task-shared.actions';
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
import { TASK_FEATURE_NAME } from '../../../features/tasks/store/task.reducer';
|
import { TASK_FEATURE_NAME } from '../../../features/tasks/store/task.reducer';
|
||||||
import { TAG_FEATURE_NAME } from '../../../features/tag/store/tag.reducer';
|
import { TAG_FEATURE_NAME } from '../../../features/tag/store/tag.reducer';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/naming-convention */
|
|
||||||
import { validateAndFixDataConsistencyAfterBatchUpdate } from './validate-and-fix-data-consistency-after-batch-update';
|
import { validateAndFixDataConsistencyAfterBatchUpdate } from './validate-and-fix-data-consistency-after-batch-update';
|
||||||
import { RootState } from '../../root-state';
|
import { RootState } from '../../root-state';
|
||||||
import { TASK_FEATURE_NAME } from '../../../features/tasks/store/task.reducer';
|
import { TASK_FEATURE_NAME } from '../../../features/tasks/store/task.reducer';
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ const observableProto = (Observable as unknown as { prototype: ObservablePrototy
|
||||||
.prototype;
|
.prototype;
|
||||||
|
|
||||||
if (!observableProto.toPromise) {
|
if (!observableProto.toPromise) {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
||||||
observableProto.toPromise = function <T>(this: Observable<T>): Promise<T> {
|
observableProto.toPromise = function <T>(this: Observable<T>): Promise<T> {
|
||||||
return lastValueFrom(this);
|
return lastValueFrom(this);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export class EnlargeImgDirective {
|
||||||
this.imageEl = this._el.nativeElement;
|
this.imageEl = this._el.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('click', ['$event']) onClick(): void {
|
@HostListener('click') onClick(): void {
|
||||||
this.isImg = this.imageEl.tagName.toLowerCase() === 'img';
|
this.isImg = this.imageEl.tagName.toLowerCase() === 'img';
|
||||||
|
|
||||||
if (this.isImg || this.enlargeImg()) {
|
if (this.isImg || this.enlargeImg()) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { KeyboardInputComponent } from '../features/config/keyboard-input/keyboa
|
||||||
import { IconInputComponent } from '../features/config/icon-input/icon-input.component';
|
import { IconInputComponent } from '../features/config/icon-input/icon-input.component';
|
||||||
import { SelectProjectComponent } from '../features/config/select-project/select-project.component';
|
import { SelectProjectComponent } from '../features/config/select-project/select-project.component';
|
||||||
import { RepeatSectionTypeComponent } from '../features/config/repeat-section-type/repeat-section-type.component';
|
import { RepeatSectionTypeComponent } from '../features/config/repeat-section-type/repeat-section-type.component';
|
||||||
import { FormlyMatSliderModule } from '@ngx-formly/material/slider';
|
import { FormlySliderComponent } from './formly-slider/formly-slider.component';
|
||||||
import { FormlyTagSelectionComponent } from './formly-tag-selection/formly-tag-selection.component';
|
import { FormlyTagSelectionComponent } from './formly-tag-selection/formly-tag-selection.component';
|
||||||
import { FormlyBtnComponent } from './formly-button/formly-btn.component';
|
import { FormlyBtnComponent } from './formly-button/formly-btn.component';
|
||||||
import { FormlyImageInputComponent } from './formly-image-input/formly-image-input.component';
|
import { FormlyImageInputComponent } from './formly-image-input/formly-image-input.component';
|
||||||
|
|
@ -28,7 +28,7 @@ import { ColorInputComponent } from '../features/config/color-input/color-input.
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
FormlyMatSliderModule,
|
FormlySliderComponent,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormlyModule.forRoot({
|
FormlyModule.forRoot({
|
||||||
validationMessages: [
|
validationMessages: [
|
||||||
|
|
@ -40,6 +40,7 @@ import { ColorInputComponent } from '../features/config/color-input/color-input.
|
||||||
{ name: 'maxLength', message: 'Value is too long' },
|
{ name: 'maxLength', message: 'Value is too long' },
|
||||||
],
|
],
|
||||||
types: [
|
types: [
|
||||||
|
{ name: 'slider', component: FormlySliderComponent, wrappers: ['form-field'] },
|
||||||
{ name: 'link', component: FormlyLinkWidgetComponent },
|
{ name: 'link', component: FormlyLinkWidgetComponent },
|
||||||
{
|
{
|
||||||
name: 'duration',
|
name: 'duration',
|
||||||
|
|
|
||||||
55
src/app/ui/formly-slider/formly-slider.component.ts
Normal file
55
src/app/ui/formly-slider/formly-slider.component.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
||||||
|
import { MatSliderModule } from '@angular/material/slider';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { FormlyFieldProps } from '@ngx-formly/material/form-field';
|
||||||
|
|
||||||
|
interface SliderProps extends FormlyFieldProps {
|
||||||
|
displayWith?: (value: number) => string;
|
||||||
|
discrete?: boolean;
|
||||||
|
showTickMarks?: boolean;
|
||||||
|
thumbLabel?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'formly-field-mat-slider',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatSliderModule, ReactiveFormsModule],
|
||||||
|
template: `
|
||||||
|
<mat-slider
|
||||||
|
[min]="props.min ?? 0"
|
||||||
|
[max]="props.max ?? 100"
|
||||||
|
[step]="props.step ?? 1"
|
||||||
|
[discrete]="props.discrete ?? props.thumbLabel ?? true"
|
||||||
|
[showTickMarks]="props.showTickMarks ?? false"
|
||||||
|
[displayWith]="props.displayWith ?? defaultDisplayWith"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
matSliderThumb
|
||||||
|
[formControl]="formControl"
|
||||||
|
/>
|
||||||
|
</mat-slider>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
mat-slider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class FormlySliderComponent extends FieldType<FieldTypeConfig<SliderProps>> {
|
||||||
|
defaultDisplayWith = (value: number): string => `${value}`;
|
||||||
|
|
||||||
|
override defaultOptions = {
|
||||||
|
props: {
|
||||||
|
hideFieldUnderline: true,
|
||||||
|
floatLabel: 'always' as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -151,4 +151,434 @@ describe('InlineMarkdownComponent', () => {
|
||||||
expect(component.changed.emit).toHaveBeenCalledWith(changedValue);
|
expect(component.changed.emit).toHaveBeenCalledWith(changedValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('_handleCheckboxClick', () => {
|
||||||
|
let mockPreviewEl: { element: { nativeElement: HTMLElement } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPreviewEl = {
|
||||||
|
element: {
|
||||||
|
nativeElement: document.createElement('div'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
spyOn(component, 'previewEl').and.returnValue(mockPreviewEl as any);
|
||||||
|
spyOn(component.changed, 'emit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle first checkbox in simple checklist', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task 1\n- [ ] Task 2';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Create mock checkbox wrappers
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
wrapper1.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 1';
|
||||||
|
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
wrapper2.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 2';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
component['_handleCheckboxClick'](wrapper1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith('- [x] Task 1\n- [ ] Task 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle checkbox after blank line', () => {
|
||||||
|
// Arrange - this is the bug scenario from issue #5950
|
||||||
|
component.model = '- [ ] Task 1\n\n- [ ] Task 2';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Create mock checkbox wrappers (blank line doesn't create a wrapper)
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
wrapper1.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 1';
|
||||||
|
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
wrapper2.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 2';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
|
||||||
|
// Act - click the second checkbox (Task 2)
|
||||||
|
component['_handleCheckboxClick'](wrapper2);
|
||||||
|
|
||||||
|
// Assert - Task 2 should be toggled, not Task 1
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith('- [ ] Task 1\n\n- [x] Task 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle checkbox with multiple blank lines', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task 1\n\n\n- [ ] Task 2\n\n- [ ] Task 3';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
wrapper1.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 1';
|
||||||
|
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
wrapper2.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 2';
|
||||||
|
|
||||||
|
const wrapper3 = document.createElement('li');
|
||||||
|
wrapper3.className = 'checkbox-wrapper';
|
||||||
|
wrapper3.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 3';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper3);
|
||||||
|
|
||||||
|
// Act - click the third checkbox (Task 3)
|
||||||
|
component['_handleCheckboxClick'](wrapper3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith(
|
||||||
|
'- [ ] Task 1\n\n\n- [ ] Task 2\n\n- [x] Task 3',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should uncheck a checked checkbox', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [x] Task 1\n- [ ] Task 2';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
wrapper1.innerHTML = '<span class="checkbox material-icons">check_box</span>Task 1';
|
||||||
|
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
wrapper2.innerHTML =
|
||||||
|
'<span class="checkbox material-icons">check_box_outline_blank</span>Task 2';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
component['_handleCheckboxClick'](wrapper1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith('- [ ] Task 1\n- [ ] Task 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clickPreview', () => {
|
||||||
|
let mockPreviewEl: { element: { nativeElement: HTMLElement } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPreviewEl = {
|
||||||
|
element: {
|
||||||
|
nativeElement: document.createElement('div'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
spyOn(component, 'previewEl').and.returnValue(mockPreviewEl as any);
|
||||||
|
spyOn(component.changed, 'emit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle checkbox click when checkbox is wrapped in <p> tag (loose list)', () => {
|
||||||
|
// Arrange - simulates loose list HTML: <li class="checkbox-wrapper"><p><span class="checkbox">...</span>Task</p></li>
|
||||||
|
component.model = '- [ ] Task 1\n\n- [ ] Task 2';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Build DOM structure for loose list (with <p> wrapper)
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper undone';
|
||||||
|
const p1 = document.createElement('p');
|
||||||
|
const checkbox1 = document.createElement('span');
|
||||||
|
checkbox1.className = 'checkbox material-icons';
|
||||||
|
checkbox1.textContent = 'check_box_outline_blank';
|
||||||
|
p1.appendChild(checkbox1);
|
||||||
|
p1.appendChild(document.createTextNode('Task 1'));
|
||||||
|
wrapper1.appendChild(p1);
|
||||||
|
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper undone';
|
||||||
|
const p2 = document.createElement('p');
|
||||||
|
const checkbox2 = document.createElement('span');
|
||||||
|
checkbox2.className = 'checkbox material-icons';
|
||||||
|
checkbox2.textContent = 'check_box_outline_blank';
|
||||||
|
p2.appendChild(checkbox2);
|
||||||
|
p2.appendChild(document.createTextNode('Task 2'));
|
||||||
|
wrapper2.appendChild(p2);
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
|
||||||
|
// Act - simulate clicking the second checkbox
|
||||||
|
const mockEvent = {
|
||||||
|
target: checkbox2,
|
||||||
|
} as unknown as MouseEvent;
|
||||||
|
component.clickPreview(mockEvent);
|
||||||
|
|
||||||
|
// Assert - Task 2 should be toggled
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith('- [ ] Task 1\n\n- [x] Task 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle checkbox when clicking on the label text (not just the checkbox icon)', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task 1\n- [ ] Task 2';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Build DOM structure
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper undone';
|
||||||
|
const checkbox1 = document.createElement('span');
|
||||||
|
checkbox1.className = 'checkbox material-icons';
|
||||||
|
checkbox1.textContent = 'check_box_outline_blank';
|
||||||
|
const textNode1 = document.createTextNode('Task 1');
|
||||||
|
wrapper1.appendChild(checkbox1);
|
||||||
|
wrapper1.appendChild(textNode1);
|
||||||
|
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper undone';
|
||||||
|
const checkbox2 = document.createElement('span');
|
||||||
|
checkbox2.className = 'checkbox material-icons';
|
||||||
|
checkbox2.textContent = 'check_box_outline_blank';
|
||||||
|
const textSpan2 = document.createElement('span');
|
||||||
|
textSpan2.textContent = 'Task 2';
|
||||||
|
wrapper2.appendChild(checkbox2);
|
||||||
|
wrapper2.appendChild(textSpan2);
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
|
||||||
|
// Act - simulate clicking on the text span (not the checkbox icon)
|
||||||
|
const mockEvent = {
|
||||||
|
target: textSpan2,
|
||||||
|
} as unknown as MouseEvent;
|
||||||
|
component.clickPreview(mockEvent);
|
||||||
|
|
||||||
|
// Assert - Task 2 should be toggled
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith('- [ ] Task 1\n- [x] Task 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle checkbox when clicking directly on the checkbox-wrapper element', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task 1';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper undone';
|
||||||
|
const checkbox1 = document.createElement('span');
|
||||||
|
checkbox1.className = 'checkbox material-icons';
|
||||||
|
checkbox1.textContent = 'check_box_outline_blank';
|
||||||
|
wrapper1.appendChild(checkbox1);
|
||||||
|
wrapper1.appendChild(document.createTextNode('Task 1'));
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
|
||||||
|
// Act - simulate clicking directly on the wrapper
|
||||||
|
const mockEvent = {
|
||||||
|
target: wrapper1,
|
||||||
|
} as unknown as MouseEvent;
|
||||||
|
component.clickPreview(mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith('- [x] Task 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not toggle checkbox when clicking on a link', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task with [link](http://example.com)';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper undone';
|
||||||
|
const checkbox1 = document.createElement('span');
|
||||||
|
checkbox1.className = 'checkbox material-icons';
|
||||||
|
checkbox1.textContent = 'check_box_outline_blank';
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = 'http://example.com';
|
||||||
|
link.textContent = 'link';
|
||||||
|
wrapper1.appendChild(checkbox1);
|
||||||
|
wrapper1.appendChild(document.createTextNode('Task with '));
|
||||||
|
wrapper1.appendChild(link);
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
|
||||||
|
// Act - simulate clicking on the link
|
||||||
|
const mockEvent = {
|
||||||
|
target: link,
|
||||||
|
} as unknown as MouseEvent;
|
||||||
|
component.clickPreview(mockEvent);
|
||||||
|
|
||||||
|
// Assert - checkbox should NOT be toggled (link should work normally)
|
||||||
|
expect(component.changed.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle edit mode when clicking outside checkbox-wrapper', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = 'Some regular text';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const paragraph = document.createElement('p');
|
||||||
|
paragraph.textContent = 'Some regular text';
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(paragraph);
|
||||||
|
|
||||||
|
spyOn<any>(component, '_toggleShowEdit');
|
||||||
|
|
||||||
|
// Act - simulate clicking on regular text
|
||||||
|
const mockEvent = {
|
||||||
|
target: paragraph,
|
||||||
|
} as unknown as MouseEvent;
|
||||||
|
component.clickPreview(mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component['_toggleShowEdit']).toHaveBeenCalled();
|
||||||
|
expect(component.changed.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_handleCheckboxClick edge cases', () => {
|
||||||
|
let mockPreviewEl: { element: { nativeElement: HTMLElement } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPreviewEl = {
|
||||||
|
element: {
|
||||||
|
nativeElement: document.createElement('div'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
spyOn(component, 'previewEl').and.returnValue(mockPreviewEl as any);
|
||||||
|
spyOn(component.changed, 'emit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve blank lines when toggling checkboxes', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task 1\n\n- [ ] Task 2\n\n- [ ] Task 3';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
const wrapper3 = document.createElement('li');
|
||||||
|
wrapper3.className = 'checkbox-wrapper';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper3);
|
||||||
|
|
||||||
|
// Act - toggle Task 2
|
||||||
|
component['_handleCheckboxClick'](wrapper2);
|
||||||
|
|
||||||
|
// Assert - blank lines should be preserved
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith(
|
||||||
|
'- [ ] Task 1\n\n- [x] Task 2\n\n- [ ] Task 3',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed checked and unchecked items', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [x] Done\n- [ ] Todo\n- [x] Also Done';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
const wrapper3 = document.createElement('li');
|
||||||
|
wrapper3.className = 'checkbox-wrapper';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper3);
|
||||||
|
|
||||||
|
// Act - toggle the middle item (Todo -> Done)
|
||||||
|
component['_handleCheckboxClick'](wrapper2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith(
|
||||||
|
'- [x] Done\n- [x] Todo\n- [x] Also Done',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle checklist with text before it', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = 'Some intro text\n\n- [ ] Task 1\n- [ ] Task 2';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
component['_handleCheckboxClick'](wrapper1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith(
|
||||||
|
'Some intro text\n\n- [x] Task 1\n- [ ] Task 2',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle checklist with text after it', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task 1\n- [ ] Task 2\n\nSome outro text';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
const wrapper2 = document.createElement('li');
|
||||||
|
wrapper2.className = 'checkbox-wrapper';
|
||||||
|
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
component['_handleCheckboxClick'](wrapper2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).toHaveBeenCalledWith(
|
||||||
|
'- [ ] Task 1\n- [x] Task 2\n\nSome outro text',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not emit if model is undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = undefined;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const wrapper1 = document.createElement('li');
|
||||||
|
wrapper1.className = 'checkbox-wrapper';
|
||||||
|
mockPreviewEl.element.nativeElement.appendChild(wrapper1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
component['_handleCheckboxClick'](wrapper1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not emit if clicked element is not found in DOM', () => {
|
||||||
|
// Arrange
|
||||||
|
component.model = '- [ ] Task 1';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Create a wrapper that's NOT in the previewEl
|
||||||
|
const orphanWrapper = document.createElement('li');
|
||||||
|
orphanWrapper.className = 'checkbox-wrapper';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
component['_handleCheckboxClick'](orphanWrapper);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.changed.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -150,15 +150,16 @@ export class InlineMarkdownComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
clickPreview($event: MouseEvent): void {
|
clickPreview($event: MouseEvent): void {
|
||||||
if (($event.target as HTMLElement).tagName === 'A') {
|
if (($event.target as HTMLElement).tagName === 'A') {
|
||||||
// } else if (($event.target as HTMLElement).classList.contains('checkbox-wrapper')) {
|
// Let links work normally
|
||||||
// this._handleCheckboxClick($event.target as HTMLElement);
|
return;
|
||||||
} else if (
|
}
|
||||||
$event?.target &&
|
|
||||||
($event.target as HTMLElement).classList.contains('checkbox')
|
// Check if click is anywhere inside a checkbox-wrapper (text or checkbox icon)
|
||||||
) {
|
const wrapper = ($event.target as HTMLElement).closest(
|
||||||
this._handleCheckboxClick(
|
'.checkbox-wrapper',
|
||||||
($event.target as HTMLElement).parentElement as HTMLElement,
|
) as HTMLElement;
|
||||||
);
|
if (wrapper) {
|
||||||
|
this._handleCheckboxClick(wrapper);
|
||||||
} else {
|
} else {
|
||||||
this._toggleShowEdit();
|
this._toggleShowEdit();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
125
src/app/ui/marked-options-factory.spec.ts
Normal file
125
src/app/ui/marked-options-factory.spec.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { markedOptionsFactory } from './marked-options-factory';
|
||||||
|
|
||||||
|
describe('markedOptionsFactory', () => {
|
||||||
|
let options: ReturnType<typeof markedOptionsFactory>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
options = markedOptionsFactory();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a MarkedOptions object', () => {
|
||||||
|
expect(options).toBeDefined();
|
||||||
|
expect(options.renderer).toBeDefined();
|
||||||
|
expect(options.gfm).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkbox renderer', () => {
|
||||||
|
it('should render unchecked checkbox with material icon', () => {
|
||||||
|
const result = options.renderer!.checkbox({ checked: false } as any);
|
||||||
|
expect(result).toContain('check_box_outline_blank');
|
||||||
|
expect(result).toContain('class="checkbox material-icons"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render checked checkbox with material icon', () => {
|
||||||
|
const result = options.renderer!.checkbox({ checked: true } as any);
|
||||||
|
expect(result).toContain('check_box');
|
||||||
|
expect(result).not.toContain('check_box_outline_blank');
|
||||||
|
expect(result).toContain('class="checkbox material-icons"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listitem renderer', () => {
|
||||||
|
it('should render regular list item without checkbox', () => {
|
||||||
|
const result = options.renderer!.listitem({
|
||||||
|
text: 'Regular item',
|
||||||
|
task: false,
|
||||||
|
checked: undefined,
|
||||||
|
} as any);
|
||||||
|
expect(result).toBe('<li>Regular item</li>');
|
||||||
|
expect(result).not.toContain('checkbox-wrapper');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render unchecked task list item with checkbox', () => {
|
||||||
|
const result = options.renderer!.listitem({
|
||||||
|
text: 'Task item',
|
||||||
|
task: true,
|
||||||
|
checked: false,
|
||||||
|
} as any);
|
||||||
|
expect(result).toContain('checkbox-wrapper');
|
||||||
|
expect(result).toContain('undone');
|
||||||
|
expect(result).toContain('check_box_outline_blank');
|
||||||
|
expect(result).toContain('Task item');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render checked task list item with checkbox', () => {
|
||||||
|
const result = options.renderer!.listitem({
|
||||||
|
text: 'Completed task',
|
||||||
|
task: true,
|
||||||
|
checked: true,
|
||||||
|
} as any);
|
||||||
|
expect(result).toContain('checkbox-wrapper');
|
||||||
|
expect(result).toContain('done');
|
||||||
|
expect(result).not.toContain('undone');
|
||||||
|
expect(result).toContain('check_box');
|
||||||
|
expect(result).not.toContain('check_box_outline_blank');
|
||||||
|
expect(result).toContain('Completed task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined checked value as unchecked', () => {
|
||||||
|
const result = options.renderer!.listitem({
|
||||||
|
text: 'Task with undefined checked',
|
||||||
|
task: true,
|
||||||
|
checked: undefined,
|
||||||
|
} as any);
|
||||||
|
expect(result).toContain('checkbox-wrapper');
|
||||||
|
expect(result).toContain('undone');
|
||||||
|
expect(result).toContain('check_box_outline_blank');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('link renderer', () => {
|
||||||
|
it('should render link with target="_blank"', () => {
|
||||||
|
const result = options.renderer!.link({
|
||||||
|
href: 'http://example.com',
|
||||||
|
title: 'Example',
|
||||||
|
text: 'Click here',
|
||||||
|
} as any);
|
||||||
|
expect(result).toContain('target="_blank"');
|
||||||
|
expect(result).toContain('href="http://example.com"');
|
||||||
|
expect(result).toContain('title="Example"');
|
||||||
|
expect(result).toContain('Click here');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty title', () => {
|
||||||
|
const result = options.renderer!.link({
|
||||||
|
href: 'http://example.com',
|
||||||
|
title: null,
|
||||||
|
text: 'Link',
|
||||||
|
} as any);
|
||||||
|
expect(result).toContain('title=""');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('text renderer with URL auto-linking', () => {
|
||||||
|
it('should auto-link URLs in text', () => {
|
||||||
|
const result = options.renderer!.text({
|
||||||
|
text: 'Check out http://example.com for more info',
|
||||||
|
type: 'text',
|
||||||
|
raw: 'Check out http://example.com for more info',
|
||||||
|
} as any);
|
||||||
|
// The text renderer modifies the token and passes it to the original renderer
|
||||||
|
// which may HTML-escape the content, so we check for the href pattern
|
||||||
|
expect(result).toContain('http://example.com');
|
||||||
|
expect(result).toContain('href=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle text without URLs', () => {
|
||||||
|
const result = options.renderer!.text({
|
||||||
|
text: 'Regular text without links',
|
||||||
|
type: 'text',
|
||||||
|
raw: 'Regular text without links',
|
||||||
|
} as any);
|
||||||
|
expect(result).not.toContain('href=');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3,18 +3,31 @@ import { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
|
||||||
export const markedOptionsFactory = (): MarkedOptions => {
|
export const markedOptionsFactory = (): MarkedOptions => {
|
||||||
const renderer = new MarkedRenderer();
|
const renderer = new MarkedRenderer();
|
||||||
|
|
||||||
renderer.checkbox = (isChecked: boolean) =>
|
renderer.checkbox = ({ checked }: { checked: boolean }) =>
|
||||||
`<span class="checkbox material-icons">${isChecked ? 'check_box' : 'check_box_outline_blank'}</span>`;
|
`<span class="checkbox material-icons">${checked ? 'check_box' : 'check_box_outline_blank'}</span>`;
|
||||||
|
|
||||||
renderer.listitem = (text: string) =>
|
renderer.listitem = ({
|
||||||
text.includes('checkbox')
|
text,
|
||||||
? `<li class="checkbox-wrapper ${text.includes('check_box_outline_blank') ? 'undone' : 'done'}">${text}</li>`
|
task,
|
||||||
: `<li>${text}</li>`;
|
checked,
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
task: boolean;
|
||||||
|
checked?: boolean;
|
||||||
|
}) => {
|
||||||
|
// In marked v17, task list items need to manually prepend the checkbox
|
||||||
|
if (task) {
|
||||||
|
const isChecked = checked === true;
|
||||||
|
const checkboxHtml = `<span class="checkbox material-icons">${isChecked ? 'check_box' : 'check_box_outline_blank'}</span>`;
|
||||||
|
return `<li class="checkbox-wrapper ${isChecked ? 'done' : 'undone'}">${checkboxHtml}${text}</li>`;
|
||||||
|
}
|
||||||
|
return `<li>${text}</li>`;
|
||||||
|
};
|
||||||
|
|
||||||
renderer.link = (href, title, text) =>
|
renderer.link = ({ href, title, text }) =>
|
||||||
`<a target="_blank" href="${href}" title="${title}">${text}</a>`;
|
`<a target="_blank" href="${href}" title="${title || ''}">${text}</a>`;
|
||||||
|
|
||||||
renderer.paragraph = (text) => {
|
renderer.paragraph = ({ text }) => {
|
||||||
const split = text.split('\n');
|
const split = text.split('\n');
|
||||||
return split.reduce((acc, p, i) => {
|
return split.reduce((acc, p, i) => {
|
||||||
const result = /h(\d)\./.exec(p);
|
const result = /h(\d)\./.exec(p);
|
||||||
|
|
@ -35,13 +48,15 @@ export const markedOptionsFactory = (): MarkedOptions => {
|
||||||
const urlPattern =
|
const urlPattern =
|
||||||
/\b((([A-Za-z][A-Za-z0-9+.-]*):\/\/([^\/?#]*))([^?#]*)(\?([^#]*))?(#(.*))?)\b/gi;
|
/\b((([A-Za-z][A-Za-z0-9+.-]*):\/\/([^\/?#]*))([^?#]*)(\?([^#]*))?(#(.*))?)\b/gi;
|
||||||
|
|
||||||
const rendererTxtOld = renderer.text;
|
const rendererTxtOld = renderer.text.bind(renderer);
|
||||||
renderer.text = (text) => {
|
renderer.text = (token) => {
|
||||||
return rendererTxtOld(
|
const modifiedToken = {
|
||||||
text.replace(urlPattern, (url) => {
|
...token,
|
||||||
|
text: token.text.replace(urlPattern, (url) => {
|
||||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||||
}),
|
}),
|
||||||
);
|
};
|
||||||
|
return rendererTxtOld(modifiedToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ import { Log } from '../../core/log';
|
||||||
>
|
>
|
||||||
@for (item of items; track item; let i = $index) {
|
@for (item of items; track item; let i = $index) {
|
||||||
<li
|
<li
|
||||||
[class.active]="activeIndex == i"
|
[class.active]="activeIndex === i"
|
||||||
[class.mention-active]="!styleOff && activeIndex == i"
|
[class.mention-active]="!styleOff && activeIndex === i"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ const audioBufferCache = new Map<string, AudioBuffer>();
|
||||||
*/
|
*/
|
||||||
export const getAudioContext = (): AudioContext => {
|
export const getAudioContext = (): AudioContext => {
|
||||||
if (!audioContext) {
|
if (!audioContext) {
|
||||||
audioContext = new ((window as any).AudioContext ||
|
audioContext = new (
|
||||||
(window as any).webkitAudioContext)();
|
(window as any).AudioContext || (window as any).webkitAudioContext
|
||||||
|
)();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume if suspended (can happen due to browser autoplay policies)
|
// Resume if suspended (can happen due to browser autoplay policies)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { Directory, Encoding, Filesystem, WriteFileResult } from '@capacitor/filesystem';
|
import { Directory, Encoding, Filesystem, WriteFileResult } from '@capacitor/filesystem';
|
||||||
import { Share } from '@capacitor/share';
|
import { Share } from '@capacitor/share';
|
||||||
import { IS_ANDROID_WEB_VIEW } from './is-android-web-view';
|
import { IS_ANDROID_WEB_VIEW } from './is-android-web-view';
|
||||||
|
|
@ -54,7 +53,14 @@ export const download = async (
|
||||||
return { isSnap: true };
|
return { isSnap: true };
|
||||||
} else {
|
} else {
|
||||||
const blob = new Blob([stringData], { type: 'text/plain;charset=utf-8' });
|
const blob = new Blob([stringData], { type: 'text/plain;charset=utf-8' });
|
||||||
saveAs(blob, filename);
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue