Browse Source

Upgrade (#1284)

* T

* started with test update

* Upgrade angular.

* Fix eslint

* Update node.

* Update Dockerfile

* Fix tour.

* Small change.
pull/1285/head
Sebastian Stehle 6 months ago
committed by GitHub
parent
commit
a444bb6a00
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      Dockerfile
  2. 2
      dev/frontend-build/Dockerfile
  3. 163
      frontend/.eslintrc.js
  4. 2
      frontend/.storybook/main.js
  5. 39
      frontend/angular.json
  6. 23838
      frontend/cache.json
  7. 159
      frontend/eslint.config.js
  8. 56
      frontend/karma.conf.js
  9. 18519
      frontend/package-lock.json
  10. 136
      frontend/package.json
  11. 2
      frontend/src/app/features/administration/guards/user-must-exist.guard.spec.ts
  12. 171
      frontend/src/app/features/administration/services/event-consumers.service.spec.ts
  13. 330
      frontend/src/app/features/administration/services/users.service.spec.ts
  14. 4
      frontend/src/app/features/administration/state/event-consumers.state.spec.ts
  15. 6
      frontend/src/app/features/administration/state/users.state.spec.ts
  16. 27
      frontend/src/app/features/apps/pages/app.component.html
  17. 6
      frontend/src/app/features/apps/pages/app.component.scss
  18. 14
      frontend/src/app/features/apps/pages/app.component.ts
  19. 18
      frontend/src/app/features/apps/pages/onboarding-dialog.component.html
  20. 5
      frontend/src/app/features/apps/pages/onboarding-dialog.component.ts
  21. 6
      frontend/src/app/features/content/shared/forms/content-field.component.html
  22. 7
      frontend/src/app/features/content/shared/forms/content-field.component.scss
  23. 2
      frontend/src/app/features/content/shared/forms/iframe-editor.component.ts
  24. 18
      frontend/src/app/features/dashboard/pages/dashboard-config.component.ts
  25. 6
      frontend/src/app/features/dashboard/pages/dashboard-page.component.html
  26. 19
      frontend/src/app/features/dashboard/pages/dashboard-page.component.ts
  27. 2
      frontend/src/app/features/settings/pages/roles/roles-page.component.ts
  28. 4
      frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html
  29. 21
      frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts
  30. 123
      frontend/src/app/features/teams/services/team-contributors.service.spec.ts
  31. 4
      frontend/src/app/features/teams/services/team-contributors.service.ts
  32. 160
      frontend/src/app/features/teams/services/team-plans.service.spec.ts
  33. 4
      frontend/src/app/features/teams/services/team-plans.service.ts
  34. 2
      frontend/src/app/features/teams/state/team-auth.forms.ts
  35. 9
      frontend/src/app/features/teams/state/team-auth.state.spec.ts
  36. 4
      frontend/src/app/features/teams/state/team-auth.state.ts
  37. 1
      frontend/src/app/features/teams/state/team-contributors.forms.ts
  38. 31
      frontend/src/app/features/teams/state/team-contributors.state.spec.ts
  39. 4
      frontend/src/app/features/teams/state/team-contributors.state.ts
  40. 10
      frontend/src/app/features/teams/state/team-plans.state.spec.ts
  41. 4
      frontend/src/app/features/teams/state/team-plans.state.ts
  42. 96
      frontend/src/app/framework/angular/animations.ts
  43. 2
      frontend/src/app/framework/angular/autosize.directive.ts
  44. 2
      frontend/src/app/framework/angular/avatar.stories.ts
  45. 11
      frontend/src/app/framework/angular/dropdown-menu.component.ts
  46. 14
      frontend/src/app/framework/angular/forms/confirm-click.directive.spec.ts
  47. 1
      frontend/src/app/framework/angular/forms/confirm-click.directive.ts
  48. 2
      frontend/src/app/framework/angular/forms/control-errors-messages.component.html
  49. 4
      frontend/src/app/framework/angular/forms/control-errors-messages.component.ts
  50. 3
      frontend/src/app/framework/angular/forms/copy-global.directive.ts
  51. 3
      frontend/src/app/framework/angular/forms/copy.directive.ts
  52. 2
      frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts
  53. 2
      frontend/src/app/framework/angular/forms/editors/code-editor.component.ts
  54. 2
      frontend/src/app/framework/angular/forms/editors/code-editor.stories.ts
  55. 2
      frontend/src/app/framework/angular/forms/editors/date-time-editor.component.ts
  56. 10
      frontend/src/app/framework/angular/forms/editors/dropdown.stories.ts
  57. 6
      frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts
  58. 30
      frontend/src/app/framework/angular/forms/extended-form-array.spec.ts
  59. 30
      frontend/src/app/framework/angular/forms/extended-form-group.spec.ts
  60. 22
      frontend/src/app/framework/angular/forms/file-drop.directive.ts
  61. 8
      frontend/src/app/framework/angular/forms/focus-on-init.directive.spec.ts
  62. 61
      frontend/src/app/framework/angular/forms/templated-form-array.spec.ts
  63. 5
      frontend/src/app/framework/angular/forms/templated-form-group.spec.ts
  64. 12
      frontend/src/app/framework/angular/forms/transform-input.directive.ts
  65. 2
      frontend/src/app/framework/angular/forms/validators.ts
  66. 4
      frontend/src/app/framework/angular/http/http-extensions.ts
  67. 2
      frontend/src/app/framework/angular/layout-container.directive.ts
  68. 2
      frontend/src/app/framework/angular/loader.component.ts
  69. 5
      frontend/src/app/framework/angular/long-hover.directive.stories.ts
  70. 6
      frontend/src/app/framework/angular/markdown.directive.spec.ts
  71. 2
      frontend/src/app/framework/angular/menu.stories.ts
  72. 2
      frontend/src/app/framework/angular/modals/dialog-renderer.component.html
  73. 5
      frontend/src/app/framework/angular/modals/dialog-renderer.component.ts
  74. 7
      frontend/src/app/framework/angular/modals/dialog-renderer.stories.ts
  75. 2
      frontend/src/app/framework/angular/modals/modal-dialog.component.html
  76. 4
      frontend/src/app/framework/angular/modals/modal-dialog.component.ts
  77. 2
      frontend/src/app/framework/angular/modals/modal-placement.directive.ts
  78. 2
      frontend/src/app/framework/angular/modals/modal.directive.ts
  79. 3
      frontend/src/app/framework/angular/modals/tooltip.directive.ts
  80. 2
      frontend/src/app/framework/angular/modals/tour-template.component.html
  81. 5
      frontend/src/app/framework/angular/modals/tour-template.component.ts
  82. 32
      frontend/src/app/framework/angular/pager.component.spec.ts
  83. 12
      frontend/src/app/framework/angular/pipes/date-time.pipes.spec.ts
  84. 1
      frontend/src/app/framework/angular/pipes/numbers.pipes.ts
  85. 4
      frontend/src/app/framework/angular/routers/router-2-state.spec.ts
  86. 4
      frontend/src/app/framework/angular/shortcut.component.spec.ts
  87. 4
      frontend/src/app/framework/angular/shortcut.directive.spec.ts
  88. 2
      frontend/src/app/framework/angular/stateful.component.ts
  89. 8
      frontend/src/app/framework/angular/title.component.spec.ts
  90. 1
      frontend/src/app/framework/internal.ts
  91. 2
      frontend/src/app/framework/services/dialog.service.ts
  92. 12
      frontend/src/app/framework/services/loading.service.spec.ts
  93. 6
      frontend/src/app/framework/services/local-store.service.ts
  94. 6
      frontend/src/app/framework/services/message-bus.service.spec.ts
  95. 18
      frontend/src/app/framework/services/toolbar.service.spec.ts
  96. 8
      frontend/src/app/framework/utils/array-extensions.spec.ts
  97. 2
      frontend/src/app/framework/utils/color-helper.spec.ts
  98. 2
      frontend/src/app/framework/utils/interpolator.spec.ts
  99. 2
      frontend/src/app/framework/utils/keys.ts
  100. 6
      frontend/src/app/framework/utils/markdown-transform.spec.ts

2
Dockerfile

@ -55,7 +55,7 @@ RUN dotnet tool install --tool-path /tools dotnet-dump \
#
# Stage 2, Build Frontend
#
FROM squidex/frontend-build:20.9 AS frontend
FROM squidex/frontend-build:22.19 AS frontend
WORKDIR /src

2
dev/frontend-build/Dockerfile

@ -1,4 +1,4 @@
FROM node:20.9.0-slim
FROM node:22.19.0-slim
ENV CHROME_BIN=/usr/bin/chromium

163
frontend/.eslintrc.js

@ -1,163 +0,0 @@
/* eslint-disable */
module.exports = {
"env": {
"browser": true,
"node": true
},
"extends": [
"airbnb-typescript/base",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json"
},
"plugins": [
"deprecation",
"eslint-plugin-import",
"@typescript-eslint"
],
"rules": {
"@angular-eslint/component-selector": [
"error",
{
"prefix": "sqx",
"style": "kebab-case",
"type": "element"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"prefix": "sqx",
"style": "camelCase",
"type": "attribute"
}
],
"@angular-eslint/use-lifecycle-interface": [
"off"
],
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/lines-between-class-members": "off",
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/naming-convention": [
"error",
{
"format": [
"camelCase",
"PascalCase",
"UPPER_CASE"
],
"leadingUnderscore": "allow",
"selector": "variable",
"trailingUnderscore": "allow"
},
{
"format": [
"PascalCase"
],
"selector": "typeLike"
}
],
"@typescript-eslint/no-shadow": "off",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/quotes": [
"error",
"single"
],
"@typescript-eslint/return-await": "off",
"@typescript-eslint/semi": [
"error",
"always"
],
"arrow-body-style": "off",
"arrow-parens": "off",
"class-methods-use-this": "off",
"default-case": "off",
"deprecation/deprecation": "warn",
"function-paren-newline": "off",
"implicit-arrow-linebreak": "off",
"import/extensions": "off",
"import/no-extraneous-dependencies": "off",
"import/no-useless-path-segments": "off",
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"pathGroups": [
{
"group": "external",
"pattern": "@app/**",
"position": "after"
}
],
"pathGroupsExcludedImportTypes": [
"builtin"
]
}
],
"import/prefer-default-export": "off",
"linebreak-style": "off",
"max-classes-per-file": "off",
"max-len": "off",
"newline-per-chained-call": "off",
"no-else-return": "off",
"no-mixed-operators": "off",
"no-nested-ternary": "off",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-prototype-builtins": "off",
"no-restricted-syntax": "off",
"no-trailing-spaces": "error",
"no-underscore-dangle": "off",
"object-curly-newline": [
"error",
{
"ExportDeclaration": "never",
"ImportDeclaration": "never",
"ObjectExpression": {
"consistent": true
},
"ObjectPattern": {
"consistent": true
}
}
],
"operator-linebreak": "off",
"prefer-destructuring": "off",
"sort-imports": [
"error",
{
"ignoreCase": true,
"ignoreDeclarationSort": true
}
]
}
};

2
frontend/.storybook/main.js

