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 8 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 # Stage 2, Build Frontend
# #
FROM squidex/frontend-build:20.9 AS frontend FROM squidex/frontend-build:22.19 AS frontend
WORKDIR /src 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 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 = { module.exports = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [ addons: [
'@storybook/addon-actions',
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions" "@storybook/addon-interactions"
], ],
framework: { framework: {

39
frontend/angular.json

@ -18,7 +18,7 @@
"prefix": "sqx", "prefix": "sqx",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:application", "builder": "@angular/build:application",
"options": { "options": {
"outputPath": "build", "outputPath": "build",
"index": "src/index.html", "index": "src/index.html",
@ -107,7 +107,7 @@
"./src/app/theme" "./src/app/theme"
], ],
"sass": { "sass": {
"silenceDeprecations": ["color-functions", "global-builtin", "import"] "silenceDeprecations": ["color-functions", "global-builtin", "import", "if-function"]
} }
}, },
"scripts": [] "scripts": []
@ -152,7 +152,7 @@
"defaultConfiguration": "production" "defaultConfiguration": "production"
}, },
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular/build:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"buildTarget": "squidex:build:production" "buildTarget": "squidex:build:production"
@ -170,29 +170,7 @@
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular/build:unit-test"
"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": []
}
}, },
"storybook": { "storybook": {
"builder": "@storybook/angular:start-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": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "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", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"test": "ng test", "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", "watch": "ng build --watch --configuration development",
"storybook": "ng run squidex:storybook", "storybook": "ng run squidex:storybook",
"storybook-build": "ng run squidex:build-storybook", "storybook-build": "ng run squidex:build-storybook",
@ -16,44 +16,43 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "19.2.14", "@angular/cdk": "21.1.3",
"@angular/cdk": "19.2.18", "@angular/cdk-experimental": "21.1.3",
"@angular/cdk-experimental": "19.2.18", "@angular/common": "21.1.3",
"@angular/common": "19.2.14", "@angular/core": "21.1.3",
"@angular/core": "19.2.14", "@angular/forms": "21.1.3",
"@angular/forms": "19.2.14", "@angular/localize": "21.1.3",
"@angular/localize": "19.2.14", "@angular/platform-browser": "21.1.3",
"@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "21.1.3",
"@angular/platform-browser-dynamic": "19.2.14", "@angular/platform-server": "21.1.3",
"@angular/platform-server": "19.2.14", "@angular/router": "21.1.3",
"@angular/router": "19.2.14",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@floating-ui/dom": "^1.7.1", "@floating-ui/dom": "^1.7.5",
"@graphiql/toolkit": "^0.11.3", "@graphiql/toolkit": "^0.11.3",
"@iharbeck/ngx-virtual-scroller": "^19.0.1", "@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@lithiumjs/angular": "^8.0.1", "@lithiumjs/angular": "^9.0.0",
"@sindresorhus/slugify": "^3.0.0", "@sindresorhus/slugify": "^3.0.0",
"ace-builds": "^1.42.0", "ace-builds": "^1.43.6",
"angular-gridster2": "19.0.0", "angular-gridster2": "21.0.1",
"bootstrap": "5.3.6", "bootstrap": "5.3.6",
"core-js": "3.42.0", "core-js": "3.48.0",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"graphiql": "4.1.1", "graphiql": "4.1.1",
"graphql": "16.11.0", "graphql": "16.12.0",
"graphql-ws": "^6.0.5", "graphql-ws": "^6.0.7",
"image-focus": "1.2.1", "image-focus": "1.2.1",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-control-geocoder": "3.2.0", "leaflet-control-geocoder": "3.3.1",
"marked": "15.0.12", "marked": "15.0.12",
"mersenne-twister": "1.1.0", "mersenne-twister": "1.1.0",
"mousetrap": "1.6.5", "mousetrap": "1.6.5",
"ng2-charts": "^8.0.0", "ng2-charts": "^8.0.0",
"ngx-doc-viewer": "15.0.1", "ngx-doc-viewer": "15.0.1",
"ngx-inline-filter": "^0.2.3", "ngx-inline-filter": "^0.2.5",
"ngx-scrollbar": "^18.0.0", "ngx-scrollbar": "^19.1.4",
"ngx-ui-tour-core": "14.0.0", "ngx-ui-tour-core": "16.0.0",
"oidc-client-ts": "^3.2.1", "oidc-client-ts": "^3.4.1",
"progressbar.js": "1.1.1", "progressbar.js": "1.1.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@ -62,66 +61,59 @@
"tslib": "2.8.1", "tslib": "2.8.1",
"tui-calendar": "^1.15.3", "tui-calendar": "^1.15.3",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"video.js": "8.23.3", "video.js": "8.23.6",
"vis-data": "7.1.9", "vis-data": "8.0.3",
"vis-network": "9.1.10", "vis-network": "10.0.2",
"vis-util": "5.0.7", "vis-util": "6.0.0",
"y-protocols": "^1.0.6", "y-protocols": "^1.0.7",
"y-websocket": "^3.0.0", "y-websocket": "^3.0.0",
"zone.js": "0.15.1" "zone.js": "0.16.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/architect": "^0.1902.14", "@angular-devkit/architect": "^0.2101.3",
"@angular-devkit/build-angular": "^19.2.14", "@angular-devkit/build-angular": "^21.1.3",
"@angular-eslint/builder": "19.7.1", "@angular-eslint/builder": "21.2.0",
"@angular-eslint/eslint-plugin": "19.7.1", "@angular-eslint/eslint-plugin": "21.2.0",
"@angular-eslint/eslint-plugin-template": "19.7.1", "@angular-eslint/eslint-plugin-template": "21.2.0",
"@angular-eslint/schematics": "19.7.1", "@angular-eslint/schematics": "21.2.0",
"@angular-eslint/template-parser": "19.7.1", "@angular-eslint/template-parser": "21.2.0",
"@angular/cli": "^19.2.14", "@angular/cli": "^21.1.3",
"@angular/compiler": "^19.2.14", "@angular/compiler": "^21.1.3",
"@angular/compiler-cli": "^19.2.14", "@angular/compiler-cli": "^21.1.3",
"@angular/elements": "^19.2.14", "@angular/elements": "^21.1.3",
"@compodoc/compodoc": "^1.1.26", "@compodoc/compodoc": "^1.2.1",
"@storybook/addon-actions": "^8.6.14", "@storybook/addon-links": "^10.2.7",
"@storybook/addon-essentials": "^8.6.14", "@storybook/angular": "^10.2.7",
"@storybook/addon-interactions": "^8.6.14",
"@storybook/addon-links": "^8.6.14",
"@storybook/angular": "^8.6.14",
"@storybook/testing-library": "^0.2.2", "@storybook/testing-library": "^0.2.2",
"@types/ace": "^0.0.52", "@types/ace": "^0.0.52",
"@types/codemirror": "5.60.16", "@types/codemirror": "5.60.17",
"@types/core-js": "2.5.8", "@types/core-js": "2.5.8",
"@types/jasmine": "5.1.8",
"@types/mersenne-twister": "1.1.7", "@types/mersenne-twister": "1.1.7",
"@types/mousetrap": "1.6.15", "@types/mousetrap": "1.6.15",
"@types/node": "22.15.29", "@types/node": "25.2.1",
"@types/react": "18.3.23", "@types/react": "19.2.13",
"@types/react-dom": "18.3.7", "@types/react-dom": "19.2.3",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^8.54.0",
"copy-webpack-plugin": "^13.0.0", "copy-webpack-plugin": "^13.0.1",
"eslint": "^8.57.1", "eslint": "^9.39.2",
"eslint-config-airbnb-typescript": "18.0.0", "eslint-config-airbnb-typescript": "18.0.0",
"eslint-plugin-deprecation": "^2.0.0", "eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-storybook": "^0.12.0", "eslint-plugin-storybook": "^10.2.7",
"jasmine-core": "~5.7.1", "jsdom": "^28.0.0",
"karma": "~6.4.4", "prettier": "3.8.1",
"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",
"squidex-prettier-plugin-organize-attributes": "^1.0.1", "squidex-prettier-plugin-organize-attributes": "^1.0.1",
"storybook": "^8.6.14", "storybook": "^10.2.7",
"stylelint": "16.20.0", "stylelint": "17.1.1",
"stylelint-config-standard": "38.0.0", "stylelint-config-standard": "40.0.0",
"stylelint-config-standard-scss": "^15.0.1", "stylelint-config-standard-scss": "^17.0.0",
"stylelint-scss": "6.12.0", "stylelint-scss": "7.0.0",
"typescript": "5.8.3" "typescript": "5.9.3",
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^4.0.18"
}, },
"overrides": { "overrides": {
"ng2-charts": { "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, () => { it(name, () => {
return TestBed.runInInjectionContext(() => assertion()); 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', () => { describe('EventConsumersService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
EventConsumersService, EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
], ],
}); });
}); });
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify(); httpMock.verify();
})); }));
it('should make get request to get event consumers', it('should make get request to get event consumers', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { let eventConsumers: EventConsumersDto;
let eventConsumers: EventConsumersDto; eventConsumersService.getEventConsumers().subscribe(result => {
eventConsumersService.getEventConsumers().subscribe(result => { eventConsumers = result;
eventConsumers = result; });
});
const req = httpMock.expectOne('http://service/p/api/event-consumers');
const req = httpMock.expectOne('http://service/p/api/event-consumers');
expect(req.request.method).toEqual('GET');
expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
req.flush({ items: [
items: [ eventConsumerResponse(12),
eventConsumerResponse(12), eventConsumerResponse(13),
eventConsumerResponse(13), ],
], _links: {},
_links: {}, });
});
expect(eventConsumers!).toEqual(new EventConsumersDto({
expect(eventConsumers!).toEqual( items: [
new EventConsumersDto({ createEventConsumer(12),
items: [ createEventConsumer(13),
createEventConsumer(12), ],
createEventConsumer(13), _links: {},
],
_links: {},
}));
})); }));
}));
it('should make put request to start event consumer', it('should make put request to start event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { const resource: IResourceDto = {
const resource: IResourceDto = { _links: {
_links: { start: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/start' }),
start: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/start' }), },
}, };
};
let eventConsumer: EventConsumerDto; let eventConsumer: EventConsumerDto;
eventConsumersService.putStart(resource).subscribe(response => { eventConsumersService.putStart(resource).subscribe(response => {
eventConsumer = 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.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull(); 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', it('should make put request to stop event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { const resource: IResourceDto = {
const resource: IResourceDto = { _links: {
_links: { stop: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' }),
stop: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' }), },
}, };
};
let eventConsumer: EventConsumerDto; let eventConsumer: EventConsumerDto;
eventConsumersService.putStop(resource).subscribe(response => { eventConsumersService.putStop(resource).subscribe(response => {
eventConsumer = 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.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull(); 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', it('should make put request to reset event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { const resource: IResourceDto = {
const resource: IResourceDto = { _links: {
_links: { reset: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' }),
reset: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' }), },
}, };
};
let eventConsumer: EventConsumerDto; let eventConsumer: EventConsumerDto;
eventConsumersService.putReset(resource).subscribe(response => { eventConsumersService.putReset(resource).subscribe(response => {
eventConsumer = 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.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull(); 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 = '') { function eventConsumerResponse(id: number, suffix = '') {
const key = `${id}${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` }), 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', () => { describe('UsersService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
UsersService, UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
], ],
}); });
}); });
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify(); httpMock.verify();
})); }));
it('should make get request to get many users', it('should make get request to get many users', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { let users: UsersDto;
let users: UsersDto; userManagementService.getUsers(20, 30).subscribe(result => {
userManagementService.getUsers(20, 30).subscribe(result => { users = result;
users = result; });
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30');
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30');
expect(req.request.method).toEqual('GET');
expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
req.flush({ total: 100,
total: 100, items: [
items: [ userResponse(12),
userResponse(12), userResponse(13),
userResponse(13), ],
], _links: {},
_links: {}, });
});
expect(users!).toEqual(new UsersDto({
expect(users!).toEqual( total: 100,
new UsersDto({ items: [
total: 100, createUser(12),
items: [ createUser(13),
createUser(12), ],
createUser(13), _links: {},
],
_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', req.flush({
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { total: 100,
let users: UsersDto; items: [
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => { userResponse(12),
users = result; userResponse(13),
}); ],
_links: {},
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query'); });
expect(req.request.method).toEqual('GET'); expect(users!).toEqual(new UsersDto({
expect(req.request.headers.get('If-Match')).toBeNull(); total: 100,
items: [
req.flush({ createUser(12),
total: 100, createUser(13),
items: [ ],
userResponse(12), _links: {},
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', it('should make get request to get single user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { let user: UserDto;
let user: UserDto; userManagementService.getUser('123').subscribe(result => {
userManagementService.getUser('123').subscribe(result => { user = 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.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); 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', it('should make post request to create user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { const dto = new CreateUserDto({
const dto = new CreateUserDto({ email: 'mail@squidex.io',
email: 'mail@squidex.io', displayName: 'Squidex User',
displayName: 'Squidex User', permissions: ['Permission1'],
permissions: ['Permission1'], password: 'password',
password: 'password', });
});
let user: UserDto; let user: UserDto;
userManagementService.postUser(dto).subscribe(result => { userManagementService.postUser(dto).subscribe(result => {
user = 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.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull(); 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', it('should make put request to update user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { const dto = new UpdateUserDto({
const dto = new UpdateUserDto({ email: 'mail@squidex.io',
email: 'mail@squidex.io', displayName: 'Squidex User',
displayName: 'Squidex User', permissions: ['Permission1'],
permissions: ['Permission1'], password: 'password',
password: 'password', });
});
const resource: IResourceDto = {
_links: {
update: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123' }),
},
};
let user: UserDto; const resource: IResourceDto = {
userManagementService.putUser(resource, dto).subscribe(result => { _links: {
user = result; 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'); const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.headers.get('If-Match')).toBeNull();
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', expect(user!).toEqual(createUser(12));
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { }));
const resource: IResourceDto = {
_links: {
lock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/lock' }),
},
};
let user: UserDto; it('should make put request to lock user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.lockUser(resource).subscribe(result => { const resource: IResourceDto = {
user = result; _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'); const req = httpMock.expectOne('http://service/p/api/user-management/123/lock');
expect(req.request.headers.get('If-Match')).toBeNull();
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', expect(user!).toEqual(createUser(12));
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { }));
const resource: IResourceDto = {
_links: {
unlock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/unlock' }),
},
};
let user: UserDto; it('should make put request to unlock user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.unlockUser(resource).subscribe(result => { const resource: IResourceDto = {
user = result; _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'); const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock');
expect(req.request.headers.get('If-Match')).toBeNull();
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', expect(user!).toEqual(createUser(12));
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { }));
const resource: IResourceDto = {
_links: { it('should make delete request to delete user', inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
delete: new ResourceLinkDto({ method: 'DELETE', href: 'api/user-management/123' }), 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.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({}); req.flush({});
})); }));
function userResponse(id: number, suffix = '') { function userResponse(id: number, suffix = '') {
const key = `${id}${suffix}`; const key = `${id}${suffix}`;
@ -263,4 +253,4 @@ export function createUser(id: number, suffix = '') {
update: new ResourceLinkDto({ method: 'PUT', href: `/users/${id}` }), 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(); eventConsumersState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
}); });
@ -71,8 +69,6 @@ describe('EventConsumersState', () => {
eventConsumersState.load(true, false).pipe(onErrorResumeNextWith()).subscribe(); eventConsumersState.load(true, false).pipe(onErrorResumeNextWith()).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyError(It.isAny()), Times.once()); 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(); usersState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
}); });
@ -73,8 +71,6 @@ describe('UsersState', () => {
.returns(() => of(new UsersDto({ items: [], total: 200, _links: {} }))).verifiable(); .returns(() => of(new UsersDto({ items: [], total: 200, _links: {} }))).verifiable();
usersState.page({ page: 1, pageSize: 10 }).subscribe(); usersState.page({ page: 1, pageSize: 10 }).subscribe();
expect().nothing();
}); });
it('should load with query if searching', () => { it('should load with query if searching', () => {
@ -204,7 +200,7 @@ describe('UsersState', () => {
describe('Selection', () => { describe('Selection', () => {
beforeEach(() => { 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()); .returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(Times.atLeastOnce());
usersState.load().subscribe(); usersState.load().subscribe();

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

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

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

@ -6,7 +6,7 @@
} }
.card { .card {
@include hover-visible('.deeplinks', inline); @include hover-visible('.deeplinks', flex);
&-title { &-title {
padding-right: 2rem; padding-right: 2rem;
@ -74,4 +74,8 @@
@include box-shadow-outer(0, 3px, 16px, .2); @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 { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { RouterLink } from '@angular/router'; 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({ @Component({
selector: 'sqx-app', selector: 'sqx-app',
@ -35,4 +35,16 @@ export class AppComponent {
public leave = new EventEmitter<AppDto>(); public leave = new EventEmitter<AppDto>();
public dropdown = new ModalModel(); 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> <a class="header-right modal-close force" (click)="cancel()">{{ "tour.skip" | sqxTranslate }}</a>
@if (step === 0) { @if (step === 0) {
<div class="onboarding-step"> <div class="onboarding-step">
<img class="header-left" @fade src="./images/logo-white-small.png" /> <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" @slide> <div class="onboarding-enter-leave text-center" animate.enter="slide-left-in" animate.leave="slide-left-out">
<h1> <h1>
{{ "tour.welcome" | sqxTranslate }} <span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span> {{ "tour.welcome" | sqxTranslate }} <span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1> </h1>
@ -23,14 +23,14 @@
@if (step === 1) { @if (step === 1) {
<div> <div>
<img class="header-left" @fade src="./images/logo-white-small.png" /> <img class="header-left" animate.enter="fade-in" animate.leave="fade-out" src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave" @slide> <div class="onboarding-enter-leave" animate.enter="slide-left-in" animate.leave="slide-left-out">
<form [formGroup]="answersForm" (ngSubmit)="submitAnswers()"> <form [formGroup]="answersForm" (ngSubmit)="submitAnswers()">
<h2>{{ "tour.stepDataTitle" | sqxTranslate }}</h2> <h2>{{ "tour.stepDataTitle" | sqxTranslate }}</h2>
<div [sqxMarkdown]="'tour.stepDataText' | sqxTranslate"></div> <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"> <select class="form-select" id="companyRole" formControlName="companyRole">
<option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option> <option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option> <option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option>
@ -41,7 +41,7 @@
</select> </select>
</sqx-form-row> </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"> <select class="form-select" id="companySize" formControlName="companySize">
<option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option> <option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option>
<option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option> <option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option>
@ -51,7 +51,7 @@
</select> </select>
</sqx-form-row> </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"> <select class="form-select" id="project" formControlName="project">
<option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option> <option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option>
<option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option> <option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option>
@ -71,8 +71,8 @@
@if (step === 2) { @if (step === 2) {
<div class="onboarding-step"> <div class="onboarding-step">
<img class="header-left" @fade src="./images/logo-white-small.png" /> <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" @slide> <div class="onboarding-enter-leave text-center" animate.enter="slide-left-in" animate.leave="slide-left-out">
<h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2> <h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2>
<div [sqxMarkdown]="'tour.stepTourText' | sqxTranslate"></div> <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 { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; 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({ @Component({
selector: 'sqx-onboarding-dialog', selector: 'sqx-onboarding-dialog',
styleUrls: ['./onboarding-dialog.component.scss'], styleUrls: ['./onboarding-dialog.component.scss'],
templateUrl: './onboarding-dialog.component.html', templateUrl: './onboarding-dialog.component.html',
animations: [
fadeAnimation, slideAnimation,
],
imports: [ imports: [
FormRowComponent, FormRowComponent,
FormsModule, 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"> <div [class.col-12]="!formModelCompare" [class.col-6]="formModelCompare">
<sqx-focus-marker [controlId]="formModel.path"> <sqx-focus-marker [controlId]="formModel.path">
@if (!(formModel.hiddenChanges | async)) { @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) { @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) { @if (showAllControls) {

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

@ -3,8 +3,7 @@
.table-items-row { .table-items-row {
border-left-width: 4px; border-left-width: 4px;
overflow-x: hidden; padding-bottom: 1.5rem;
overflow-y: hidden;
padding-top: 1.125rem; padding-top: 1.125rem;
} }
@ -22,12 +21,10 @@
.change-marker-host { .change-marker-host {
position: relative; position: relative;
overflow-x: visible;
overflow-y: visible;
} }
.change-marker { .change-marker {
@include absolute(-.5rem, null, null, 1rem); @include absolute(-1.5rem, null, null, 0);
background: $color-white; background: $color-white;
border: 1px solid $color-border; border: 1px solid $color-border;
border-radius: $border-radius; 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; schemaIds = schemas;
} if (this.schemaIds && this.schemaIds.length > 0) { } if (this.schemaIds && this.schemaIds.length > 0) {
schemaIds = this.schemaIds; schemaIds = this.schemaIds;
} }
this.contentsQuery = query; this.contentsQuery = query;
this.contentsCorrelationId = correlationId; 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 { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { GridsterItem } from 'angular-gridster2'; import { GridsterItemConfig } from 'angular-gridster2';
import { take } from 'rxjs/operators'; 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'; 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; public app!: AppDto;
@Input({ required: true }) @Input({ required: true })
public config!: GridsterItem[]; public config!: GridsterItemConfig[];
@Input() @Input()
public configDefaults!: GridsterItem[]; public configDefaults!: GridsterItemConfig[];
@Input() @Input()
public configAvailable!: GridsterItem[]; public configAvailable!: GridsterItemConfig[];
@Input({ transform: booleanAttribute }) @Input({ transform: booleanAttribute })
public needsAttention?: boolean | null; public needsAttention?: boolean | null;
@Output() @Output()
public configChange = new EventEmitter<GridsterItem[]>(); public configChange = new EventEmitter<GridsterItemConfig[]>();
public configOptions: ReadonlyArray<GridsterItem> = []; public configOptions: ReadonlyArray<GridsterItemConfig> = [];
public expertDialog = new DialogModel(); public expertDialog = new DialogModel();
public expertConfig?: GridsterItem[]; public expertConfig?: GridsterItemConfig[];
public dropdownModal = new ModalModel(); public dropdownModal = new ModalModel();
@ -113,7 +113,7 @@ export class DashboardConfigComponent {
this.dialogs.notifyInfo('i18n:dashboard.configSaved'); this.dialogs.notifyInfo('i18n:dashboard.configSaved');
} }
public addOrRemove(item: GridsterItem) { public addOrRemove(item: GridsterItemConfig) {
const current = this.config.find(x => x.type === item.type); const current = this.config.find(x => x.type === item.type);
if (current) { 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); 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" /> <sqx-title message="i18n:dashboard.pageTitle" />
@if (selectedApp | async; as app) { @if (selectedApp | async; as app) {
<div class="dashboard" @fade> <div class="dashboard" animate.enter="fade-in" animate.leave="fade-out">
@if (!isScrolled) { @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> <h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div class="subtext" [sqxMarkdown]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName }"></div> <div class="subtext" [sqxMarkdown]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName }"></div>
@ -24,7 +24,7 @@
<sqx-github-card /> <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" /> <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 { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2'; import { Gridster, GridsterConfig, GridsterItem, GridsterItemConfig, 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 { 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 { ApiCardComponent } from './cards/api-card.component';
import { ContentSummaryCardComponent } from './cards/content-summary-card.component'; import { ContentSummaryCardComponent } from './cards/content-summary-card.component';
import { GithubCardComponent } from './cards/github-card.component'; import { GithubCardComponent } from './cards/github-card.component';
@ -20,9 +20,6 @@ import { DashboardConfigComponent } from './dashboard-config.component';
selector: 'sqx-dashboard-page', selector: 'sqx-dashboard-page',
styleUrls: ['./dashboard-page.component.scss'], styleUrls: ['./dashboard-page.component.scss'],
templateUrl: './dashboard-page.component.html', templateUrl: './dashboard-page.component.html',
animations: [
fadeAnimation,
],
imports: [ imports: [
ApiCallsCardComponent, ApiCallsCardComponent,
ApiCallsSummaryCardComponent, ApiCallsSummaryCardComponent,
@ -37,8 +34,8 @@ import { DashboardConfigComponent } from './dashboard-config.component';
ContentSummaryCardComponent, ContentSummaryCardComponent,
DashboardConfigComponent, DashboardConfigComponent,
GithubCardComponent, GithubCardComponent,
GridsterComponent, Gridster,
GridsterItemComponent, GridsterItem,
HistoryCardComponent, HistoryCardComponent,
IFrameCardComponent, IFrameCardComponent,
MarkdownDirective, MarkdownDirective,
@ -55,7 +52,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
private readonly subscriptions = new Subscriptions(); private readonly subscriptions = new Subscriptions();
@ViewChild('grid') @ViewChild('grid')
public grid!: GridsterComponent; public grid!: Gridster;
public selectedApp = this.appsState.selectedApp.pipe(defined()); public selectedApp = this.appsState.selectedApp.pipe(defined());
@ -67,7 +64,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
public callsUsage?: CallsUsageDto; public callsUsage?: CallsUsageDto;
public gridConfig?: GridsterItem[]; public gridConfig?: GridsterItemConfig[];
public gridConfigAvailable = [...DEFAULT_CELLS, ...ADDITIONAL_CELLS]; public gridConfigAvailable = [...DEFAULT_CELLS, ...ADDITIONAL_CELLS];
public gridConfigDefaults = DEFAULT_CELLS; public gridConfigDefaults = DEFAULT_CELLS;
public gridOptions = DEFAULT_OPTIONS; public gridOptions = DEFAULT_OPTIONS;
@ -136,14 +133,14 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
this.isStacked = value; this.isStacked = value;
} }
public changeConfig(config: GridsterItem[]) { public changeConfig(config: GridsterItemConfig[]) {
this.gridConfig = config; this.gridConfig = config;
this.grid?.updateGrid(); this.grid?.updateGrid();
} }
} }
const DEFAULT_CELLS: GridsterItem[] = [ const DEFAULT_CELLS: GridsterItemConfig[] = [
// Row 1 // Row 1
{ cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.schemasCard', type: 'schemas' }, { 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' }, { 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
/* eslint-disable no-return-assign */
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; 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" /> <sqx-title message="i18n:dashboard.pageTitle" />
@if (selectedTeam | async; as team) { @if (selectedTeam | async; as team) {
<div class="dashboard" @fade> <div class="dashboard" animate.enter="fade-in" animate.leave="fade-out">
@if (!isScrolled) { @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> <h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div class="subtext" [sqxMarkdown]="'dashboard.welcomeTextTeam' | sqxTranslate: { team: team.name }"></div> <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 { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2'; import { Gridster, GridsterConfig, GridsterItem, GridsterItemConfig, 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 { 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'; import { AppsCardComponent } from './cards/apps-card.component';
@Component({ @Component({
selector: 'sqx-dashboard-page', selector: 'sqx-dashboard-page',
styleUrls: ['./dashboard-page.component.scss'], styleUrls: ['./dashboard-page.component.scss'],
templateUrl: './dashboard-page.component.html', templateUrl: './dashboard-page.component.html',
animations: [
fadeAnimation,
],
imports: [ imports: [
ApiCallsCardComponent, ApiCallsCardComponent,
ApiCallsSummaryCardComponent, ApiCallsSummaryCardComponent,
@ -29,8 +26,8 @@ import { AppsCardComponent } from './cards/apps-card.component';
AssetUploadsSizeCardComponent, AssetUploadsSizeCardComponent,
AssetUploadsSizeSummaryCardComponent, AssetUploadsSizeSummaryCardComponent,
AsyncPipe, AsyncPipe,
GridsterComponent, Gridster,
GridsterItemComponent, GridsterItem,
IFrameCardComponent, IFrameCardComponent,
MarkdownDirective, MarkdownDirective,
RandomCatCardComponent, RandomCatCardComponent,
@ -44,7 +41,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
private readonly subscriptions = new Subscriptions(); private readonly subscriptions = new Subscriptions();
@ViewChild('grid') @ViewChild('grid')
public grid!: GridsterComponent; public grid!: Gridster;
public selectedTeam = this.teamsState.selectedTeam.pipe(defined()); public selectedTeam = this.teamsState.selectedTeam.pipe(defined());
@ -56,7 +53,7 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
public callsUsage?: CallsUsageDto; public callsUsage?: CallsUsageDto;
public gridConfig: GridsterItem[] = DEFAULT_CONFIG; public gridConfig: GridsterItemConfig[] = DEFAULT_CONFIG;
public gridOptions = DEFAULT_OPTIONS; public gridOptions = DEFAULT_OPTIONS;
public gridDefault = DEFAULT_CONFIG; public gridDefault = DEFAULT_CONFIG;
public gridOptionals = OPTIONAL_CARDS; public gridOptionals = OPTIONAL_CARDS;
@ -121,18 +118,16 @@ export class DashboardPageComponent implements AfterViewInit, OnInit {
public changeIsStacked(value: boolean) { public changeIsStacked(value: boolean) {
this.localStore.setBoolean(Settings.Local.DASHBOARD_CHART_STACKED, value); this.localStore.setBoolean(Settings.Local.DASHBOARD_CHART_STACKED, value);
this.isStacked = value; this.isStacked = value;
} }
public changeConfig(config: GridsterItem[]) { public changeConfig(config: GridsterItemConfig[]) {
this.gridConfig = config; this.gridConfig = config;
this.grid?.updateGrid(); this.grid?.updateGrid();
} }
} }
const DEFAULT_CONFIG: GridsterItem[] = [ const DEFAULT_CONFIG: GridsterItemConfig[] = [
// Row 1 // Row 1
{ cols: 1, rows: 1, x: 0, y: 0, name: 'i18n:dashboard.trafficSummaryCard', type: 'api-traffic-summary' }, { 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' }, { 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(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
TeamContributorsService, TeamContributorsService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
], ],
}); });
}); });
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify(); httpMock.verify();
})); }));
it('should make get request to get team contributors', it('should make get request to get team contributors', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { let contributors: Versioned<ContributorsDto>;
let contributors: Versioned<ContributorsDto>; contributorsService.getContributors('my-team').subscribe(result => {
contributorsService.getContributors('my-team').subscribe(result => { contributors = 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.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(contributorsResponse(1, 2, 3), { req.flush(contributorsResponse(1, 2, 3), {
headers: { headers: {
etag: '2', 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', it('should make post request to assign contributor', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { const dto = new AssignContributorDto({ contributorId: '123', role: 'Owner' });
const dto = new AssignContributorDto({ contributorId: '123', role: 'Owner' });
let contributors: Versioned<ContributorsDto>; let contributors: Versioned<ContributorsDto>;
contributorsService.postContributor('my-team', dto, version).subscribe(result => { contributorsService.postContributor('my-team', dto, version).subscribe(result => {
contributors = 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.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(contributorsResponse(1, 2, 3), { req.flush(contributorsResponse(1, 2, 3), {
headers: { headers: {
etag: '2', 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', it('should make delete request to remove contributor', inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => {
inject([TeamContributorsService, HttpTestingController], (contributorsService: TeamContributorsService, httpMock: HttpTestingController) => { const resource: Resource = {
const resource: Resource = { _links: {
_links: { delete: { method: 'DELETE', href: '/api/teams/my-team/contributors/123' },
delete: { method: 'DELETE', href: '/api/teams/my-team/contributors/123' }, },
}, };
};
let contributors: Versioned<ContributorsDto>; let contributors: Versioned<ContributorsDto>;
contributorsService.deleteContributor('my-team', resource, version).subscribe(result => { contributorsService.deleteContributor('my-team', resource, version).subscribe(result => {
contributors = 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.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(contributorsResponse(1, 2, 3), { req.flush(contributorsResponse(1, 2, 3), {
headers: { headers: {
etag: '2', 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[]) { function contributorsResponse(...ids: number[]) {
return { return {
@ -147,4 +144,4 @@ export function createContributor(id: number) {
update: new ResourceLinkDto({ method: 'PUT', href: `/contributors/id${id}` }), 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 { Observable } from 'rxjs';
import { ApiUrlConfig, AssignContributorDto, ContributorsDto, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/shared'; import { ApiUrlConfig, AssignContributorDto, ContributorsDto, HTTP, mapVersioned, pretifyError, Resource, Versioned, VersionOrTag } from '@app/shared';
@Injectable({ @Injectable()
providedIn: 'any',
})
export class TeamContributorsService { export class TeamContributorsService {
constructor( constructor(
private readonly http: HttpClient, private readonly http: HttpClient,

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

@ -16,38 +16,78 @@ describe('TeamPlansService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
TeamPlansService, TeamPlansService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
], ],
}); });
}); });
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify(); httpMock.verify();
})); }));
it('should make get request to get team plans', it('should make get request to get team plans', inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => {
inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => { let plans: Versioned<PlansDto>;
let plans: Versioned<PlansDto>; plansService.getPlans('my-team').subscribe(result => {
plansService.getPlans('my-team').subscribe(result => { plans = 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'); req.flush({
expect(req.request.headers.get('If-Match')).toBeNull(); 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', currentPlanId: 'free',
portalLink: 'link/to/portal', portalLink: 'link/to/portal',
planOwner: '456', planOwner: '456',
plans: [ plans: [
{ new PlanDto({
id: 'free', id: 'free',
name: 'Free', name: 'Free',
costs: '14 €', costs: '14 €',
@ -59,8 +99,8 @@ describe('TeamPlansService', () => {
maxApiCalls: 1000, maxApiCalls: 1000,
maxAssetSize: 1500, maxAssetSize: 1500,
maxContributors: 2500, maxContributors: 2500,
}, }),
{ new PlanDto({
id: 'professional', id: 'professional',
name: 'Professional', name: 'Professional',
costs: '18 €', costs: '18 €',
@ -72,72 +112,30 @@ describe('TeamPlansService', () => {
maxApiCalls: 4000, maxApiCalls: 4000,
maxAssetSize: 5500, maxAssetSize: 5500,
maxContributors: 6500, maxContributors: 6500,
}, }),
], ],
referral: { code: 'CODE', earned: '0', condition: 'None' }, referral: new ReferralInfoDto({ code: 'CODE', earned: '0', condition: 'None' }),
locked: 'ManagedByTeam', locked: 'ManagedByTeam',
}, { }),
headers: { version: new VersionTag('2'),
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'),
});
}));
it('should make put request to change plan', it('should make put request to change plan', inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => {
inject([TeamPlansService, HttpTestingController], (plansService: TeamPlansService, httpMock: HttpTestingController) => { const dto = new ChangePlanDto({ planId: 'enterprise' });
const dto = new ChangePlanDto({ planId: 'enterprise' });
let planChanged: PlanChangedDto; let planChanged: PlanChangedDto;
plansService.putPlan('my-team', dto, version).subscribe(result => { plansService.putPlan('my-team', dto, version).subscribe(result => {
planChanged = result.payload; 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.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value); 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 { Observable } from 'rxjs';
import { ApiUrlConfig, ChangePlanDto, HTTP, mapVersioned, PlanChangedDto, PlansDto, pretifyError, Versioned, VersionOrTag } from '@app/shared'; import { ApiUrlConfig, ChangePlanDto, HTTP, mapVersioned, PlanChangedDto, PlansDto, pretifyError, Versioned, VersionOrTag } from '@app/shared';
@Injectable({ @Injectable()
providedIn: 'any',
})
export class TeamPlansService { export class TeamPlansService {
constructor( constructor(
private readonly http: HttpClient, 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
/* eslint-disable no-useless-escape */
import { UntypedFormControl, Validators } from '@angular/forms'; import { UntypedFormControl, Validators } from '@angular/forms';
import { map, shareReplay } from 'rxjs/operators'; import { map, shareReplay } from 'rxjs/operators';
import { ExtendedFormGroup, Form, value$ } from '@app/framework'; 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'; import { TeamAuthState } from '../internal';
describe('TeamAuthState', () => { describe('TeamAuthState', () => {
const { const { newVersion, team, teamsState, version } = TestValues;
newVersion,
team,
teamsState,
version,
} = TestValues;
const scheme = new AuthSchemeDto({ const scheme = new AuthSchemeDto({
domain: 'squidex.io', domain: 'squidex.io',
@ -72,8 +67,6 @@ describe('TeamAuthState', () => {
authState.load(true).subscribe(); authState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); 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; version: VersionTag;
} }
@Injectable({ @Injectable()
providedIn: 'any',
})
export class TeamAuthState extends State<Snapshot> { export class TeamAuthState extends State<Snapshot> {
public scheme = public scheme =
this.project(x => x.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; 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; 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'; import { TeamContributorsService, TeamContributorsState } from '../internal';
describe('TeamContributorsState', () => { describe('TeamContributorsState', () => {
const { const { team, teamsState, newVersion, version } = TestValues;
team,
teamsState,
newVersion,
version,
} = TestValues;
const allIds: number[] = []; const allIds: number[] = [];
@ -73,8 +68,6 @@ describe('TeamContributorsState', () => {
it('should not load if already loaded', () => { it('should not load if already loaded', () => {
contributorsState.load(true).subscribe(); contributorsState.load(true).subscribe();
contributorsState.loadIfNotLoaded().subscribe(); contributorsState.loadIfNotLoaded().subscribe();
expect().nothing();
}); });
it('should only show current page of contributors', () => { it('should only show current page of contributors', () => {
@ -124,8 +117,6 @@ describe('TeamContributorsState', () => {
it('should show notification on load if reload is true', () => { it('should show notification on load if reload is true', () => {
contributorsState.load(true).subscribe(); contributorsState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
}); });
}); });
@ -156,13 +147,11 @@ describe('TeamContributorsState', () => {
let error: ErrorDto; let error: ErrorDto;
contributorsState.assign(request).pipe( contributorsState.assign(request).pipe(catchError(err => {
catchError(err => { error = err;
error = err;
return EMPTY; return EMPTY;
}), })).subscribe();
).subscribe();
expect(error!.message).toBe('i18n:contributors.userNotFound'); expect(error!.message).toBe('i18n:contributors.userNotFound');
}); });
@ -175,13 +164,11 @@ describe('TeamContributorsState', () => {
let error: ErrorDto; let error: ErrorDto;
contributorsState.assign(request).pipe( contributorsState.assign(request).pipe(catchError(err => {
catchError(err => { error = err;
error = err;
return EMPTY; return EMPTY;
}), })).subscribe();
).subscribe();
expect(error!.message).toBe('500'); 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; canCreate?: boolean;
} }
@Injectable({ @Injectable()
providedIn: 'any',
})
export class TeamContributorsState extends State<Snapshot> { export class TeamContributorsState extends State<Snapshot> {
public contributors = public contributors =
this.project(x => x.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'; import { TeamPlansService, TeamPlansState } from '../internal';
describe('TeamPlansState', () => { describe('TeamPlansState', () => {
const { const { creator, newVersion, team, teamsState, version } = TestValues;
creator,
newVersion,
team,
teamsState,
version,
} = TestValues;
const oldPlans = new PlansDto({ const oldPlans = new PlansDto({
currentPlanId: 'free', currentPlanId: 'free',
@ -118,8 +112,6 @@ describe('TeamPlansState', () => {
plansState.load(true).subscribe(); plansState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); 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; version: VersionTag;
} }
@Injectable({ @Injectable()
providedIn: 'any',
})
export class TeamPlansState extends State<Snapshot> { export class TeamPlansState extends State<Snapshot> {
public plans = public plans =
this.project(x => x.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') @HostListener('input')
private resize() { protected resize() {
const textarea = this.element.nativeElement as HTMLTextAreaElement; const textarea = this.element.nativeElement as HTMLTextAreaElement;
// Calculate border height which is not included in scrollHeight // 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 = { export const Image = {
args: { 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 { ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input } from '@angular/core';
import { fadeAnimation } from './animations';
@Component({ @Component({
selector: 'sqx-dropdown-menu', selector: 'sqx-dropdown-menu',
@ -14,18 +13,12 @@ import { fadeAnimation } from './animations';
templateUrl: './dropdown-menu.component.html', templateUrl: './dropdown-menu.component.html',
host: { host: {
class: 'dropdown-menu', class: 'dropdown-menu',
['animate.enter']: 'fade-in',
['animate.leave']: 'fade-out',
}, },
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DropdownMenuComponent { export class DropdownMenuComponent {
@HostBinding('@fade')
public get fade() {
return true;
}
@HostBinding('class') @HostBinding('class')
@Input() @Input()
public class?: string; 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')); 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()); dialogs.verify(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAny()), Times.never());
}); });
@ -55,7 +55,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click')); confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeTrue(); expect(invoked).toBe(true);
}); });
it('Should invoke action when unsubscribed in between', () => { it('Should invoke action when unsubscribed in between', () => {
@ -78,7 +78,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click')); confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeTrue(); expect(invoked).toBe(true);
}); });
it('Should not invoke action when not confirmed', () => { it('Should not invoke action when not confirmed', () => {
@ -97,7 +97,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click')); confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeFalse(); expect(invoked).toBe(false);
}); });
it('Should invoke action when unsubscribed before', () => { it('Should invoke action when unsubscribed before', () => {
@ -116,7 +116,7 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.onClick(new Event('click')); confirmClickDirective.onClick(new Event('click'));
expect(invoked).toBeFalse(); expect(invoked).toBe(false);
}); });
it('Should not confirm when text is empty', () => { it('Should not confirm when text is empty', () => {
@ -126,8 +126,6 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.confirmTitle = 'confirmTitle'; confirmClickDirective.confirmTitle = 'confirmTitle';
confirmClickDirective.onClick(new Event('click')); confirmClickDirective.onClick(new Event('click'));
expect().nothing();
dialogs.verify(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAny()), Times.never()); dialogs.verify(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAny()), Times.never());
}); });
@ -138,8 +136,6 @@ describe('ConfirmClickDirective', () => {
confirmClickDirective.confirmText = 'confirmText'; confirmClickDirective.confirmText = 'confirmText';
confirmClickDirective.onClick(new Event('click')); confirmClickDirective.onClick(new Event('click'));
expect().nothing();
dialogs.verify(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAny()), Times.never()); 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.confirmTitle.length > 0 &&
this.confirmText && this.confirmText &&
this.confirmText.length > 0) { this.confirmText.length > 0) {
// eslint-disable-next-line deprecation/deprecation
const destinations = this.clickConfirmed.observers?.map(x => (x as Subscriber<any>)['destination']) || []; const destinations = this.clickConfirmed.observers?.map(x => (x as Subscriber<any>)['destination']) || [];
this.beforeClick.emit(); this.beforeClick.emit();

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

@ -1,5 +1,5 @@
@if (errorMessages && errorMessages.length > 0) { @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"> <div class="errors">
@for (message of errorMessages; track message) { @for (message of errorMessages; track message) {
{{ 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 { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { fadeAnimation } from '@app/framework/internal';
@Component({ @Component({
selector: 'sqx-errors-messages', selector: 'sqx-errors-messages',
styleUrls: ['./control-errors-messages.component.scss'], styleUrls: ['./control-errors-messages.component.scss'],
templateUrl: './control-errors-messages.component.html', templateUrl: './control-errors-messages.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ControlErrorsMessagesComponent { export class ControlErrorsMessagesComponent {

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

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

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

@ -63,13 +63,12 @@ export class CopyDirective {
private copy() { private copy() {
try { try {
// eslint-disable-next-line deprecation/deprecation
document.execCommand('copy'); document.execCommand('copy');
this.dialogs.notifyInfo('i18n:common.clipboardAdded'); this.dialogs.notifyInfo('i18n:common.clipboardAdded');
return true; return true;
} catch (e) { } catch {
return false; 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { map, Observable, timer } from 'rxjs'; import { map, Observable, timer } from 'rxjs';
import { AutocompleteComponent, AutocompleteSource, LocalizerService, RootViewComponent } from '@app/framework'; import { AutocompleteComponent, AutocompleteSource, LocalizerService, RootViewComponent } from '@app/framework';
@ -48,7 +47,6 @@ export default {
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [ imports: [
BrowserAnimationsModule,
RootViewComponent, RootViewComponent,
], ],
providers: [ 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) { if (isValid) {
try { try {
newValueOut = JSON.parse(newValueText); newValueOut = JSON.parse(newValueText);
} catch (e) { } catch {
newValueOut = null; newValueOut = null;
newValueText = ''; newValueText = '';
} }

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

@ -6,7 +6,6 @@
*/ */
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { action } from '@storybook/addon-actions';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { CodeEditorComponent, ScriptCompletions } from '@app/framework'; import { CodeEditorComponent, ScriptCompletions } from '@app/framework';
@ -131,6 +130,5 @@ export const SingleLine: Story = {
}), }),
args: { args: {
singleLine: true, singleLine: true,
ngModelChange: action('ngModelChange'),
} as any, } 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; let value: DateTime | null;
try { try {
value = DateTime.parseISO(obj, false); value = DateTime.parseISO(obj, false);
} catch (ex) { } catch {
value = null; value = null;
} }

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

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

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

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

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

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

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

@ -25,21 +25,21 @@ describe('ExtendedFormGroup', () => {
describe('UndefinableFormGroup', () => { describe('UndefinableFormGroup', () => {
const tests = [{ const tests = [{
name: 'undefined (on)', name: 'undefined (on)',
undefinable: true, undefinable: true,
valueExpected: undefined, valueExpected: undefined,
valueActual: undefined, valueActual: undefined,
}, { }, {
name: 'defined (on)', name: 'defined (on)',
undefinable: true, undefinable: true,
valueExpected: { field: 1 }, valueExpected: { field: 1 },
valueActual: { field: 1 }, valueActual: { field: 1 },
}, { }, {
name: 'defined (off)', name: 'defined (off)',
undefinable: false, undefinable: false,
valueExpected: { field: 1 }, valueExpected: { field: 1 },
valueActual: { field: 1 }, valueActual: { field: 1 },
}]; }];
it('should initialize with undefined', () => { it('should initialize with undefined', () => {
const control = new UndefinableFormGroup(); 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('dragend', ['$event'])
@HostListener('dragleave', ['$event']) @HostListener('dragleave', ['$event'])
public onDragEnd(event: DragDropEvent) { public onDragEnd(event: DragEvent) {
const hasFile = this.hasAllowedFile(event.dataTransfer); const hasFile = this.hasAllowedFile(event.dataTransfer);
if (hasFile) { if (hasFile) {
@ -61,7 +61,7 @@ export class FileDropDirective {
} }
@HostListener('dragenter', ['$event']) @HostListener('dragenter', ['$event'])
public onDragEnter(event: DragDropEvent) { public onDragEnter(event: DragEvent) {
const hasFile = this.hasAllowedFile(event.dataTransfer); const hasFile = this.hasAllowedFile(event.dataTransfer);
if (hasFile) { if (hasFile) {
@ -70,7 +70,7 @@ export class FileDropDirective {
} }
@HostListener('dragover', ['$event']) @HostListener('dragover', ['$event'])
public onDragOver(event: DragDropEvent) { public onDragOver(event: DragEvent) {
const isFiles = hasFiles(event.dataTransfer); const isFiles = hasFiles(event.dataTransfer);
if (isFiles) { if (isFiles) {
@ -79,7 +79,7 @@ export class FileDropDirective {
} }
@HostListener('drop', ['$event']) @HostListener('drop', ['$event'])
public async onDrop(event: DragDropEvent) { public async onDrop(event: DragEvent) {
if (hasFiles(event.dataTransfer)) { if (hasFiles(event.dataTransfer)) {
this.stopDrag(event); this.stopDrag(event);
@ -96,7 +96,7 @@ export class FileDropDirective {
event.stopPropagation(); event.stopPropagation();
} }
private stopDrag(event: DragDropEvent) { private stopDrag(event: DragEvent) {
this.dragEnd(0); this.dragEnd(0);
this.stopEvent(event); this.stopEvent(event);
} }
@ -139,7 +139,7 @@ export class FileDropDirective {
const webkitEntry = item.webkitGetAsEntry(); const webkitEntry = item.webkitGetAsEntry();
if (webkitEntry && webkitEntry.isDirectory) { if (webkitEntry && webkitEntry.isDirectory) {
// eslint-disable-next-line no-await-in-loop
await this.traverseWebkitTree(webkitEntry, files); await this.traverseWebkitTree(webkitEntry, files);
} }
} }
@ -163,7 +163,7 @@ export class FileDropDirective {
const entries = await getFilesPromise(item); const entries = await getFilesPromise(item);
for (const entry of entries) { for (const entry of entries) {
// eslint-disable-next-line no-await-in-loop
await this.traverseWebkitTree(entry, files); 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) { if (!dataTransfer || !dataTransfer.types) {
return false; return false;
} }
@ -270,8 +270,4 @@ function getFiles(dataTransfer: DataTransfer) {
} }
return result; 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; 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); const directive = new FocusOnInitDirective(element);
directive.select = false; directive.select = false;
directive.ngAfterViewInit(); directive.ngAfterViewInit();
@ -37,11 +37,11 @@ describe('FocusOnInitDirective', () => {
expect(isFocusCalled).toBeTruthy(); expect(isFocusCalled).toBeTruthy();
expect(isSelectCalled).toBeFalsy(); expect(isSelectCalled).toBeFalsy();
cb();
}, 200); }, 200);
}); });
it('should call select on element if init', (cb) => { it('should call select on element if init', async () => {
const directive = new FocusOnInitDirective(element); const directive = new FocusOnInitDirective(element);
directive.select = true; directive.select = true;
directive.ngAfterViewInit(); directive.ngAfterViewInit();
@ -50,7 +50,7 @@ describe('FocusOnInitDirective', () => {
expect(isFocusCalled).toBeTruthy(); expect(isFocusCalled).toBeTruthy();
expect(isSelectCalled).toBeTruthy(); expect(isSelectCalled).toBeTruthy();
cb();
}, 200); }, 200);
}); });
}); });

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

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

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

@ -66,7 +66,7 @@ export module ValidatorsEx {
if (v) { if (v) {
try { try {
DateTime.parseISO(v); DateTime.parseISO(v);
} catch (e) { } catch {
return { validdatetime: false }; 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. * 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 { HttpClient, HttpErrorResponse, HttpEvent, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http';
import { catchError, map, Observable, throwError } from 'rxjs'; import { catchError, map, Observable, throwError } from 'rxjs';
import { ErrorDto, getActualVersion, Types, Versioned, VersionOrTag, VersionTag } from '@app/framework/internal'; 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)) { } else if (Types.isString(error)) {
try { try {
parsed = JSON.parse(error); parsed = JSON.parse(error);
} catch (e) { } catch {
parsed = undefined; 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
/* eslint-disable import/no-cycle */
import { AfterViewInit, Directive, ElementRef, HostListener, Renderer2 } from '@angular/core'; import { AfterViewInit, Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
import { LayoutComponent } from './layout.component'; 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
/* eslint-disable import/no-cycle */
import { ChangeDetectionStrategy, Component, Input, numberAttribute } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, numberAttribute } from '@angular/core';
@Component({ @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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { action } from '@storybook/addon-actions';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { LongHoverDirective } from '@app/framework'; import { LongHoverDirective } from '@app/framework';
@ -45,16 +44,12 @@ type Story = StoryObj<LongHoverDirective>;
export const Default: Story = { export const Default: Story = {
args: { args: {
hover: action('Hover'),
selector: '', selector: '',
cancelled: action('Cancelled'),
}, },
}; };
export const Selector: Story = { export const Selector: Story = {
args: { args: {
hover: action('Hover'),
selector: 'button', 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) { function verifyTextRender(text: string) {
renderer.verify(x => x.setProperty(It.isAny(), 'textContent', text), Times.once()); renderer.verify(x => x.setProperty(It.isAny(), 'textContent', text), Times.once());
expect().nothing();
} }
function verifyHtmlRender(text: string) { function verifyHtmlRender(text: string) {
renderer.verify(x => x.setProperty(It.isAny(), 'innerHTML', text), Times.once()); 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { LocalizerService, ResizeService } from '@app/framework/internal'; import { LocalizerService, ResizeService } from '@app/framework/internal';
import { MenuItemComponent } from './menu-item.component'; import { MenuItemComponent } from './menu-item.component';
@ -33,7 +32,6 @@ export default {
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [ imports: [
BrowserAnimationsModule,
MenuItemComponent, MenuItemComponent,
RootViewComponent, RootViewComponent,
], ],

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

@ -22,7 +22,7 @@
<div class="notification-container notification-container-bottom-right"> <div class="notification-container notification-container-bottom-right">
@for (notification of snapshot.notifications; track notification) { @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> <button class="btn-sm btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button>
<div class="timer timer-{{ notification.messageType }}"></div> <div class="timer timer-{{ notification.messageType }}"></div>
<div [sqxMarkdown]="notification.message | sqxTranslate"></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 { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { timer } from 'rxjs'; 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 { FocusOnInitDirective } from '../forms/focus-on-init.directive';
import { MarkdownDirective } from '../markdown.directive'; import { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe'; import { TranslatePipe } from '../pipes/translate.pipe';
@ -33,9 +33,6 @@ interface State {
selector: 'sqx-dialog-renderer', selector: 'sqx-dialog-renderer',
styleUrls: ['./dialog-renderer.component.scss'], styleUrls: ['./dialog-renderer.component.scss'],
templateUrl: './dialog-renderer.component.html', templateUrl: './dialog-renderer.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
FocusOnInitDirective, FocusOnInitDirective,

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

@ -6,7 +6,6 @@
*/ */
import { Component, inject, Input } from '@angular/core'; import { Component, inject, Input } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { ConfirmClickDirective, DialogRendererComponent, DialogService, ErrorDto, LocalizerService, RootViewComponent, TooltipDirective } from '@app/framework'; 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()"> <button class="btn btn-primary" (click)="test()">
Show {{ mode }} Show {{ mode }}
</button> </button>
`, `
imports: [
DialogRendererComponent,
],
}) })
class TestComponent { class TestComponent {
public readonly dialogs = inject(DialogService); public readonly dialogs = inject(DialogService);
@ -57,7 +53,6 @@ export default {
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [ imports: [
BrowserAnimationsModule,
ConfirmClickDirective, ConfirmClickDirective,
RootViewComponent, RootViewComponent,
TestComponent, 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-dialog modal-{{ size }}" [class.modal-fh]="fullHeight">
<div class="modal-content" [sqxTourStep]="tourId"> <div class="modal-content" [sqxTourStep]="tourId">
@if (showHeader) { @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 { 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 { TranslatePipe } from '../pipes/translate.pipe';
import { ShortcutComponent } from '../shortcut.component'; import { ShortcutComponent } from '../shortcut.component';
import { TourStepDirective } from './tour-step.directive'; import { TourStepDirective } from './tour-step.directive';
@ -16,9 +15,6 @@ import { TourStepDirective } from './tour-step.directive';
selector: 'sqx-modal-dialog', selector: 'sqx-modal-dialog',
styleUrls: ['./modal-dialog.component.scss'], styleUrls: ['./modal-dialog.component.scss'],
templateUrl: './modal-dialog.component.html', templateUrl: './modal-dialog.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,
imports: [ imports: [
ShortcutComponent, 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) { if (this.adjustHeight) {
const height = rects.reference.height + 2 * this.spaceY; 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; return false;
} catch (ex) { } catch {
return false; return false;
} }
} }

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

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

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

@ -1,6 +1,6 @@
<div <div
class="tour-step" class="tour-step"
@fade animate.enter="fade-in" animate.leave="fade-out"
[class.d-none]="!isVisible" [class.d-none]="!isVisible"
offset="4" offset="4"
[position]="currentStep.position || 'bottom-start'" [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 { 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 { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe'; import { TranslatePipe } from '../pipes/translate.pipe';
import { ModalPlacementDirective } from './modal-placement.directive'; import { ModalPlacementDirective } from './modal-placement.directive';
@ -18,9 +18,6 @@ import { StepDefinition, TourService } from './tour.service';
selector: 'sqx-tour-template', selector: 'sqx-tour-template',
styleUrls: ['./tour-template.component.scss'], styleUrls: ['./tour-template.component.scss'],
templateUrl: './tour-template.component.html', templateUrl: './tour-template.component.html',
animations: [
fadeAnimation,
],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
MarkdownDirective, 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.paging = { page: 0, pageSize: 10, count: 0, total: 0 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse(); expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBeFalse(); expect(pager.canGoPrev).toBe(false);
expect(pager.itemFirst).toEqual(0); expect(pager.itemFirst).toEqual(0);
expect(pager.itemLast).toEqual(0); expect(pager.itemLast).toEqual(0);
@ -27,8 +27,8 @@ describe('Pager', () => {
pager.paging = { page: 0, pageSize: 10, count: 10, total: 100 }; pager.paging = { page: 0, pageSize: 10, count: 10, total: 100 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeTrue(); expect(pager.canGoNext).toBe(true);
expect(pager.canGoPrev).toBeFalse(); expect(pager.canGoPrev).toBe(false);
expect(pager.itemFirst).toEqual(1); expect(pager.itemFirst).toEqual(1);
expect(pager.itemLast).toEqual(10); expect(pager.itemLast).toEqual(10);
@ -40,8 +40,8 @@ describe('Pager', () => {
pager.paging = { page: 4, pageSize: 10, count: 10, total: 100 }; pager.paging = { page: 4, pageSize: 10, count: 10, total: 100 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeTrue(); expect(pager.canGoNext).toBe(true);
expect(pager.canGoPrev).toBeTrue(); expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(41); expect(pager.itemFirst).toEqual(41);
expect(pager.itemLast).toEqual(50); expect(pager.itemLast).toEqual(50);
@ -53,8 +53,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 10, total: 100 }; pager.paging = { page: 9, pageSize: 10, count: 10, total: 100 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse(); expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBeTrue(); expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91); expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(100); expect(pager.itemLast).toEqual(100);
@ -66,8 +66,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 4, total: 100 }; pager.paging = { page: 9, pageSize: 10, count: 4, total: 100 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse(); expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBeTrue(); expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91); expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(94); expect(pager.itemLast).toEqual(94);
@ -79,8 +79,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 9, total: 100 }; pager.paging = { page: 9, pageSize: 10, count: 9, total: 100 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse(); expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBeTrue(); expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91); expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(99); expect(pager.itemLast).toEqual(99);
@ -92,8 +92,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 10, total: -1 }; pager.paging = { page: 9, pageSize: 10, count: 10, total: -1 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeTrue(); expect(pager.canGoNext).toBe(true);
expect(pager.canGoPrev).toBeTrue(); expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91); expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(100); expect(pager.itemLast).toEqual(100);
@ -105,8 +105,8 @@ describe('Pager', () => {
pager.paging = { page: 9, pageSize: 10, count: 5, total: -1 }; pager.paging = { page: 9, pageSize: 10, count: 5, total: -1 };
pager.ngOnChanges(); pager.ngOnChanges();
expect(pager.canGoNext).toBeFalse(); expect(pager.canGoNext).toBe(false);
expect(pager.canGoPrev).toBeTrue(); expect(pager.canGoPrev).toBe(true);
expect(pager.itemFirst).toEqual(91); expect(pager.itemFirst).toEqual(91);
expect(pager.itemLast).toEqual(95); 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 dateString = dateTime.toISOString();
const TestCases = [{ const TestCases = [{
value: dateString, value: dateString,
name: 'String', name: 'String',
}, { }, {
value: dateTime, value: dateTime,
name: 'DateTime', name: 'DateTime',
}]; }];
describe('DurationPipe', () => { describe('DurationPipe', () => {
beforeEach(() => { beforeEach(() => {

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

@ -49,6 +49,5 @@ export function calculateFileSize(value: number, factor = 1024) {
u++; u++;
} }
// eslint-disable-next-line prefer-template
return (u ? `${value.toFixed(1)} ` : value) + ' kMGTPEZY'[u] + 'B'; 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 }, state2: { squidex: true, cms: true },
}); });
expect(routeExtras!.replaceUrl).toBeTrue(); expect(routeExtras!.replaceUrl).toBe(true);
expect(routeExtras!.queryParamsHandling).toBe('merge'); expect(routeExtras!.queryParamsHandling).toBe('merge');
expect(routeExtras!.queryParams).toEqual({ state1: 'hello', state2: 'squidex,cms' }); expect(routeExtras!.queryParams).toEqual({ state1: 'hello', state2: 'squidex,cms' });
@ -259,7 +259,7 @@ describe('Router2State', () => {
state2: { squidex: true, cms: true }, state2: { squidex: true, cms: true },
}); });
expect(routeExtras!.replaceUrl).toBeTrue(); expect(routeExtras!.replaceUrl).toBe(true);
expect(routeExtras!.queryParamsHandling).toBe('merge'); expect(routeExtras!.queryParamsHandling).toBe('merge');
expect(routeExtras!.queryParams).toEqual({ state1: 'hello', state2: 'squidex,cms' }); 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', () => { it('should init without keys', () => {
shortcutComponent.keys = null!; shortcutComponent.keys = null!;
shortcutComponent.ngOnInit(); shortcutComponent.ngOnInit();
expect().nothing();
}); });
it('should destroy without keys', () => { it('should destroy without keys', () => {
shortcutComponent.keys = null!; shortcutComponent.keys = null!;
shortcutComponent.ngOnDestroy(); shortcutComponent.ngOnDestroy();
expect().nothing();
}); });
it('should raise event if triggered', () => { 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', () => { it('should init without keys', () => {
shortcutDirective.shortcut = null!; shortcutDirective.shortcut = null!;
shortcutDirective.ngOnInit(); shortcutDirective.ngOnInit();
expect().nothing();
}); });
it('should destroy without keys', () => { it('should destroy without keys', () => {
shortcutDirective.shortcut = null!; shortcutDirective.shortcut = null!;
shortcutDirective.ngOnDestroy(); shortcutDirective.ngOnDestroy();
expect().nothing();
}); });
it('should raise event if triggered', () => { 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 { try {
this.changeDetector.detectChanges(); this.changeDetector.detectChanges();
} catch { } catch {
return; /* NOOP */
} }
}); });
} }

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

@ -40,8 +40,6 @@ describe('TitleComponent', () => {
titleComponent.message = 'title1'; titleComponent.message = 'title1';
titleComponent.ngOnChanges(); titleComponent.ngOnChanges();
expect().nothing();
titleService.verify(x => x.push('title1', undefined, 'my-url'), Times.once()); titleService.verify(x => x.push('title1', undefined, 'my-url'), Times.once());
}); });
@ -52,8 +50,6 @@ describe('TitleComponent', () => {
titleComponent.message = 'title2'; titleComponent.message = 'title2';
titleComponent.ngOnChanges(); titleComponent.ngOnChanges();
expect().nothing();
titleService.verify(x => x.push('title2', 0, 'my-url'), Times.once()); titleService.verify(x => x.push('title2', 0, 'my-url'), Times.once());
}); });
@ -62,8 +58,6 @@ describe('TitleComponent', () => {
titleComponent.ngOnChanges(); titleComponent.ngOnChanges();
titleComponent.ngOnDestroy(); titleComponent.ngOnDestroy();
expect().nothing();
titleService.verify(x => x.pop(), Times.once()); titleService.verify(x => x.pop(), Times.once());
}); });
@ -72,8 +66,6 @@ describe('TitleComponent', () => {
titleComponent.ngOnChanges(); titleComponent.ngOnChanges();
titleComponent.ngOnDestroy(); titleComponent.ngOnDestroy();
expect().nothing();
titleService.verify(x => x.pop(), Times.never()); 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
export * from './angular/animations';
export * from './angular/drag-helper'; export * from './angular/drag-helper';
export * from './angular/routers/router-utils'; export * from './angular/routers/router-utils';
export * from './angular/stateful.component'; 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
/* eslint-disable deprecation/deprecation */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, ReplaySubject, Subject, throwError } from 'rxjs'; import { Observable, ReplaySubject, Subject, throwError } from 'rxjs';
import { ErrorDto } from '../utils/error'; import { ErrorDto } from '../utils/error';

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

@ -61,7 +61,7 @@ describe('LoadingService', () => {
expect(state).toBeTruthy(); 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 }); const loadingService = new LoadingService(<any>{ events });
let state = false; let state = false;
@ -75,11 +75,11 @@ describe('LoadingService', () => {
setTimeout(() => { setTimeout(() => {
expect(state).toBeFalsy(); expect(state).toBeFalsy();
cb();
}, 400); }, 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 }); const loadingService = new LoadingService(<any>{ events });
let state = false; let state = false;
@ -93,11 +93,11 @@ describe('LoadingService', () => {
setTimeout(() => { setTimeout(() => {
expect(state).toBeFalsy(); expect(state).toBeFalsy();
cb();
}, 400); }, 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 }); const loadingService = new LoadingService(<any>{ events });
let state = false; let state = false;
@ -113,7 +113,7 @@ describe('LoadingService', () => {
setTimeout(() => { setTimeout(() => {
expect(state).toBeTruthy(); expect(state).toBeTruthy();
cb();
}, 400); }, 400);
}); });
}); });

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

@ -22,7 +22,7 @@ export class LocalStoreService {
public get(key: string): string | null { public get(key: string): string | null {
try { try {
return this.store.getItem(key); return this.store.getItem(key);
} catch (e) { } catch {
return this.fallback[key]; return this.fallback[key];
} }
} }
@ -52,7 +52,7 @@ export class LocalStoreService {
public set(key: string, value: string) { public set(key: string, value: string) {
try { try {
this.store.setItem(key, value); this.store.setItem(key, value);
} catch (e) { } catch {
this.fallback[key] = value; this.fallback[key] = value;
} }
} }
@ -72,7 +72,7 @@ export class LocalStoreService {
public remove(key: string) { public remove(key: string) {
try { try {
this.store.removeItem(key); this.store.removeItem(key);
} catch (e) { } catch {
delete this.fallback[key]; 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 { NgZone } from '@angular/core';
import { MessageBus } from './message-bus.service'; import { MessageBus } from './message-bus.service';
class Event1 {} class Event1 {
class Event2 {} }
class Event2 {
}
describe('MessageBus', () => { describe('MessageBus', () => {
const zone = { const zone = {

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

@ -25,8 +25,8 @@ describe('ToolbarService', () => {
buttonsTriggered++; buttonsTriggered++;
}); });
toolbarService.addButton(undefined, 'button1', () => {}); toolbarService.addButton(undefined, 'button1', () => { });
toolbarService.addButton(undefined, 'button2', () => {}); toolbarService.addButton(undefined, 'button2', () => { });
expect(buttons!.length).toBe(2); expect(buttons!.length).toBe(2);
expect(buttonsTriggered).toEqual(3); expect(buttonsTriggered).toEqual(3);
@ -43,8 +43,8 @@ describe('ToolbarService', () => {
buttonsTriggered++; buttonsTriggered++;
}); });
toolbarService.addButton(undefined, 'button1', () => {}); toolbarService.addButton(undefined, 'button1', () => { });
toolbarService.addButton(undefined, 'button1', () => {}, { disabled: true }); toolbarService.addButton(undefined, 'button1', () => { }, { disabled: true });
expect(buttons!.length).toBe(1); expect(buttons!.length).toBe(1);
expect(buttonsTriggered).toEqual(3); expect(buttonsTriggered).toEqual(3);
@ -61,7 +61,7 @@ describe('ToolbarService', () => {
buttonsTriggered++; buttonsTriggered++;
}); });
const action = () => {}; const action = () => { };
toolbarService.addButton(undefined, 'button1', action); toolbarService.addButton(undefined, 'button1', action);
toolbarService.addButton(undefined, 'button1', action); toolbarService.addButton(undefined, 'button1', action);
@ -84,8 +84,8 @@ describe('ToolbarService', () => {
buttonsTriggered++; buttonsTriggered++;
}); });
toolbarService.addButton(owner1, 'button1', () => {}); toolbarService.addButton(owner1, 'button1', () => { });
toolbarService.addButton(owner2, 'button2', () => {}); toolbarService.addButton(owner2, 'button2', () => { });
toolbarService.remove(owner1); toolbarService.remove(owner1);
expect(buttons!.length).toBe(1); expect(buttons!.length).toBe(1);
@ -103,8 +103,8 @@ describe('ToolbarService', () => {
buttonsTriggered++; buttonsTriggered++;
}); });
toolbarService.addButton(undefined, 'button1', () => {}); toolbarService.addButton(undefined, 'button1', () => { });
toolbarService.addButton(undefined, 'button2', () => {}); toolbarService.addButton(undefined, 'button2', () => { });
toolbarService.removeAll(); toolbarService.removeAll();
expect(buttons!.length).toBe(0); 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', () => { it('should return true if array include item', () => {
const array_0 = [0, 1, 2]; const array_0 = [0, 1, 2];
expect(array_0.includes(0)).toBeTrue(); expect(array_0.includes(0)).toBe(true);
expect(array_0.includes(1)).toBeTrue(); expect(array_0.includes(1)).toBe(true);
}); });
it('should return false if array does not include item', () => { it('should return false if array does not include item', () => {
const array_0 = [0, 1, 2]; const array_0 = [0, 1, 2];
expect(array_0.includes(3)).toBeFalse(); expect(array_0.includes(3)).toBe(false);
expect(array_0.includes(4)).toBeFalse(); expect(array_0.includes(4)).toBe(false);
}); });
it('should convert to map', () => { 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).toEqual(color1_2);
expect(color1_1).not.toEqual(color2); 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
/* eslint-disable no-template-curly-in-string */
import { DateTime } from './date-time'; import { DateTime } from './date-time';
import { interpolate } from './interpolator'; 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
/* eslint-disable deprecation/deprecation */
export module Keys { export module Keys {
const ALT = 18; const ALT = 18;
const CONTROL = 17; 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 () => { it('should transform multiple images', async () => {
const md = ` const md = `
# Header 1 # Header 1
![Alt1](https://squidex.io/image1.png "Picture1") ![Alt1](https://squidex.io/image1.png "Picture1")
![Alt2](https://squidex.io/image2.png "Picture2") ![Alt2](https://squidex.io/image2.png "Picture2")
## Header 2 ## Header 2
`; `;
@ -102,11 +100,9 @@ describe('MarkdownTransform', () => {
expect(result).toEqual(` expect(result).toEqual(`
# Header 1 # Header 1
![Alt1](https://squidex.io/image1.png?transformed "Picture1") ![Alt1](https://squidex.io/image1.png?transformed "Picture1")
![Alt2](https://squidex.io/image2.png?transformed "Picture2") ![Alt2](https://squidex.io/image2.png?transformed "Picture2")
## Header 2 ## Header 2
`); `);
}); });
}); });

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

Loading…
Cancel
Save