@ -21,9 +21,7 @@ class FilterSassWarningsPlugin {
module.exports = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
'@storybook/addon-actions',
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
framework: {

39
frontend/angular.json

@ -18,7 +18,7 @@
"prefix": "sqx",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular/build:application",
"options": {
"outputPath": "build",
"index": "src/index.html",
@ -107,7 +107,7 @@
"./src/app/theme"
],
"sass": {
"silenceDeprecations": ["color-functions", "global-builtin", "import"]
"silenceDeprecations": ["color-functions", "global-builtin", "import", "if-function"]
}
},
"scripts": []
@ -152,7 +152,7 @@
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "squidex:build:production"
@ -170,29 +170,7 @@
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"karmaConfig": "karma.conf.js",
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/app/theme",
"node_modules"
]
},
"scripts": []
}
"builder": "@angular/build:unit-test"
},
"storybook": {
"builder": "@storybook/angular:start-storybook",
@ -209,15 +187,6 @@
}
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "squidex:build",
"compodoc": false,
"outputDir": "storybook-static"
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {

23838
frontend/cache.json

File diff suppressed because it is too large

159
frontend/eslint.config.js

@ -0,0 +1,159 @@
const {
defineConfig,
} = require("eslint/config");
const globals = require("globals");
const tsParser = require("@typescript-eslint/parser");
const deprecation = require("eslint-plugin-deprecation");
const importPlugin = require("eslint-plugin-import");
const typescriptEslint = require("@typescript-eslint/eslint-plugin");
const js = require("@eslint/js");
const {
FlatCompat,
} = require("@eslint/eslintrc");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
module.exports = defineConfig([{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tsParser,
parserOptions: {
"project": ["tsconfig.app.json", "tsconfig.spec.json"],
"createDefaultProgram": true,
},
},
extends: compat.extends(
"eslint:recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
),
plugins: {
deprecation,
import: importPlugin,
"@typescript-eslint": typescriptEslint,
},
},
{
files: ['src/**/*.ts', '**/*.ts'],
rules: {
"@angular-eslint/component-selector": ["error", {
"prefix": "sqx",
"style": "kebab-case",
"type": "element",
}],
"@angular-eslint/directive-selector": ["error", {
"prefix": "sqx",
"style": "camelCase",
"type": "attribute",
}],
"@angular-eslint/prefer-inject": ["off"],
"@angular-eslint/use-lifecycle-interface": ["off"],
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/lines-between-class-members": "off",
"@typescript-eslint/naming-convention": ["error", {
"format": ["camelCase", "PascalCase", "UPPER_CASE"],
"leadingUnderscore": "allow",
"selector": "variable",
"trailingUnderscore": "allow",
}, {
"format": ["PascalCase"],
"selector": "typeLike",
}],
"@typescript-eslint/no-shadow": "off",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
}],
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/return-await": "off",
"arrow-body-style": "off",
"arrow-parens": "off",
"class-methods-use-this": "off",
"default-case": "off",
"function-paren-newline": "off",
"implicit-arrow-linebreak": "off",
"import/extensions": "off",
"import/no-extraneous-dependencies": "off",
"import/no-useless-path-segments": "off",
"import/order": ["error", {
"alphabetize": {
"order": "asc",
},
"pathGroups": [{
"group": "external",
"pattern": "@app/**",
"position": "after",
}],
"pathGroupsExcludedImportTypes": ["builtin"],
}],
"import/prefer-default-export": "off",
"linebreak-style": "off",
"max-classes-per-file": "off",
"max-len": "off",
"newline-per-chained-call": "off",
"no-else-return": "off",
"no-extra-boolean-cast": "off",
"no-mixed-operators": "off",
"no-nested-ternary": "off",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-prototype-builtins": "off",
"no-restricted-syntax": "off",
"no-trailing-spaces": "error",
"no-underscore-dangle": "off",
"no-undef": "off",
"no-unused-vars": "off",
"no-useless-escape": "off",
"no-useless-return": "warn",
"object-curly-newline": ["error", {
"ExportDeclaration": "never",
"ImportDeclaration": "never",
"ObjectExpression": {
"consistent": true,
},
"ObjectPattern": {
"consistent": true,
},
}],
"operator-linebreak": "off",
"prefer-destructuring": "off",
"sort-imports": ["error", {
"ignoreCase": true,
"ignoreDeclarationSort": true,
}],
},
}]);

56
frontend/karma.conf.js

@ -1,56 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('@angular-devkit/build-angular/plugins/karma'),
require('karma-chrome-launcher'),
require('karma-coverage'),
require('karma-jasmine-html-reporter'),
require('karma-jasmine'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/squidex'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' },
],
},
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
// See /integration/README.md#browser-tests for more info on these args
flags: [
'--disable-dev-shm-usage',
'--disable-gpu',
'--headless',
'--no-sandbox',
],
},
},
autoWatch: true,
browsers: ['Chrome'],
colors: true,
logLevel: config.LOG_INFO,
port: 9876,
reporters: ['progress', 'kjhtml'],
restartOnFileChange: true,
singleRun: false,
});
};

18519
frontend/package-lock.json

File diff suppressed because it is too large

136
frontend/package.json

@ -8,7 +8,7 @@
"ng": "ng",
"start": "ng serve",
"test": "ng test",
"test:coverage": "ng test --no-watch --code-coverage --browsers=ChromeHeadlessNoSandbox",
"test:coverage": "ng test --no-watch",
"watch": "ng build --watch --configuration development",
"storybook": "ng run squidex:storybook",
"storybook-build": "ng run squidex:build-storybook",
@ -16,44 +16,43 @@
},
"private": true,
"dependencies": {
"@angular/animations": "19.2.14",
"@angular/cdk": "19.2.18",
"@angular/cdk-experimental": "19.2.18",
"@angular/common": "19.2.14",
"@angular/core": "19.2.14",
"@angular/forms": "19.2.14",
"@angular/localize": "19.2.14",
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/platform-server": "19.2.14",
"@angular/router": "19.2.14",
"@angular/cdk": "21.1.3",
"@angular/cdk-experimental": "21.1.3",
"@angular/common": "21.1.3",
"@angular/core": "21.1.3",
"@angular/forms": "21.1.3",
"@angular/localize": "21.1.3",
"@angular/platform-browser": "21.1.3",
"@angular/platform-browser-dynamic": "21.1.3",
"@angular/platform-server": "21.1.3",
"@angular/router": "21.1.3",
"@egjs/hammerjs": "2.0.17",
"@floating-ui/dom": "^1.7.1",
"@floating-ui/dom": "^1.7.5",
"@graphiql/toolkit": "^0.11.3",
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@lithiumjs/angular": "^8.0.1",
"@lithiumjs/angular": "^9.0.0",
"@sindresorhus/slugify": "^3.0.0",
"ace-builds": "^1.42.0",
"angular-gridster2": "19.0.0",
"ace-builds": "^1.43.6",
"angular-gridster2": "21.0.1",
"bootstrap": "5.3.6",
"core-js": "3.42.0",
"core-js": "3.48.0",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"graphiql": "4.1.1",
"graphql": "16.11.0",
"graphql-ws": "^6.0.5",
"graphql": "16.12.0",
"graphql-ws": "^6.0.7",
"image-focus": "1.2.1",
"leaflet": "1.9.4",
"leaflet-control-geocoder": "3.2.0",
"leaflet-control-geocoder": "3.3.1",
"marked": "15.0.12",
"mersenne-twister": "1.1.0",
"mousetrap": "1.6.5",
"ng2-charts": "^8.0.0",
"ngx-doc-viewer": "15.0.1",
"ngx-inline-filter": "^0.2.3",
"ngx-scrollbar": "^18.0.0",
"ngx-ui-tour-core": "14.0.0",
"oidc-client-ts": "^3.2.1",
"ngx-inline-filter": "^0.2.5",
"ngx-scrollbar": "^19.1.4",
"ngx-ui-tour-core": "16.0.0",
"oidc-client-ts": "^3.4.1",
"progressbar.js": "1.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
@ -62,66 +61,59 @@
"tslib": "2.8.1",
"tui-calendar": "^1.15.3",
"typemoq": "^2.1.0",
"video.js": "8.23.3",
"vis-data": "7.1.9",
"vis-network": "9.1.10",
"vis-util": "5.0.7",
"y-protocols": "^1.0.6",
"video.js": "8.23.6",
"vis-data": "8.0.3",
"vis-network": "10.0.2",
"vis-util": "6.0.0",
"y-protocols": "^1.0.7",
"y-websocket": "^3.0.0",
"zone.js": "0.15.1"
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/architect": "^0.1902.14",
"@angular-devkit/build-angular": "^19.2.14",
"@angular-eslint/builder": "19.7.1",
"@angular-eslint/eslint-plugin": "19.7.1",
"@angular-eslint/eslint-plugin-template": "19.7.1",
"@angular-eslint/schematics": "19.7.1",
"@angular-eslint/template-parser": "19.7.1",
"@angular/cli": "^19.2.14",
"@angular/compiler": "^19.2.14",
"@angular/compiler-cli": "^19.2.14",
"@angular/elements": "^19.2.14",
"@compodoc/compodoc": "^1.1.26",
"@storybook/addon-actions": "^8.6.14",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-interactions": "^8.6.14",
"@storybook/addon-links": "^8.6.14",
"@storybook/angular": "^8.6.14",
"@angular-devkit/architect": "^0.2101.3",
"@angular-devkit/build-angular": "^21.1.3",
"@angular-eslint/builder": "21.2.0",
"@angular-eslint/eslint-plugin": "21.2.0",
"@angular-eslint/eslint-plugin-template": "21.2.0",
"@angular-eslint/schematics": "21.2.0",
"@angular-eslint/template-parser": "21.2.0",
"@angular/cli": "^21.1.3",
"@angular/compiler": "^21.1.3",
"@angular/compiler-cli": "^21.1.3",
"@angular/elements": "^21.1.3",
"@compodoc/compodoc": "^1.2.1",
"@storybook/addon-links": "^10.2.7",
"@storybook/angular": "^10.2.7",
"@storybook/testing-library": "^0.2.2",
"@types/ace": "^0.0.52",
"@types/codemirror": "5.60.16",
"@types/codemirror": "5.60.17",
"@types/core-js": "2.5.8",
"@types/jasmine": "5.1.8",
"@types/mersenne-twister": "1.1.7",
"@types/mousetrap": "1.6.15",
"@types/node": "22.15.29",
"@types/react": "18.3.23",
"@types/react-dom": "18.3.7",
"@types/node": "25.2.1",
"@types/react": "19.2.13",
"@types/react-dom": "19.2.3",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"copy-webpack-plugin": "^13.0.0",
"eslint": "^8.57.1",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"copy-webpack-plugin": "^13.0.1",
"eslint": "^9.39.2",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-storybook": "^0.12.0",
"jasmine-core": "~5.7.1",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "3.5.3",
"eslint-plugin-storybook": "^10.2.7",
"jsdom": "^28.0.0",
"prettier": "3.8.1",
"squidex-prettier-plugin-organize-attributes": "^1.0.1",
"storybook": "^8.6.14",
"stylelint": "16.20.0",
"stylelint-config-standard": "38.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"stylelint-scss": "6.12.0",
"typescript": "5.8.3"
"storybook": "^10.2.7",
"stylelint": "17.1.1",
"stylelint-config-standard": "40.0.0",
"stylelint-config-standard-scss": "^17.0.0",
"stylelint-scss": "7.0.0",
"typescript": "5.9.3",
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^4.0.18"
},
"overrides": {
"ng2-charts": {

2
frontend/src/app/features/administration/guards/user-must-exist.guard.spec.ts

@ -107,4 +107,4 @@ function bit(name: string, assertion: (() => PromiseLike<any>) | (() => void)) {
it(name, () => {
return TestBed.runInInjectionContext(() => assertion());
});
}
}

171
frontend/src/app/features/administration/services/event-consumers.service.spec.ts

@ -15,118 +15,113 @@ import { EventConsumersService } from './event-consumers.service';
describe('EventConsumersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get event consumers',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
let eventConsumers: EventConsumersDto;
eventConsumersService.getEventConsumers().subscribe(result => {
eventConsumers = result;
});
const req = httpMock.expectOne('http://service/p/api/event-consumers');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
items: [
eventConsumerResponse(12),
eventConsumerResponse(13),
],
_links: {},
});
expect(eventConsumers!).toEqual(
new EventConsumersDto({
items: [
createEventConsumer(12),
createEventConsumer(13),
],
_links: {},
}));
it('should make get request to get event consumers', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
let eventConsumers: EventConsumersDto;
eventConsumersService.getEventConsumers().subscribe(result => {
eventConsumers = result;
});
const req = httpMock.expectOne('http://service/p/api/event-consumers');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
items: [
eventConsumerResponse(12),
eventConsumerResponse(13),
],
_links: {},
});
expect(eventConsumers!).toEqual(new EventConsumersDto({
items: [
createEventConsumer(12),
createEventConsumer(13),
],
_links: {},
}));
}));
it('should make put request to start event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
start: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/start' }),
},
};
it('should make put request to start event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
start: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/start' }),
},
};
let eventConsumer: EventConsumerDto;
eventConsumersService.putStart(resource).subscribe(response => {
eventConsumer = response;
});
let eventConsumer: EventConsumerDto;
eventConsumersService.putStart(resource).subscribe(response => {
eventConsumer = response;
});
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer123/start');
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer123/start');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(eventConsumerResponse(123));
req.flush(eventConsumerResponse(123));
expect(eventConsumer!).toEqual(createEventConsumer(123));
}));
expect(eventConsumer!).toEqual(createEventConsumer(123));
}));
it('should make put request to stop event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
stop: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' }),
},
};
it('should make put request to stop event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
stop: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' }),
},
};
let eventConsumer: EventConsumerDto;
eventConsumersService.putStop(resource).subscribe(response => {
eventConsumer = response;
});
let eventConsumer: EventConsumerDto;
eventConsumersService.putStop(resource).subscribe(response => {
eventConsumer = response;
});
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer123/stop');
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer123/stop');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(eventConsumerResponse(12));
req.flush(eventConsumerResponse(12));
expect(eventConsumer!).toEqual(createEventConsumer(12));
}));
expect(eventConsumer!).toEqual(createEventConsumer(12));
}));
it('should make put request to reset event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
reset: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' }),
},
};
it('should make put request to reset event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
reset: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' }),
},
};
let eventConsumer: EventConsumerDto;
eventConsumersService.putReset(resource).subscribe(response => {
eventConsumer = response;
});
let eventConsumer: EventConsumerDto;
eventConsumersService.putReset(resource).subscribe(response => {
eventConsumer = response;
});
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer123/reset');
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer123/reset');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(eventConsumerResponse(12));
req.flush(eventConsumerResponse(12));
expect(eventConsumer!).toEqual(createEventConsumer(12));
}));
expect(eventConsumer!).toEqual(createEventConsumer(12));
}));
function eventConsumerResponse(id: number, suffix = '') {
const key = `${id}${suffix}`;
@ -159,4 +154,4 @@ export function createEventConsumer(id: number, suffix = '') {
reset: new ResourceLinkDto({ method: 'PUT', href: `/event-consumers/${id}/reset` }),
},
});
}
}

330
frontend/src/app/features/administration/services/users.service.spec.ts

@ -15,218 +15,208 @@ import { UsersService } from './users.service';
describe('UsersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto;
userManagementService.getUsers(20, 30).subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
userResponse(12),
userResponse(13),
],
_links: {},
});
expect(users!).toEqual(
new UsersDto({
total: 100,
items: [
createUser(12),
createUser(13),
],
_links: {},
}));
it('should make get request to get many users', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto;
userManagementService.getUsers(20, 30).subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
userResponse(12),
userResponse(13),
],
_links: {},
});
expect(users!).toEqual(new UsersDto({
total: 100,
items: [
createUser(12),
createUser(13),
],
_links: {},
}));
}));
it('should make get request with query to get many users', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto;
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
it('should make get request with query to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto;
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
userResponse(12),
userResponse(13),
],
_links: {},
});
expect(users!).toEqual(
new UsersDto({
total: 100,
items: [
createUser(12),
createUser(13),
],
_links: {},
}));
req.flush({
total: 100,
items: [
userResponse(12),
userResponse(13),
],
_links: {},
});
expect(users!).toEqual(new UsersDto({
total: 100,
items: [
createUser(12),
createUser(13),
],
_links: {},
}));
}));
it('should make get request to get single user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto;
userManagementService.getUser('123').subscribe(result => {
user = result;
});
it('should make get request to get single user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto;
userManagementService.getUser('123').subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management/123');
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(userResponse(12));
req.flush(userResponse(12));
expect(user!).toEqual(createUser(12));
}));
expect(user!).toEqual(createUser(12));
}));
it('should make post request to create user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new CreateUserDto({
email: 'mail@squidex.io',
displayName: 'Squidex User',
permissions: ['Permission1'],
password: 'password',
});
it('should make post request to create user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new CreateUserDto({
email: 'mail@squidex.io',
displayName: 'Squidex User',
permissions: ['Permission1'],
password: 'password',
});
let user: UserDto;
userManagementService.postUser(dto).subscribe(result => {
user = result;
});
let user: UserDto;
userManagementService.postUser(dto).subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management');
const req = httpMock.expectOne('http://service/p/api/user-management');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(userResponse(12));
req.flush(userResponse(12));
expect(user!).toEqual(createUser(12));
}));
expect(user!).toEqual(createUser(12));
}));
it('should make put request to update user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new UpdateUserDto({
email: 'mail@squidex.io',
displayName: 'Squidex User',
permissions: ['Permission1'],
password: 'password',
});
const resource: IResourceDto = {
_links: {
update: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123' }),
},
};
it('should make put request to update user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new UpdateUserDto({
email: 'mail@squidex.io',
displayName: 'Squidex User',
permissions: ['Permission1'],
password: 'password',
});
let user: UserDto;
userManagementService.putUser(resource, dto).subscribe(result => {
user = result;
});
const resource: IResourceDto = {
_links: {
update: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123' }),
},
};
const req = httpMock.expectOne('http://service/p/api/user-management/123');
let user: UserDto;
userManagementService.putUser(resource, dto).subscribe(result => {
user = result;
});
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
const req = httpMock.expectOne('http://service/p/api/user-management/123');
req.flush(userResponse(12));
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(user!).toEqual(createUser(12));
}));
req.flush(userResponse(12));
it('should make put request to lock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
lock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/lock' }),
},
};
expect(user!).toEqual(createUser(12));
}));
let user: UserDto;
userManagementService.lockUser(resource).subscribe(result => {
user = result;
});
it('should make put request to lock user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
lock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/lock' }),
},
};
const req = httpMock.expectOne('http://service/p/api/user-management/123/lock');
let user: UserDto;
userManagementService.lockUser(resource).subscribe(result => {
user = result;
});
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
const req = httpMock.expectOne('http://service/p/api/user-management/123/lock');
req.flush(userResponse(12));
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(user!).toEqual(createUser(12));
}));
req.flush(userResponse(12));
it('should make put request to unlock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
unlock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/unlock' }),
},
};
expect(user!).toEqual(createUser(12));
}));
let user: UserDto;
userManagementService.unlockUser(resource).subscribe(result => {
user = result;
});
it('should make put request to unlock user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
unlock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/unlock' }),
},
};
const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock');
let user: UserDto;
userManagementService.unlockUser(resource).subscribe(result => {
user = result;
});
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock');
req.flush(userResponse(12));
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(user!).toEqual(createUser(12));
}));
req.flush(userResponse(12));
it('should make delete request to delete user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
delete: new ResourceLinkDto({ method: 'DELETE', href: 'api/user-management/123' }),
},
};
expect(user!).toEqual(createUser(12));
}));
it('should make delete request to delete user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: IResourceDto = {
_links: {
delete: new ResourceLinkDto({ method: 'DELETE', href: 'api/user-management/123' }),
},
};
userManagementService.deleteUser(resource).subscribe();
userManagementService.deleteUser(resource).subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123');
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
req.flush({});
}));
function userResponse(id: number, suffix = '') {
const key = `${id}${suffix}`;
@ -263,4 +253,4 @@ export function createUser(id: number, suffix = '') {
update: new ResourceLinkDto({ method: 'PUT', href: `/users/${id}` }),
},
});
}
}

4
frontend/src/app/features/administration/state/event-consumers.state.spec.ts

@ -60,8 +60,6 @@ describe('EventConsumersState', () => {
eventConsumersState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
@ -71,8 +69,6 @@ describe('EventConsumersState', () => {
eventConsumersState.load(true, false).pipe(onErrorResumeNextWith()).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyError(It.isAny()), Times.once());
});
});

6
frontend/src/app/features/administration/state/users.state.spec.ts

@ -63,8 +63,6 @@ describe('UsersState', () => {
usersState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
@ -73,8 +71,6 @@ describe('UsersState', () => {
.returns(() => of(new UsersDto({ items: [], total: 200, _links: {} }))).verifiable();
usersState.page({ page: 1, pageSize: 10 }).subscribe();
expect().nothing();
});
it('should load with query if searching', () => {
@ -204,7 +200,7 @@ describe('UsersState', () => {
describe('Selection', () => {
beforeEach(() => {
usersService.setup(x => x.getUsers(10, 0, undefined))
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(Times.atLeastOnce());
usersState.load().subscribe();

27
frontend/src/app/features/apps/pages/app.component.html

@ -6,14 +6,27 @@
<div class="col card-right">
<h3 class="card-title mb-1">{{ app.displayName }}</h3>
<div class="card-text card-links truncate">
<div class="card-text card-links truncate d-flex gap-2">
<a [routerLink]="['/app', app.name]" sqxStopClick>{{ "common.edit" | sqxTranslate }}</a>
<span class="deeplinks">
&nbsp;|
<a [routerLink]="['/app', app.name, 'content']" sqxStopClick>{{ "common.content" | sqxTranslate }}</a> &middot;
<a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>{{ "common.assets" | sqxTranslate }}</a> &middot;
<a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>{{ "common.settings" | sqxTranslate }}</a>
</span>
@if (canShowContents || canShowAssets || canShowSettings) {
<span class="deeplinks gap-2">
|
@if (canShowContents) {
<a [routerLink]="['/app', app.name, 'content']" sqxStopClick>{{ "common.content" | sqxTranslate }}</a>
<span class="separator">&middot;</span>
}
@if (canShowAssets) {
<a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>{{ "common.assets" | sqxTranslate }}</a>
<span class="separator">&middot;</span>
}
@if (canShowSettings) {
<a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>{{ "common.settings" | sqxTranslate }}</a>
<span class="separator">&middot;</span>
}
</span>
}
</div>
@if (app.description) {

6
frontend/src/app/features/apps/pages/app.component.scss

@ -6,7 +6,7 @@
}
.card {
@include hover-visible('.deeplinks', inline);
@include hover-visible('.deeplinks', flex);
&-title {
padding-right: 2rem;
@ -74,4 +74,8 @@
@include box-shadow-outer(0, 3px, 16px, .2);
}
}
}
.separator:last-child {
display: none;
}

14
frontend/src/app/features/apps/pages/app.component.ts

@ -8,7 +8,7 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { AppDto, AvatarComponent, ConfirmClickDirective, DropdownMenuComponent, ModalDirective, ModalModel, ModalPlacementDirective, StopClickDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { AppDto, AvatarComponent, ConfirmClickDirective, DropdownMenuComponent, ModalDirective, ModalModel, ModalPlacementDirective, Settings, StopClickDirective, TourStepDirective, TranslatePipe } from '@app/shared';
@Component({
selector: 'sqx-app',
@ -35,4 +35,16 @@ export class AppComponent {
public leave = new EventEmitter<AppDto>();
public dropdown = new ModalModel();
public get canShowContents() {
return this.app.canAccessContent;
}
public get canShowAssets() {
return this.app.canReadAssets && this.app.roleProperties[Settings.AppProperties.HIDE_ASSETS] !== true;
}
public get canShowSettings() {
return this.app.roleProperties[Settings.AppProperties.HIDE_SETTINGS] !== true;
}
}

18
frontend/src/app/features/apps/pages/onboarding-dialog.component.html

@ -6,8 +6,8 @@
<a class="header-right modal-close force" (click)="cancel()">{{ "tour.skip" | sqxTranslate }}</a>
@if (step === 0) {
<div class="onboarding-step">
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" @slide>
<img class="header-left" animate.enter="fade-in" animate.leave="fade-out" src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" animate.enter="slide-left-in" animate.leave="slide-left-out">
<h1>
{{ "tour.welcome" | sqxTranslate }} <span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1>
@ -23,14 +23,14 @@
@if (step === 1) {
<div>
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave" @slide>
<img class="header-left" animate.enter="fade-in" animate.leave="fade-out" src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave" animate.enter="slide-left-in" animate.leave="slide-left-out">
<form [formGroup]="answersForm" (ngSubmit)="submitAnswers()">
<h2>{{ "tour.stepDataTitle" | sqxTranslate }}</h2>
<div [sqxMarkdown]="'tour.stepDataText' | sqxTranslate"></div>
<sqx-form-row for="companyRole" label="tour.stepDataCompanyRole" class="mt-4">
<sqx-form-row class="mt-4" for="companyRole" label="tour.stepDataCompanyRole" vertical="true">
<select class="form-select" id="companyRole" formControlName="companyRole">
<option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option>
@ -41,7 +41,7 @@
</select>
</sqx-form-row>
<sqx-form-row for="companySize" label="tour.stepDataCompanySize">
<sqx-form-row for="companySize" label="tour.stepDataCompanySize" vertical="true">
<select class="form-select" id="companySize" formControlName="companySize">
<option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option>
<option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option>
@ -51,7 +51,7 @@
</select>
</sqx-form-row>
<sqx-form-row for="project" label="tour.stepDataProject">
<sqx-form-row for="project" label="tour.stepDataProject" vertical="true">
<select class="form-select" id="project" formControlName="project">
<option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option>
<option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option>
@ -71,8 +71,8 @@
@if (step === 2) {
<div class="onboarding-step">
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" @slide>
<img class="header-left" animate.enter="fade-in" animate.leave="fade-out" src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" animate.enter="slide-left-in" animate.leave="slide-left-out">
<h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2>
<div [sqxMarkdown]="'tour.stepTourText' | sqxTranslate"></div>

5
frontend/src/app/features/apps/pages/onboarding-dialog.component.ts

@ -8,15 +8,12 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { fadeAnimation, FormRowComponent, MarkdownDirective, ModalDialogComponent, slideAnimation, TourState, TranslatePipe, UsersService } from '@app/shared';
import { FormRowComponent, MarkdownDirective, ModalDialogComponent, TourState, TranslatePipe, UsersService } from '@app/shared';
@Component({
selector: 'sqx-onboarding-dialog',
styleUrls: ['./onboarding-dialog.component.scss'],
templateUrl: './onboarding-dialog.component.html',
animations: [
fadeAnimation, slideAnimation,
],
imports: [
FormRowComponent,
FormsModule,

6
frontend/src/app/features/content/shared/forms/content-field.component.html

@ -2,9 +2,11 @@
<div [class.col-12]="!formModelCompare" [class.col-6]="formModelCompare">
<sqx-focus-marker [controlId]="formModel.path">
@if (!(formModel.hiddenChanges | async)) {
<div class="table-items-row table-items-row-summary change-marker-host" [class.field-invalid]="isInvalid | async">
<div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async">
@if (formModel.hasChanges | async) {
<div class="change-marker">{{ "contents.pendingChangesTitle" | sqxTranslate }}</div>
<div class="change-marker-host">
<div class="change-marker">{{ "contents.pendingChangesTitle" | sqxTranslate }}</div>
</div>
}
@if (showAllControls) {

7
frontend/src/app/features/content/shared/forms/content-field.component.scss

@ -3,8 +3,7 @@
.table-items-row {
border-left-width: 4px;
overflow-x: hidden;
overflow-y: hidden;
padding-bottom: 1.5rem;
padding-top: 1.125rem;
}
@ -22,12 +21,10 @@
.change-marker-host {
position: relative;
overflow-x: visible;
overflow-y: visible;
}
.change-marker {
@include absolute(-.5rem, null, null, 1rem);
@include absolute(-1.5rem, null, null, 0);
background: $color-white;
border: 1px solid $color-border;
border-radius: $border-radius;

2
frontend/src/app/features/content/shared/forms/iframe-editor.component.ts

@ -232,7 +232,7 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
schemaIds = schemas;
} if (this.schemaIds && this.schemaIds.length > 0) {
schemaIds = this.schemaIds;
}
}
this.contentsQuery = query;
this.contentsCorrelationId = correlationId;

18
frontend/src/app/features/dashboard/pages/dashboard-config.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { GridsterItem } from 'angular-gridster2';
import { GridsterItemConfig } from 'angular-gridster2';
import { take } from 'rxjs/operators';
import { AppDto, AppsState, AuthService, CodeEditorComponent, ConfirmClickDirective, DialogModel, DialogService, DropdownMenuComponent, LocalizerService, ModalDialogComponent, ModalDirective, ModalModel, ModalPlacementDirective, TooltipDirective, TranslatePipe, TypedSimpleChanges, Types, UIState } from '@app/shared';
@ -33,24 +33,24 @@ export class DashboardConfigComponent {
public app!: AppDto;
@Input({ required: true })
public config!: GridsterItem[];
public config!: GridsterItemConfig[];
@Input()
public configDefaults!: GridsterItem[];
public configDefaults!: GridsterItemConfig[];
@Input()
public configAvailable!: GridsterItem[];
public configAvailable!: GridsterItemConfig[];
@Input({ transform: booleanAttribute })
public needsAttention?: boolean | null;
@Output()
public configChange = new EventEmitter<GridsterItem[]>();
public configChange = new EventEmitter<GridsterItemConfig[]>();
public configOptions: ReadonlyArray<GridsterItem> = [];
public configOptions: ReadonlyArray<GridsterItemConfig> = [];
public expertDialog = new DialogModel();
public expertConfig?: GridsterItem[];
public expertConfig?: GridsterItemConfig[];
public dropdownModal = new ModalModel();
@ -113,7 +113,7 @@ export class DashboardConfigComponent {
this.dialogs.notifyInfo('i18n:dashboard.configSaved');
}
public addOrRemove(item: GridsterItem) {
public addOrRemove(item: GridsterItemConfig) {
const current = this.config.find(x => x.type === item.type);
if (current) {
@ -133,7 +133,7 @@ export class DashboardConfigComponent {
}
}
public isSelected(item: GridsterItem) {
public isSelected(item: GridsterItemConfig) {
return this.config.find(x => x.type === item.type);
}
}

6
frontend/src/app/features/dashboard/pages/dashboard-page.component.html

@ -1,8 +1,8 @@
<sqx-title message="i18n:dashboard.pageTitle" />
@if (selectedApp | async; as app) {
<div class="dashboard" @fade>
<div class="dashboard" animate.enter="fade-in" animate.leave="fade-out">
@if (!isScrolled) {
<div class="dashboard-summary" @fade sqxTourStep="appDashboard">
<div class="dashboard-summary" animate.enter="fade-in" animate.leave="fade-out" sqxTourStep="appDashboard">
<h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div class="subtext" [sqxMarkdown]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName }"></div>
@ -24,7 +24,7 @@
<sqx-github-card />
}
@case ("api-calls") {
@case ("api-calls") {animate.enter="fade-in" animate.leave="fade-out"
<sqx-api-calls-card [app]="app" [usage]="callsUsage" />
}

19
frontend/src/app/features/dashboard/pages/dashboard-page.component.ts

@ -7,8 +7,8 @@
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppsState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, fadeAnimation, IFrameCardComponent, LocalStoreService, MarkdownDirective, RandomCatCardComponent, RandomDogCardComponent, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TitleComponent, TourStepDirective, TranslatePipe, UsagesService } from '@app/shared';
import { Gridster, GridsterConfig, GridsterItem, GridsterItemConfig, GridType } from 'angular-gridster2';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppsState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, IFrameCardComponent, LocalStoreService, MarkdownDirective, RandomCatCardComponent, RandomDogCardComponent, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TitleComponent, TourStepDirective, TranslatePipe, UsagesService } from '@app/shared';
import { ApiCardComponent } from './cards/api-card.component';
import { ContentSummaryCardComponent } from './cards/content-summary-card.component';
import { GithubCardComponent } from './cards/github-card.component';
@ -20,9 +20,6 @@ import { DashboardConfigComponent } from './dashboard-config.component';
selector: 'sqx-dashboard-page',
styleUrls: ['./dashboard-page.component.scss'],
templateUrl: './dashboard-page.component.html',
animations: [
fadeAnimation,
],
imports: [
ApiCallsCardComponent,
ApiCallsSummaryCardComponent,
@ -37,8 +34,8 @@ import { DashboardConfigComponent } from './dashboard-config.component';
ContentSummaryCardComponent,
DashboardConfigComponent,
GithubCardComponent,
GridsterComponent,
GridsterItemComponent,
Gridster,
GridsterItem,
HistoryCardComponent,
IFrameCardComponent,
MarkdownDirective,
@ -55,7 +52,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
private readonly subscriptions = new Subscriptions();
@ViewChild('grid')
public grid!: GridsterComponent;
public grid!: Gridster;
public selectedApp = this.appsState.selectedApp.pipe(defined());
@ -67,7 +64,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
public callsUsage?: CallsUsageDto;
public gridConfig?: GridsterItem[];
public gridConfig?: GridsterItemConfig[];
public gridConfigAvailable = [...DEFAULT_CELLS, ...ADDITIONAL_CELLS];
public gridConfigDefaults = DEFAULT_CELLS;
public gridOptions = DEFAULT_OPTIONS;
@ -136,14 +133,14 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
this.isStacked = value;
}
public changeConfig(config: GridsterItem[]) {
public changeConfig(config: GridsterItemConfig[]) {
this.gridConfig = config;
this.grid?.updateGrid();
}
}
const DEFAULT_CELLS: GridsterItem[] = [
const DEFAULT_CELLS: GridsterItemConfig[] = [
// Row 1
{ cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.schemasCard', type: 'schemas' },
{ cols: 1, rows: 1, x: 1, y: 0, name: 'i18n:dashboard.apiDocumentationCard', type: 'api' },

2
frontend/src/app/features/settings/pages/roles/roles-page.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable no-return-assign */
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

4
frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html

@ -1,8 +1,8 @@
<sqx-title message="i18n:dashboard.pageTitle" />
@if (selectedTeam | async; as team) {
<div class="dashboard" @fade>
<div class="dashboard" animate.enter="fade-in" animate.leave="fade-out">
@if (!isScrolled) {
<div class="dashboard-summary" @fade>
<div class="dashboard-summary" animate.enter="fade-in" animate.leave="fade-out">
<h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div class="subtext" [sqxMarkdown]="'dashboard.welcomeTextTeam' | sqxTranslate: { team: team.name }"></div>

21
frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts

@ -7,17 +7,14 @@
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, fadeAnimation, IFrameCardComponent, LocalStoreService, MarkdownDirective, RandomCatCardComponent, RandomDogCardComponent, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TeamsState, TitleComponent, TranslatePipe, UsagesService } from '@app/shared';
import { Gridster, GridsterConfig, GridsterItem, GridsterItemConfig, GridType } from 'angular-gridster2';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, IFrameCardComponent, LocalStoreService, MarkdownDirective, RandomCatCardComponent, RandomDogCardComponent, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TeamsState, TitleComponent, TranslatePipe, UsagesService } from '@app/shared';
import { AppsCardComponent } from './cards/apps-card.component';
@Component({
selector: 'sqx-dashboard-page',
styleUrls: ['./dashboard-page.component.scss'],
templateUrl: './dashboard-page.component.html',
animations: [
fadeAnimation,
],
imports: [
ApiCallsCardComponent,
ApiCallsSummaryCardComponent,
@ -29,8 +26,8 @@ import { AppsCardComponent } from './cards/apps-card.component';
AssetUploadsSizeCardComponent,
AssetUploadsSizeSummaryCardComponent,
AsyncPipe,
GridsterComponent,
GridsterItemComponent,
Gridster,
GridsterItem,
IFrameCardComponent,
MarkdownDirective,
RandomCatCardComponent,
@ -44,7 +41,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
private readonly subscriptions = new Subscriptions();
@ViewChild('grid')
public grid!: GridsterComponent;
public grid!: Gridster;
public selectedTeam = this.teamsState.selectedTeam.pipe(defined());
@ -56,7 +53,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
public callsUsage?: CallsUsageDto;
public gridConfig: GridsterItem[] = DEFAULT_CONFIG;
public gridConfig: GridsterItemConfig[] = DEFAULT_CONFIG;
public gridOptions = DEFAULT_OPTIONS;
public gridDefault = DEFAULT_CONFIG;
public gridOptionals = OPTIONAL_CARDS;
@ -121,18 +118,16 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
public changeIsStacked(value: boolean) {
this.localStore.setBoolean(Settings.Local.DASHBOARD_CHART_STACKED, value);
this.isStacked = value;
}
public changeConfig(config: GridsterItem[]) {
public changeConfig(config: GridsterItemConfig[]) {
this.gridConfig = config;
this.grid?.updateGrid();
}
}
const DEFAULT_CONFIG: GridsterItem[] = [
const DEFAULT_CONFIG: GridsterItemConfig[] = [
// Row 1
{ cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.trafficSummaryCard', type: 'api-traffic-summary' },
{ cols: 2, rows: 1, x: 1, y: 0, name: 'i18n:dashboard.appsCard', type: 'apps' },

123
frontend/src/app/features/teams/services/team-contributors.service.spec.ts

@ -17,90 +17,87 @@ describe('TeamContributorsService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
TeamContributorsService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
TeamContributorsService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get team contributors',
inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
let contributors: Versioned<ContributorsDto>;
contributorsService.getContributors('my-team').subscribe(result => {
contributors = result;
});
it('should make get request to get team contributors', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
let contributors: Versioned<ContributorsDto>;
contributorsService.getContributors('my-team').subscribe(result => {
contributors = result;
});
const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors');
const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(contributorsResponse(1, 2, 3), {
headers: {
etag: '2',
},
});
req.flush(contributorsResponse(1, 2, 3), {
headers: {
etag: '2',
},
});
expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') });
}));
expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') });
}));
it('should make post request to assign contributor',
inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
const dto = new AssignContributorDto({ contributorId: '123', role: 'Owner' });
it('should make post request to assign contributor', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
const dto = new AssignContributorDto({ contributorId: '123', role: 'Owner' });
let contributors: Versioned<ContributorsDto>;
contributorsService.postContributor('my-team', dto, version).subscribe(result => {
contributors = result;
});
let contributors: Versioned<ContributorsDto>;
contributorsService.postContributor('my-team', dto, version).subscribe(result => {
contributors = result;
});
const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors');
const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(contributorsResponse(1, 2, 3), {
headers: {
etag: '2',
},
});
req.flush(contributorsResponse(1, 2, 3), {
headers: {
etag: '2',
},
});
expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') });
}));
expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') });
}));
it('should make delete request to remove contributor',
inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
delete: { method: 'DELETE', href: '/api/teams/my-team/contributors/123' },
},
};
it('should make delete request to remove contributor', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
delete: { method: 'DELETE', href: '/api/teams/my-team/contributors/123' },
},
};
let contributors: Versioned<ContributorsDto>;
contributorsService.deleteContributor('my-team', resource, version).subscribe(result => {
contributors = result;
});
let contributors: Versioned<ContributorsDto>;
contributorsService.deleteContributor('my-team', resource, version).subscribe(result => {
contributors = result;
});
const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors/123');
const req = httpMock.expectOne('http://service/p/api/teams/my-team/contributors/123');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(contributorsResponse(1, 2, 3), {
headers: {
etag: '2',
},
});
req.flush(contributorsResponse(1, 2, 3), {
headers: {
etag: '2',
},
});
expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') });
}));
expect(contributors!).toEqual({ payload: createContributors(1, 2, 3), version: new VersionTag('2') });
}));
function contributorsResponse(...ids: number[]) {
return {
@ -147,4 +144,4 @@ export function createContributor(id: number) {
update: new ResourceLinkDto({ method: 'PUT', href: `/contributors/id${id}` }),
},
});
}
}

4
frontend/src/app/features/teams/services/team-contributors.service.ts

@ -10,9 +10,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig, AssignContributorDto, ContributorsDto, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/shared';
@Injectable({
providedIn: 'any',
})
@Injectable()
export class TeamContributorsService {
constructor(
private readonly http: HttpClient,

160
frontend/src/app/features/teams/services/team-plans.service.spec.ts

@ -16,38 +16,78 @@ describe('TeamPlansService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
TeamPlansService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
TeamPlansService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get team plans',
inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => {
let plans: Versioned<PlansDto>;
plansService.getPlans('my-team').subscribe(result => {
plans = result;
});
it('should make get request to get team plans', inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => {
let plans: Versioned<PlansDto>;
plansService.getPlans('my-team').subscribe(result => {
plans = result;
});
const req = httpMock.expectOne('http://service/p/api/teams/my-team/plans');
const req = httpMock.expectOne('http://service/p/api/teams/my-team/plans');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
currentPlanId: 'free',
portalLink: 'link/to/portal',
planOwner: '456',
plans: [
{
id: 'free',
name: 'Free',
costs: '14 €',
confirmText: 'Change for 14 € per month?',
yearlyId: 'free_yearly',
yearlyCosts: '120 €',
yearlyConfirmText: 'Change for 120 € per year?',
maxApiBytes: 128,
maxApiCalls: 1000,
maxAssetSize: 1500,
maxContributors: 2500,
},
{
id: 'professional',
name: 'Professional',
costs: '18 €',
confirmText: 'Change for 18 € per month?',
yearlyId: 'professional_yearly',
yearlyCosts: '160 €',
yearlyConfirmText: 'Change for 160 € per year?',
maxApiBytes: 512,
maxApiCalls: 4000,
maxAssetSize: 5500,
maxContributors: 6500,
},
],
referral: { code: 'CODE', earned: '0', condition: 'None' },
locked: 'ManagedByTeam',
}, {
headers: {
etag: '2',
},
});
req.flush({
expect(plans!).toEqual({
payload: new PlansDto({
currentPlanId: 'free',
portalLink: 'link/to/portal',
planOwner: '456',
plans: [
{
new PlanDto({
id: 'free',
name: 'Free',
costs: '14 €',
@ -59,8 +99,8 @@ describe('TeamPlansService', () => {
maxApiCalls: 1000,
maxAssetSize: 1500,
maxContributors: 2500,
},
{
}),
new PlanDto({
id: 'professional',
name: 'Professional',
costs: '18 €',
@ -72,72 +112,30 @@ describe('TeamPlansService', () => {
maxApiCalls: 4000,
maxAssetSize: 5500,
maxContributors: 6500,
},
}),
],
referral: { code: 'CODE', earned: '0', condition: 'None' },
referral: new ReferralInfoDto({ code: 'CODE', earned: '0', condition: 'None' }),
locked: 'ManagedByTeam',
}, {
headers: {
etag: '2',
},
});
expect(plans!).toEqual({
payload: new PlansDto({
currentPlanId: 'free',
portalLink: 'link/to/portal',
planOwner: '456',
plans: [
new PlanDto({
id: 'free',
name: 'Free',
costs: '14 €',
confirmText: 'Change for 14 € per month?',
yearlyId: 'free_yearly',
yearlyCosts: '120 €',
yearlyConfirmText: 'Change for 120 € per year?',
maxApiBytes: 128,
maxApiCalls: 1000,
maxAssetSize: 1500,
maxContributors: 2500,
}),
new PlanDto({
id: 'professional',
name: 'Professional',
costs: '18 €',
confirmText: 'Change for 18 € per month?',
yearlyId: 'professional_yearly',
yearlyCosts: '160 €',
yearlyConfirmText: 'Change for 160 € per year?',
maxApiBytes: 512,
maxApiCalls: 4000,
maxAssetSize: 5500,
maxContributors: 6500,
}),
],
referral: new ReferralInfoDto({ code: 'CODE', earned: '0', condition: 'None' }),
locked: 'ManagedByTeam',
}),
version: new VersionTag('2'),
});
}));
}),
version: new VersionTag('2'),
});
}));
it('should make put request to change plan',
inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => {
const dto = new ChangePlanDto({ planId: 'enterprise' });
it('should make put request to change plan', inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => {
const dto = new ChangePlanDto({ planId: 'enterprise' });
let planChanged: PlanChangedDto;
plansService.putPlan('my-team', dto, version).subscribe(result => {
planChanged = result.payload;
});
let planChanged: PlanChangedDto;
plansService.putPlan('my-team', dto, version).subscribe(result => {
planChanged = result.payload;
});
const req = httpMock.expectOne('http://service/p/api/teams/my-team/plan');
const req = httpMock.expectOne('http://service/p/api/teams/my-team/plan');
req.flush({ redirectUri: 'http://url' });
req.flush({ redirectUri: 'http://url' });
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
expect(planChanged!).toEqual(new PlanChangedDto({ redirectUri: 'http://url' }));
}));
expect(planChanged!).toEqual(new PlanChangedDto({ redirectUri: 'http://url' }));
}));
});

4
frontend/src/app/features/teams/services/team-plans.service.ts

@ -10,9 +10,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig, ChangePlanDto, HTTP, mapVersioned, PlanChangedDto, PlansDto, pretifyError, Versioned, VersionOrTag } from '@app/shared';
@Injectable({
providedIn: 'any',
})
@Injectable()
export class TeamPlansService {
constructor(
private readonly http: HttpClient,

2
frontend/src/app/features/teams/state/team-auth.forms.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable no-useless-escape */
import { UntypedFormControl, Validators } from '@angular/forms';
import { map, shareReplay } from 'rxjs/operators';
import { ExtendedFormGroup, Form, value$ } from '@app/framework';

9
frontend/src/app/features/teams/state/team-auth.state.spec.ts

@ -12,12 +12,7 @@ import { AuthSchemeDto, AuthSchemeResponseDto, DialogService, ResourceLinkDto, T
import { TeamAuthState } from '../internal';
describe('TeamAuthState', () => {
const {
newVersion,
team,
teamsState,
version,
} = TestValues;
const { newVersion, team, teamsState, version } = TestValues;
const scheme = new AuthSchemeDto({
domain: 'squidex.io',
@ -72,8 +67,6 @@ describe('TeamAuthState', () => {
authState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
});

4
frontend/src/app/features/teams/state/team-auth.state.ts

@ -21,9 +21,7 @@ interface Snapshot extends LoadingState {
version: VersionTag;
}
@Injectable({
providedIn: 'any',
})
@Injectable()
export class TeamAuthState extends State<Snapshot> {
public scheme =
this.project(x => x.scheme);

1
frontend/src/app/features/teams/state/team-contributors.forms.ts

@ -77,5 +77,4 @@ function extractEmails(value: string) {
return result;
}
// eslint-disable-next-line no-useless-escape
const EMAIL_REGEX = /(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*/gim;

31
frontend/src/app/features/teams/state/team-contributors.state.spec.ts

@ -14,12 +14,7 @@ import { AssignContributorDto, ContributorDto, ContributorsDto, DialogService, E
import { TeamContributorsService, TeamContributorsState } from '../internal';
describe('TeamContributorsState', () => {
const {
team,
teamsState,
newVersion,
version,
} = TestValues;
const { team, teamsState, newVersion, version } = TestValues;
const allIds: number[] = [];
@ -73,8 +68,6 @@ describe('TeamContributorsState', () => {
it('should not load if already loaded', () => {
contributorsState.load(true).subscribe();
contributorsState.loadIfNotLoaded().subscribe();
expect().nothing();
});
it('should only show current page of contributors', () => {
@ -124,8 +117,6 @@ describe('TeamContributorsState', () => {
it('should show notification on load if reload is true', () => {
contributorsState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
});
@ -156,13 +147,11 @@ describe('TeamContributorsState', () => {
let error: ErrorDto;
contributorsState.assign(request).pipe(
catchError(err => {
error = err;
contributorsState.assign(request).pipe(catchError(err => {
error = err;
return EMPTY;
}),
).subscribe();
return EMPTY;
})).subscribe();
expect(error!.message).toBe('i18n:contributors.userNotFound');
});
@ -175,13 +164,11 @@ describe('TeamContributorsState', () => {
let error: ErrorDto;
contributorsState.assign(request).pipe(
catchError(err => {
error = err;
contributorsState.assign(request).pipe(catchError(err => {
error = err;
return EMPTY;
}),
).subscribe();
return EMPTY;
})).subscribe();
expect(error!.message).toBe('500');
});

4
frontend/src/app/features/teams/state/team-contributors.state.ts

@ -25,9 +25,7 @@ interface Snapshot extends ListState<string> {
canCreate?: boolean;
}
@Injectable({
providedIn: 'any',
})
@Injectable()
export class TeamContributorsState extends State<Snapshot> {
public contributors =
this.project(x => x.contributors);

10
frontend/src/app/features/teams/state/team-plans.state.spec.ts

@ -12,13 +12,7 @@ import { DialogService, PlanChangedDto, PlanDto, PlansDto, versioned } from '@ap
import { TeamPlansService, TeamPlansState } from '../internal';
describe('TeamPlansState', () => {
const {
creator,
newVersion,
team,
teamsState,
version,
} = TestValues;
const { creator, newVersion, team, teamsState, version } = TestValues;
const oldPlans = new PlansDto({
currentPlanId: 'free',
@ -118,8 +112,6 @@ describe('TeamPlansState', () => {
plansState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
});

4
frontend/src/app/features/teams/state/team-plans.state.ts

@ -42,9 +42,7 @@ interface Snapshot extends LoadingState {
version: VersionTag;
}
@Injectable({
providedIn: 'any',
})
@Injectable()
export class TeamPlansState extends State<Snapshot> {
public plans =
this.project(x => x.plans);

96
frontend/src/app/framework/angular/animations.ts

@ -1,96 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { animate, AnimationTriggerMetadata, keyframes, state, style, transition, trigger } from '@angular/animations';
export function buildSlideRightAnimation(name = 'slideRight', timing = '150ms'): AnimationTriggerMetadata {
return trigger(
name, [
transition(':enter', [
style({ transform: 'translateX(-100%)' }),
animate(timing, style({ transform: 'translateX(0%)' })),
]),
transition(':leave', [
style({ transform: 'translateX(0%)' }),
animate(timing, style({ transform: 'translateX(-100%)' })),
]),
state('true',
style({ transform: 'translateX(0%)' }),
),
state('false',
style({ transform: 'translateX(-100%)' }),
),
transition('1 => 0', animate(timing)),
transition('0 => 1', animate(timing)),
],
);
}
export function buildSlideAnimation(name = 'slide', timing = '400ms'): AnimationTriggerMetadata {
return trigger(
name, [
transition(':enter', [
style({ transform: 'translateX(100%)' }),
animate(timing, style({ transform: 'translateX(0%)' })),
]),
transition(':leave', [
style({ transform: 'translateX(0%)' }),
animate(timing, style({ transform: 'translateX(-100%)' })),
]),
state('true',
style({ transform: 'translateX(0%)' }),
),
state('false',
style({ transform: 'translateX(-100%)' }),
),
transition('1 => 0', animate(timing)),
transition('0 => 1', animate(timing)),
],
);
}
export function buildFadeAnimation(name = 'fade', timing = '150ms'): AnimationTriggerMetadata {
return trigger(
name, [
transition(':enter', [
style({ opacity: 0 }),
animate(timing, style({ opacity: 1 })),
]),
transition(':leave', [
style({ opacity: 1 }),
animate(timing, style({ opacity: 0 })),
]),
state('true',
style({ opacity: 1 }),
),
state('false',
style({ opacity: 0 }),
),
transition('1 => 0', animate(timing)),
transition('0 => 1', animate(timing)),
],
);
}
export function buildBounceAnimation(name = 'bounce', timing = '150ms'): AnimationTriggerMetadata {
return trigger(
name, [
transition('* => true', [
animate(timing, keyframes([
style({ transform: 'translateX(0)' }),
style({ transform: 'translateX(-10px)' }),
style({ transform: 'translateX(0)' }),
])),
]),
],
);
}
export const bounceAnimation = buildBounceAnimation();
export const fadeAnimation = buildFadeAnimation();
export const slideAnimation = buildSlideAnimation();
export const slideRightAnimation = buildSlideRightAnimation();

2
frontend/src/app/framework/angular/autosize.directive.ts

@ -34,7 +34,7 @@ export class AutosizeDirective implements AfterViewInit, DoCheck {
}
@HostListener('input')
private resize() {
protected resize() {
const textarea = this.element.nativeElement as HTMLTextAreaElement;
// Calculate border height which is not included in scrollHeight

2
frontend/src/app/framework/angular/avatar.stories.ts

@ -38,7 +38,7 @@ export const Empty: Story = {
export const Image = {
args: {
image: 'https://via.placeholder.com/50',
image: 'https://placehold.co/400',
},
};

11
frontend/src/app/framework/angular/dropdown-menu.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input } from '@angular/core';
import { fadeAnimation } from './animations';
@Component({
selector: 'sqx-dropdown-menu',
@ -14,18 +13,12 @@ import { fadeAnimation } from './animations';
templateUrl: './dropdown-menu.component.html',
host: {
class: 'dropdown-menu',
['animate.enter']: 'fade-in',
['animate.leave']: 'fade-out',
},
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DropdownMenuComponent {
@HostBinding('@fade')
public get fade() {
return true;
}
@HostBinding('class')
@Input()
public class?: string;

14
frontend/src/app/framework/angular/forms/confirm-click.directive.spec.ts

@ -34,7 +34,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeTrue();
expect(invoked).toBe(true);
dialogs.verify(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAny()), Times.never());
});
@ -55,7 +55,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeTrue();
expect(invoked).toBe(true);
});
it('Should invoke action when unsubscribed in between', () => {
@ -78,7 +78,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeTrue();
expect(invoked).toBe(true);
});
it('Should not invoke action when not confirmed', () => {
@ -97,7 +97,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeFalse();
expect(invoked).toBe(false);
});
it('Should invoke action when unsubscribed before', () => {
@ -116,7 +116,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeFalse();
expect(invoked).toBe(false);
});
it('Should not confirm when text is empty', () => {
@ -126,8 +126,6 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.confirmTitle = 'confirmTitle';
confirmClickDirective.onClick(new Event('click'));
expect().nothing();
dialogs.verify(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAny()), Times.never());
});
@ -138,8 +136,6 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.confirmText = 'confirmText';
confirmClickDirective.onClick(new Event('click'));
expect().nothing();
dialogs.verify(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAny()), Times.never());
});
});

1
frontend/src/app/framework/angular/forms/confirm-click.directive.ts

@ -43,7 +43,6 @@ export class ConfirmClickDirective {
this.confirmTitle.length > 0 &&
this.confirmText &&
this.confirmText.length > 0) {
// eslint-disable-next-line deprecation/deprecation
const destinations = this.clickConfirmed.observers?.map(x => (x as Subscriber<any>)['destination']) || [];
this.beforeClick.emit();

2
frontend/src/app/framework/angular/forms/control-errors-messages.component.html

@ -1,5 +1,5 @@
@if (errorMessages && errorMessages.length > 0) {
<div class="errors-container" @fade>
<div class="errors-container" animate.enter="fade-in" animate.leave="fade-out">
<div class="errors">
@for (message of errorMessages; track message) {
{{ message }}

4
frontend/src/app/framework/angular/forms/control-errors-messages.component.ts

@ -7,15 +7,11 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { fadeAnimation } from '@app/framework/internal';
@Component({
selector: 'sqx-errors-messages',
styleUrls: ['./control-errors-messages.component.scss'],
templateUrl: './control-errors-messages.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ControlErrorsMessagesComponent {

3
frontend/src/app/framework/angular/forms/copy-global.directive.ts

@ -78,13 +78,12 @@ export class CopyGlobalDirective {
private copy() {
try {
// eslint-disable-next-line deprecation/deprecation
document.execCommand('copy');
this.dialogs.notifyInfo('i18n:common.clipboardAdded');
return true;
} catch (e) {
} catch {
return false;
}
}

3
frontend/src/app/framework/angular/forms/copy.directive.ts

@ -63,13 +63,12 @@ export class CopyDirective {
private copy() {
try {
// eslint-disable-next-line deprecation/deprecation
document.execCommand('copy');
this.dialogs.notifyInfo('i18n:common.clipboardAdded');
return true;
} catch (e) {
} catch {
return false;
}
}

2
frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { map, Observable, timer } from 'rxjs';
import { AutocompleteComponent, AutocompleteSource, LocalizerService, RootViewComponent } from '@app/framework';
@ -48,7 +47,6 @@ export default {
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
RootViewComponent,
],
providers: [

2
frontend/src/app/framework/angular/forms/editors/code-editor.component.ts

@ -241,7 +241,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, any> imple
if (isValid) {
try {
newValueOut = JSON.parse(newValueText);
} catch (e) {
} catch {
newValueOut = null;
newValueText = '';
}

2
frontend/src/app/framework/angular/forms/editors/code-editor.stories.ts

@ -6,7 +6,6 @@
*/
import { FormsModule } from '@angular/forms';
import { action } from '@storybook/addon-actions';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { CodeEditorComponent, ScriptCompletions } from '@app/framework';
@ -131,6 +130,5 @@ export const SingleLine: Story = {
}),
args: {
singleLine: true,
ngModelChange: action('ngModelChange'),
} as any,
};

2
frontend/src/app/framework/angular/forms/editors/date-time-editor.component.ts

@ -112,7 +112,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<State, str
let value: DateTime | null;
try {
value = DateTime.parseISO(obj, false);
} catch (ex) {
} catch {
value = null;
}

10
frontend/src/app/framework/angular/forms/editors/dropdown.stories.ts

@ -7,7 +7,6 @@
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { DropdownComponent, LocalizerService, RootViewComponent } from '@app/framework';
@ -18,10 +17,10 @@ const TRANSLATIONS = {
@Component({
selector: 'sqx-dropdown-test',
imports: [RootViewComponent, DropdownComponent],
template: `
<sqx-root-view>
<sqx-dropdown
[allowOpen]="true"
<sqx-dropdown
[items]="items"
[itemsLoading]="itemsLoading"
(dropdownOpen)="load()">
@ -86,13 +85,10 @@ export default {
}),
decorators: [
moduleMetadata({
declarations: [
TestComponent,
],
imports: [
BrowserAnimationsModule,
FormsModule,
RootViewComponent,
TestComponent,
],
providers: [
{

6
frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts

@ -17,6 +17,7 @@ const TRANSLATIONS = {
@Component({
selector: 'sqx-tag-editor-test',
imports: [RootViewComponent, TagEditorComponent],
template: `
<sqx-root-view>
<sqx-tag-editor
@ -27,6 +28,7 @@ const TRANSLATIONS = {
</sqx-tag-editor>
</sqx-root-view>
`,
standalone: true,
})
class TestComponent {
public itemsSource: string[] = [];
@ -77,12 +79,10 @@ export default {
}),
decorators: [
moduleMetadata({
declarations: [
TestComponent,
],
imports: [
FormsModule,
RootViewComponent,
TestComponent,
],
providers: [
{

30
frontend/src/app/framework/angular/forms/extended-form-array.spec.ts

@ -25,21 +25,21 @@ describe('ExtendedFormArray', () => {
describe('UndefinableFormArray', () => {
const tests = [{
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: [1],
valueActual: [1],
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: [1],
valueActual: [1],
}];
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: [1],
valueActual: [1],
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: [1],
valueActual: [1],
}];
it('should initialize with undefined', () => {
const control = new UndefinableFormArray();

30
frontend/src/app/framework/angular/forms/extended-form-group.spec.ts

@ -25,21 +25,21 @@ describe('ExtendedFormGroup', () => {
describe('UndefinableFormGroup', () => {
const tests = [{
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}];
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}];
it('should initialize with undefined', () => {
const control = new UndefinableFormGroup();

22
frontend/src/app/framework/angular/forms/file-drop.directive.ts

@ -52,7 +52,7 @@ export class FileDropDirective {
@HostListener('dragend', ['$event'])
@HostListener('dragleave', ['$event'])
public onDragEnd(event: DragDropEvent) {
public onDragEnd(event: DragEvent) {
const hasFile = this.hasAllowedFile(event.dataTransfer);
if (hasFile) {
@ -61,7 +61,7 @@ export class FileDropDirective {
}
@HostListener('dragenter', ['$event'])
public onDragEnter(event: DragDropEvent) {
public onDragEnter(event: DragEvent) {
const hasFile = this.hasAllowedFile(event.dataTransfer);
if (hasFile) {
@ -70,7 +70,7 @@ export class FileDropDirective {
}
@HostListener('dragover', ['$event'])
public onDragOver(event: DragDropEvent) {
public onDragOver(event: DragEvent) {
const isFiles = hasFiles(event.dataTransfer);
if (isFiles) {
@ -79,7 +79,7 @@ export class FileDropDirective {
}
@HostListener('drop', ['$event'])
public async onDrop(event: DragDropEvent) {
public async onDrop(event: DragEvent) {
if (hasFiles(event.dataTransfer)) {
this.stopDrag(event);
@ -96,7 +96,7 @@ export class FileDropDirective {
event.stopPropagation();
}
private stopDrag(event: DragDropEvent) {
private stopDrag(event: DragEvent) {
this.dragEnd(0);
this.stopEvent(event);
}
@ -139,7 +139,7 @@ export class FileDropDirective {
const webkitEntry = item.webkitGetAsEntry();
if (webkitEntry && webkitEntry.isDirectory) {
// eslint-disable-next-line no-await-in-loop
await this.traverseWebkitTree(webkitEntry, files);
}
}
@ -163,7 +163,7 @@ export class FileDropDirective {
const entries = await getFilesPromise(item);
for (const entry of entries) {
// eslint-disable-next-line no-await-in-loop
await this.traverseWebkitTree(entry, files);
}
}
@ -202,7 +202,7 @@ export class FileDropDirective {
}
}
function hasFiles(dataTransfer: DataTransfer): boolean {
function hasFiles(dataTransfer: DataTransfer | null): boolean {
if (!dataTransfer || !dataTransfer.types) {
return false;
}
@ -270,8 +270,4 @@ function getFiles(dataTransfer: DataTransfer) {
}
return result;
}
interface DragDropEvent extends MouseEvent {
readonly dataTransfer: DataTransfer;
}
}

8
frontend/src/app/framework/angular/forms/focus-on-init.directive.spec.ts

@ -28,7 +28,7 @@ describe('FocusOnInitDirective', () => {
isSelectCalled = false;
});
it('should call focus on element if init', (cb) => {
it('should call focus on element if init', async () => {
const directive = new FocusOnInitDirective(element);
directive.select = false;
directive.ngAfterViewInit();
@ -37,11 +37,11 @@ describe('FocusOnInitDirective', () => {
expect(isFocusCalled).toBeTruthy();
expect(isSelectCalled).toBeFalsy();
cb();
}, 200);
});
it('should call select on element if init', (cb) => {
it('should call select on element if init', async () => {
const directive = new FocusOnInitDirective(element);
directive.select = true;
directive.ngAfterViewInit();
@ -50,7 +50,7 @@ describe('FocusOnInitDirective', () => {
expect(isFocusCalled).toBeTruthy();
expect(isSelectCalled).toBeTruthy();
cb();
}, 200);
});
});

61
frontend/src/app/framework/angular/forms/templated-form-array.spec.ts

@ -36,7 +36,10 @@ describe('TemplatedFormArray', () => {
formArray = new TemplatedFormArray(formTemplate);
});
type Test = [ (value: any) => void, string];
type Test = [
(value: any) => void,
string,
];
const methods: Test[] = [
[x => formArray.setValue(x), 'setValue'],
@ -47,10 +50,10 @@ describe('TemplatedFormArray', () => {
methods.forEach(([method, name]) => {
it(`Should call template to construct items for ${name}`, () => {
const value1 = [{
value: 1,
}, {
value: 2,
}];
value: 1,
}, {
value: 2,
}];
method(value1);
@ -59,20 +62,20 @@ describe('TemplatedFormArray', () => {
it(`Should call template to remove items for ${name}`, () => {
const value1 = [{
value: 1,
}, {
value: 2,
}, {
value: 3,
}, {
value: 4,
}];
value: 1,
}, {
value: 2,
}, {
value: 3,
}, {
value: 4,
}];
const value2 = [{
value: 1,
}, {
value: 2,
}];
value: 1,
}, {
value: 2,
}];
method(value1);
method(value2);
@ -84,10 +87,10 @@ describe('TemplatedFormArray', () => {
it(`Should call template to clear items with undefined for ${name}`, () => {
const value1 = [{
value: 1,
}, {
value: 2,
}];
value: 1,
}, {
value: 2,
}];
method(value1);
method(undefined);
@ -99,10 +102,10 @@ describe('TemplatedFormArray', () => {
it(`Should call template to clear items with empty array for ${name}`, () => {
const value1 = [{
value: 1,
}, {
value: 2,
}];
value: 1,
}, {
value: 2,
}];
method(value1);
method([]);
@ -118,10 +121,10 @@ describe('TemplatedFormArray', () => {
formArray.add();
expect(formArray.value).toEqual([{
value: null,
}, {
value: null,
}]);
value: null,
}, {
value: null,
}]);
});
it('should call template when cleared', () => {

5
frontend/src/app/framework/angular/forms/templated-form-group.spec.ts

@ -30,7 +30,10 @@ describe('TemplatedFormGroup', () => {
formArray = new TemplatedFormGroup(formTemplate);
});
type Test = [ (value: any) => void, string];
type Test = [
(value: any) => void,
string,
];
const methods: Test[] = [
[x => formArray.setValue(x), 'setValue'],

12
frontend/src/app/framework/angular/forms/transform-input.directive.ts

@ -52,12 +52,16 @@ export class TransformInputDirective implements ControlValueAccessor {
) {
}
@HostListener('input', ['$event.target.value'])
public onChange(value: any) {
const normalizedValue = this.transformValue(value);
@HostListener('input', ['$event'])
public onChange(event: Event): void {
const target = event.target as HTMLInputElement;
if (target?.value === undefined) {
return;
}
this.renderer.setProperty(this.element.nativeElement, 'value', normalizedValue);
const normalizedValue = this.transformValue(target.value);
this.renderer.setProperty(this.element.nativeElement, 'value', normalizedValue);
this.callChange(normalizedValue);
}

2
frontend/src/app/framework/angular/forms/validators.ts

@ -66,7 +66,7 @@ export module ValidatorsEx {
if (v) {
try {
DateTime.parseISO(v);
} catch (e) {
} catch {
return { validdatetime: false };
}
}

4
frontend/src/app/framework/angular/http/http-extensions.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable no-inner-declarations */
import { HttpClient, HttpErrorResponse, HttpEvent, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http';
import { catchError, map, Observable, throwError } from 'rxjs';
import { ErrorDto, getActualVersion, Types, Versioned, VersionOrTag, VersionTag } from '@app/framework/internal';
@ -130,7 +128,7 @@ export function parseError(response: HttpErrorResponse, fallback: string) {
} else if (Types.isString(error)) {
try {
parsed = JSON.parse(error);
} catch (e) {
} catch {
parsed = undefined;
}
}

2
frontend/src/app/framework/angular/layout-container.directive.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable import/no-cycle */
import { AfterViewInit, Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
import { LayoutComponent } from './layout.component';

2
frontend/src/app/framework/angular/loader.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable import/no-cycle */
import { ChangeDetectionStrategy, Component, Input, numberAttribute } from '@angular/core';
@Component({

5
frontend/src/app/framework/angular/long-hover.directive.stories.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { action } from '@storybook/addon-actions';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { LongHoverDirective } from '@app/framework';
@ -45,16 +44,12 @@ type Story = StoryObj<LongHoverDirective>;
export const Default: Story = {
args: {
hover: action('Hover'),
selector: '',
cancelled: action('Cancelled'),
},
};
export const Selector: Story = {
args: {
hover: action('Hover'),
selector: 'button',
cancelled: action('Cancelled'),
},
};

6
frontend/src/app/framework/angular/markdown.directive.spec.ts

@ -92,13 +92,9 @@ describe('MarkdownDirective', () => {
function verifyTextRender(text: string) {
renderer.verify(x => x.setProperty(It.isAny(), 'textContent', text), Times.once());
expect().nothing();
}
function verifyHtmlRender(text: string) {
renderer.verify(x => x.setProperty(It.isAny(), 'innerHTML', text), Times.once());
expect().nothing();
}
});
});

2
frontend/src/app/framework/angular/menu.stories.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { LocalizerService, ResizeService } from '@app/framework/internal';
import { MenuItemComponent } from './menu-item.component';
@ -33,7 +32,6 @@ export default {
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
MenuItemComponent,
RootViewComponent,
],

2
frontend/src/app/framework/angular/modals/dialog-renderer.component.html

@ -22,7 +22,7 @@
<div class="notification-container notification-container-bottom-right">
@for (notification of snapshot.notifications; track notification) {
<div class="alert alert-light alert-dismissible border shadow-sm timed" @fade (click)="close(notification)" role="alert">
<div class="alert alert-light alert-dismissible border shadow-sm timed" animate.enter="fade-in" animate.leave="fade-out" (click)="close(notification)" role="alert">
<button class="btn-sm btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button>
<div class="timer timer-{{ notification.messageType }}"></div>
<div [sqxMarkdown]="notification.message | sqxTranslate"></div>

5
frontend/src/app/framework/angular/modals/dialog-renderer.component.ts

@ -9,7 +9,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { timer } from 'rxjs';
import { DialogModel, DialogRequest, DialogService, fadeAnimation, Notification, StatefulComponent, Subscriptions, Tooltip } from '@app/framework/internal';
import { DialogModel, DialogRequest, DialogService, Notification, StatefulComponent, Subscriptions, Tooltip } from '@app/framework/internal';
import { FocusOnInitDirective } from '../forms/focus-on-init.directive';
import { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe';
@ -33,9 +33,6 @@ interface State {
selector: 'sqx-dialog-renderer',
styleUrls: ['./dialog-renderer.component.scss'],
templateUrl: './dialog-renderer.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FocusOnInitDirective,

7
frontend/src/app/framework/angular/modals/dialog-renderer.stories.ts

@ -6,7 +6,6 @@
*/
import { Component, inject, Input } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { ConfirmClickDirective, DialogRendererComponent, DialogService, ErrorDto, LocalizerService, RootViewComponent, TooltipDirective } from '@app/framework';
@ -18,10 +17,7 @@ type TestMode = 'ErrorText' | 'ErrorDetails' | 'Info';
<button class="btn btn-primary" (click)="test()">
Show {{ mode }}
</button>
`,
imports: [
DialogRendererComponent,
],
`
})
class TestComponent {
public readonly dialogs = inject(DialogService);
@ -57,7 +53,6 @@ export default {
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
ConfirmClickDirective,
RootViewComponent,
TestComponent,

2
frontend/src/app/framework/angular/modals/modal-dialog.component.html

@ -1,4 +1,4 @@
<div class="modal" @fade data-testid="dialog">
<div class="modal" animate.enter="fade-in" animate.leave="fade-out" data-testid="dialog">
<div class="modal-dialog modal-{{ size }}" [class.modal-fh]="fullHeight">
<div class="modal-content" [sqxTourStep]="tourId">
@if (showHeader) {

4
frontend/src/app/framework/angular/modals/modal-dialog.component.ts

@ -7,7 +7,6 @@
import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2, ViewChild } from '@angular/core';
import { fadeAnimation } from '@app/framework/internal';
import { TranslatePipe } from '../pipes/translate.pipe';
import { ShortcutComponent } from '../shortcut.component';
import { TourStepDirective } from './tour-step.directive';
@ -16,9 +15,6 @@ import { TourStepDirective } from './tour-step.directive';
selector: 'sqx-modal-dialog',
styleUrls: ['./modal-dialog.component.scss'],
templateUrl: './modal-dialog.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.Default,
imports: [
ShortcutComponent,

2
frontend/src/app/framework/angular/modals/modal-placement.directive.ts

@ -158,7 +158,7 @@ export class ModalPlacementDirective implements AfterViewInit, OnDestroy {
if (this.adjustHeight) {
const height = rects.reference.height + 2 * this.spaceY;
this.renderer.setStyle(modalRef, 'width', `${height}px`);
this.renderer.setStyle(modalRef, 'height', `${height}px`);
}
},
}));

2
frontend/src/app/framework/angular/modals/modal.directive.ts

@ -183,7 +183,7 @@ export class ModalDirective<T = unknown> implements OnDestroy {
}
return false;
} catch (ex) {
} catch {
return false;
}
}

3
frontend/src/app/framework/angular/modals/tooltip.directive.ts

@ -112,9 +112,8 @@ export class TooltipDirective implements OnDestroy {
private unsetAttribute() {
try {
this.renderer.setProperty(this.target, 'title', '');
return true;
} catch (ex) {
} catch {
return false;
}
}

2
frontend/src/app/framework/angular/modals/tour-template.component.html

@ -1,6 +1,6 @@
<div
class="tour-step"
@fade
animate.enter="fade-in" animate.leave="fade-out"
[class.d-none]="!isVisible"
offset="4"
[position]="currentStep.position || 'bottom-start'"

5
frontend/src/app/framework/angular/modals/tour-template.component.ts

@ -7,7 +7,7 @@
import { AfterContentInit, ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { fadeAnimation, StatefulComponent } from '@app/framework/internal';
import { StatefulComponent } from '@app/framework/internal';
import { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe';
import { ModalPlacementDirective } from './modal-placement.directive';
@ -18,9 +18,6 @@ import { StepDefinition, TourService } from './tour.service';
selector: 'sqx-tour-template',
styleUrls: ['./tour-template.component.scss'],
templateUrl: './tour-template.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MarkdownDirective,

32
frontend/src/app/framework/angular/pager.component.spec.ts

@ -14,8 +14,8 @@ describe('Pager', () => {
pager.paging = { page: 0, pageSize: 10, count: 0, total: 0 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse();
expect(pager.canGoPrev).toBeFalse();
expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBe(false);
expect(pager.itemFirst).toEqual(0);
expect(pager.itemLast).toEqual(0);
@ -27,8 +27,8 @@ describe('Pager', () => {
pager.paging = { page: 0, pageSize: 10, count: 10, total: 100 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeTrue();
expect(pager.canGoPrev).toBeFalse();
expect(pager.canGoNext).toBe(true);
expect(pager.canGoPrev).toBe(false);
expect(pager.itemFirst).toEqual(1);
expect(pager.itemLast).toEqual(10);
@ -40,8 +40,8 @@ describe('Pager', () => {
pager.paging = { page: 4, pageSize: 10, count: 10, total: 100 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeTrue();
expect(pager.canGoPrev).toBeTrue();
expect(pager.canGoNext).toBe(true);
expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(41);
expect(pager.itemLast).toEqual(50);
@ -53,8 +53,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 10, total: 100 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse();
expect(pager.canGoPrev).toBeTrue();
expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(100);
@ -66,8 +66,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 4, total: 100 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse();
expect(pager.canGoPrev).toBeTrue();
expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(94);
@ -79,8 +79,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 9, total: 100 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse();
expect(pager.canGoPrev).toBeTrue();
expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(99);
@ -92,8 +92,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 10, total: -1 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeTrue();
expect(pager.canGoPrev).toBeTrue();
expect(pager.canGoNext).toBe(true);
expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(100);
@ -105,8 +105,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 5, total: -1 };
pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse();
expect(pager.canGoPrev).toBeTrue();
expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(95);

12
frontend/src/app/framework/angular/pipes/date-time.pipes.spec.ts

@ -13,12 +13,12 @@ const dateTime = DateTime.parseISO(dateSource, false);
const dateString = dateTime.toISOString();
const TestCases = [{
value: dateString,
name: 'String',
}, {
value: dateTime,
name: 'DateTime',
}];
value: dateString,
name: 'String',
}, {
value: dateTime,
name: 'DateTime',
}];
describe('DurationPipe', () => {
beforeEach(() => {

1
frontend/src/app/framework/angular/pipes/numbers.pipes.ts

@ -49,6 +49,5 @@ export function calculateFileSize(value: number, factor = 1024) {
u++;
}
// eslint-disable-next-line prefer-template
return (u ? `${value.toFixed(1)} ` : value) + ' kMGTPEZY'[u] + 'B';
}

4
frontend/src/app/framework/angular/routers/router-2-state.spec.ts

@ -236,7 +236,7 @@ describe('Router2State', () => {
state2: { squidex: true, cms: true },
});
expect(routeExtras!.replaceUrl).toBeTrue();
expect(routeExtras!.replaceUrl).toBe(true);
expect(routeExtras!.queryParamsHandling).toBe('merge');
expect(routeExtras!.queryParams).toEqual({ state1: 'hello', state2: 'squidex,cms' });
@ -259,7 +259,7 @@ describe('Router2State', () => {
state2: { squidex: true, cms: true },
});
expect(routeExtras!.replaceUrl).toBeTrue();
expect(routeExtras!.replaceUrl).toBe(true);
expect(routeExtras!.queryParamsHandling).toBe('merge');
expect(routeExtras!.queryParams).toEqual({ state1: 'hello', state2: 'squidex,cms' });

4
frontend/src/app/framework/angular/shortcut.component.spec.ts

@ -26,15 +26,11 @@ describe('ShortcutComponent', () => {
it('should init without keys', () => {
shortcutComponent.keys = null!;
shortcutComponent.ngOnInit();
expect().nothing();
});
it('should destroy without keys', () => {
shortcutComponent.keys = null!;
shortcutComponent.ngOnDestroy();
expect().nothing();
});
it('should raise event if triggered', () => {

4
frontend/src/app/framework/angular/shortcut.directive.spec.ts

@ -23,15 +23,11 @@ describe('ShortcutDirective', () => {
it('should init without keys', () => {
shortcutDirective.shortcut = null!;
shortcutDirective.ngOnInit();
expect().nothing();
});
it('should destroy without keys', () => {
shortcutDirective.shortcut = null!;
shortcutDirective.ngOnDestroy();
expect().nothing();
});
it('should raise event if triggered', () => {

2
frontend/src/app/framework/angular/stateful.component.ts

@ -23,7 +23,7 @@ export abstract class StatefulComponent<T extends {} = object> extends State<T>
try {
this.changeDetector.detectChanges();
} catch {
return;
/* NOOP */
}
});
}

8
frontend/src/app/framework/angular/title.component.spec.ts

@ -40,8 +40,6 @@ describe('TitleComponent', () => {
titleComponent.message = 'title1';
titleComponent.ngOnChanges();
expect().nothing();
titleService.verify(x => x.push('title1', undefined, 'my-url'), Times.once());
});
@ -52,8 +50,6 @@ describe('TitleComponent', () => {
titleComponent.message = 'title2';
titleComponent.ngOnChanges();
expect().nothing();
titleService.verify(x => x.push('title2', 0, 'my-url'), Times.once());
});
@ -62,8 +58,6 @@ describe('TitleComponent', () => {
titleComponent.ngOnChanges();
titleComponent.ngOnDestroy();
expect().nothing();
titleService.verify(x => x.pop(), Times.once());
});
@ -72,8 +66,6 @@ describe('TitleComponent', () => {
titleComponent.ngOnChanges();
titleComponent.ngOnDestroy();
expect().nothing();
titleService.verify(x => x.pop(), Times.never());
});
});

1
frontend/src/app/framework/internal.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export * from './angular/animations';
export * from './angular/drag-helper';
export * from './angular/routers/router-utils';
export * from './angular/stateful.component';

2
frontend/src/app/framework/services/dialog.service.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable deprecation/deprecation */
import { Injectable } from '@angular/core';
import { Observable, ReplaySubject, Subject, throwError } from 'rxjs';
import { ErrorDto } from '../utils/error';

12
frontend/src/app/framework/services/loading.service.spec.ts

@ -61,7 +61,7 @@ describe('LoadingService', () => {
expect(state).toBeTruthy();
});
it('should not unset from loaded delayed', (cb) => {
it('should not unset from loaded delayed', async () => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
@ -75,11 +75,11 @@ describe('LoadingService', () => {
setTimeout(() => {
expect(state).toBeFalsy();
cb();
}, 400);
});
it('should not unset from loaded delayed on navigation event', (cb) => {
it('should not unset from loaded delayed on navigation event', async () => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
@ -93,11 +93,11 @@ describe('LoadingService', () => {
setTimeout(() => {
expect(state).toBeFalsy();
cb();
}, 400);
});
it('should set back to loaded after several completions', (cb) => {
it('should set back to loaded after several completions', async () => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
@ -113,7 +113,7 @@ describe('LoadingService', () => {
setTimeout(() => {
expect(state).toBeTruthy();
cb();
}, 400);
});
});

6
frontend/src/app/framework/services/local-store.service.ts

@ -22,7 +22,7 @@ export class LocalStoreService {
public get(key: string): string | null {
try {
return this.store.getItem(key);
} catch (e) {
} catch {
return this.fallback[key];
}
}
@ -52,7 +52,7 @@ export class LocalStoreService {
public set(key: string, value: string) {
try {
this.store.setItem(key, value);
} catch (e) {
} catch {
this.fallback[key] = value;
}
}
@ -72,7 +72,7 @@ export class LocalStoreService {
public remove(key: string) {
try {
this.store.removeItem(key);
} catch (e) {
} catch {
delete this.fallback[key];
}
}

6
frontend/src/app/framework/services/message-bus.service.spec.ts

@ -8,8 +8,10 @@
import { NgZone } from '@angular/core';
import { MessageBus } from './message-bus.service';
class Event1 {}
class Event2 {}
class Event1 {
}
class Event2 {
}
describe('MessageBus', () => {
const zone = {

18
frontend/src/app/framework/services/toolbar.service.spec.ts

@ -25,8 +25,8 @@ describe('ToolbarService', () => {
buttonsTriggered++;
});
toolbarService.addButton(undefined, 'button1', () => {});
toolbarService.addButton(undefined, 'button2', () => {});
toolbarService.addButton(undefined, 'button1', () => { });
toolbarService.addButton(undefined, 'button2', () => { });
expect(buttons!.length).toBe(2);
expect(buttonsTriggered).toEqual(3);
@ -43,8 +43,8 @@ describe('ToolbarService', () => {
buttonsTriggered++;
});
toolbarService.addButton(undefined, 'button1', () => {});
toolbarService.addButton(undefined, 'button1', () => {}, { disabled: true });
toolbarService.addButton(undefined, 'button1', () => { });
toolbarService.addButton(undefined, 'button1', () => { }, { disabled: true });
expect(buttons!.length).toBe(1);
expect(buttonsTriggered).toEqual(3);
@ -61,7 +61,7 @@ describe('ToolbarService', () => {
buttonsTriggered++;
});
const action = () => {};
const action = () => { };
toolbarService.addButton(undefined, 'button1', action);
toolbarService.addButton(undefined, 'button1', action);
@ -84,8 +84,8 @@ describe('ToolbarService', () => {
buttonsTriggered++;
});
toolbarService.addButton(owner1, 'button1', () => {});
toolbarService.addButton(owner2, 'button2', () => {});
toolbarService.addButton(owner1, 'button1', () => { });
toolbarService.addButton(owner2, 'button2', () => { });
toolbarService.remove(owner1);
expect(buttons!.length).toBe(1);
@ -103,8 +103,8 @@ describe('ToolbarService', () => {
buttonsTriggered++;
});
toolbarService.addButton(undefined, 'button1', () => {});
toolbarService.addButton(undefined, 'button2', () => {});
toolbarService.addButton(undefined, 'button1', () => { });
toolbarService.addButton(undefined, 'button2', () => { });
toolbarService.removeAll();
expect(buttons!.length).toBe(0);

8
frontend/src/app/framework/utils/array-extensions.spec.ts

@ -139,15 +139,15 @@ describe('ArrayExtensions', () => {
it('should return true if array include item', () => {
const array_0 = [0, 1, 2];
expect(array_0.includes(0)).toBeTrue();
expect(array_0.includes(1)).toBeTrue();
expect(array_0.includes(0)).toBe(true);
expect(array_0.includes(1)).toBe(true);
});
it('should return false if array does not include item', () => {
const array_0 = [0, 1, 2];
expect(array_0.includes(3)).toBeFalse();
expect(array_0.includes(4)).toBeFalse();
expect(array_0.includes(3)).toBe(false);
expect(array_0.includes(4)).toBe(false);
});
it('should convert to map', () => {

2
frontend/src/app/framework/utils/color-helper.spec.ts

@ -35,4 +35,4 @@ describe('ColorHelper', () => {
expect(color1_1).toEqual(color1_2);
expect(color1_1).not.toEqual(color2);
});
});
});

2
frontend/src/app/framework/utils/interpolator.spec.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable no-template-curly-in-string */
import { DateTime } from './date-time';
import { interpolate } from './interpolator';

2
frontend/src/app/framework/utils/keys.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable deprecation/deprecation */
export module Keys {
const ALT = 18;
const CONTROL = 17;

6
frontend/src/app/framework/utils/markdown-transform.spec.ts

@ -91,10 +91,8 @@ describe('MarkdownTransform', () => {
it('should transform multiple images', async () => {
const md = `
# Header 1
![Alt1](https://squidex.io/image1.png "Picture1")
![Alt2](https://squidex.io/image2.png "Picture2")
## Header 2
`;
@ -102,11 +100,9 @@ describe('MarkdownTransform', () => {
expect(result).toEqual(`
# Header 1
![Alt1](https://squidex.io/image1.png?transformed "Picture1")
![Alt2](https://squidex.io/image2.png?transformed "Picture2")
## Header 2
`);
});
});
});

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save