Feat/frontend linting (#9424)

* Change eslintrc config

* Try esprint fix

* Run linter fix

* Set most of the typescript linting rules to warn, run with --fix

* Fix some linting errors

* Optimize imports

* Build works again

* Remove fixes that didn't fix anything

* Make imports lint-conform again && disable trailing underscores as it is part of Angular and our convention

* Remove wrong automated fix

* Rename components with suffix "Component" for linting

* Linting, refactor reorder-delta-builder to a more functional style

* Update delta reorder specs

* Add exceptions for "++" in loops and bracket expressions in arrow functions

* Some linting fixes

* Fix some more linting

* Optimize imports

Co-authored-by: Henriette Darge <h.darge@openproject.com>
pull/9438/head
Benjamin Bädorf 3 years ago committed by GitHub
parent 89bd15516d
commit 3a877457ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 136
      frontend/.eslintrc.js
  2. 10
      frontend/.esprintrc
  3. 20289
      frontend/npm-shrinkwrap.json
  4. 8
      frontend/package.json
  5. 146
      frontend/src/app/app.module.ts
  6. 13
      frontend/src/app/core/active-window/active-window.service.ts
  7. 36
      frontend/src/app/core/apiv3/api-v3.service.spec.ts
  8. 69
      frontend/src/app/core/apiv3/api-v3.service.ts
  9. 43
      frontend/src/app/core/apiv3/cache/cachable-apiv3-collection.ts
  10. 48
      frontend/src/app/core/apiv3/cache/cachable-apiv3-resource.ts
  11. 26
      frontend/src/app/core/apiv3/cache/state-cache.service.ts
  12. 28
      frontend/src/app/core/apiv3/endpoints/capabilities/apiv3-capabilities-paths.ts
  13. 10
      frontend/src/app/core/apiv3/endpoints/capabilities/apiv3-capability-paths.ts
  14. 15
      frontend/src/app/core/apiv3/endpoints/capabilities/capability-cache.service.ts
  15. 20
      frontend/src/app/core/apiv3/endpoints/configuration/apiv3-configuration-path.ts
  16. 36
      frontend/src/app/core/apiv3/endpoints/grids/apiv3-grid-form.ts
  17. 12
      frontend/src/app/core/apiv3/endpoints/grids/apiv3-grid-paths.ts
  18. 30
      frontend/src/app/core/apiv3/endpoints/grids/apiv3-grids-paths.ts
  19. 12
      frontend/src/app/core/apiv3/endpoints/groups/apiv3-group-paths.ts
  20. 26
      frontend/src/app/core/apiv3/endpoints/groups/apiv3-groups-paths.ts
  21. 14
      frontend/src/app/core/apiv3/endpoints/help_texts/apiv3-help-texts-paths.ts
  22. 21
      frontend/src/app/core/apiv3/endpoints/memberships/apiv3-memberships-form.ts
  23. 33
      frontend/src/app/core/apiv3/endpoints/memberships/apiv3-memberships-paths.ts
  24. 21
      frontend/src/app/core/apiv3/endpoints/news/apiv3-news-paths.ts
  25. 24
      frontend/src/app/core/apiv3/endpoints/notifications/apiv3-notification-paths.ts
  26. 39
      frontend/src/app/core/apiv3/endpoints/notifications/apiv3-notifications-paths.ts
  27. 12
      frontend/src/app/core/apiv3/endpoints/placeholder-users/apiv3-placeholder-user-paths.ts
  28. 26
      frontend/src/app/core/apiv3/endpoints/placeholder-users/apiv3-placeholder-users-paths.ts
  29. 29
      frontend/src/app/core/apiv3/endpoints/projects/apiv3-available-projects-paths.ts
  30. 10
      frontend/src/app/core/apiv3/endpoints/projects/apiv3-project-copy-paths.ts
  31. 20
      frontend/src/app/core/apiv3/endpoints/projects/apiv3-project-paths.ts
  32. 28
      frontend/src/app/core/apiv3/endpoints/projects/apiv3-projects-paths.ts
  33. 12
      frontend/src/app/core/apiv3/endpoints/projects/project.cache.ts
  34. 44
      frontend/src/app/core/apiv3/endpoints/queries/apiv3-queries-paths.ts
  35. 45
      frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-form.ts
  36. 28
      frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-order.ts
  37. 23
      frontend/src/app/core/apiv3/endpoints/queries/apiv3-query-paths.ts
  38. 22
      frontend/src/app/core/apiv3/endpoints/relations/apiv3-relations-paths.ts
  39. 9
      frontend/src/app/core/apiv3/endpoints/roles/apiv3-role-paths.ts
  40. 24
      frontend/src/app/core/apiv3/endpoints/roles/apiv3-roles-paths.ts
  41. 9
      frontend/src/app/core/apiv3/endpoints/statuses/apiv3-status-paths.ts
  42. 24
      frontend/src/app/core/apiv3/endpoints/statuses/apiv3-statuses-paths.ts
  43. 32
      frontend/src/app/core/apiv3/endpoints/time-entries/apiv3-time-entries-paths.ts
  44. 31
      frontend/src/app/core/apiv3/endpoints/time-entries/apiv3-time-entry-paths.ts
  45. 17
      frontend/src/app/core/apiv3/endpoints/time-entries/time-entry-cache.service.ts
  46. 11
      frontend/src/app/core/apiv3/endpoints/types/apiv3-type-paths.ts
  47. 15
      frontend/src/app/core/apiv3/endpoints/types/apiv3-types-paths.ts
  48. 11
      frontend/src/app/core/apiv3/endpoints/users/apiv3-user-paths.ts
  49. 25
      frontend/src/app/core/apiv3/endpoints/users/apiv3-user-preferences-paths.ts
  50. 22
      frontend/src/app/core/apiv3/endpoints/users/apiv3-users-paths.ts
  51. 22
      frontend/src/app/core/apiv3/endpoints/versions/apiv3-version-paths.ts
  52. 22
      frontend/src/app/core/apiv3/endpoints/versions/apiv3-versions-paths.ts
  53. 30
      frontend/src/app/core/apiv3/endpoints/work_packages/api-v3-work-package-cached-subresource.ts
  54. 13
      frontend/src/app/core/apiv3/endpoints/work_packages/api-v3-work-package-paths.ts
  55. 56
      frontend/src/app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths.ts
  56. 12
      frontend/src/app/core/apiv3/endpoints/work_packages/apiv3-work-package-form.ts
  57. 40
      frontend/src/app/core/apiv3/endpoints/work_packages/work-package-cache.spec.ts
  58. 17
      frontend/src/app/core/apiv3/endpoints/work_packages/work-package.cache.ts
  59. 18
      frontend/src/app/core/apiv3/forms/apiv3-form-resource.ts
  60. 6
      frontend/src/app/core/apiv3/openproject-api-v3.module.ts
  61. 8
      frontend/src/app/core/apiv3/paths/apiv3-list-resource.interface.ts
  62. 42
      frontend/src/app/core/apiv3/paths/apiv3-resource.ts
  63. 5
      frontend/src/app/core/apiv3/paths/path-resources.ts
  64. 2
      frontend/src/app/core/apiv3/types/hal-collection.type.ts
  65. 33
      frontend/src/app/core/apiv3/virtual/apiv3-board-path.ts
  66. 67
      frontend/src/app/core/apiv3/virtual/apiv3-boards-paths.ts
  67. 6
      frontend/src/app/core/augmenting/dynamic-scripts/backlogs/model.js
  68. 84
      frontend/src/app/core/augmenting/dynamic-scripts/global_roles.ts
  69. 49
      frontend/src/app/core/augmenting/dynamic-scripts/two_factor_authentication.ts
  70. 13
      frontend/src/app/core/augmenting/openproject-augmenting.module.ts
  71. 14
      frontend/src/app/core/augmenting/services/path-script.augment.service.ts
  72. 32
      frontend/src/app/core/backup/op-backup.service.ts
  73. 8
      frontend/src/app/core/browser/browser-detector.service.ts
  74. 1
      frontend/src/app/core/browser/device.service.ts
  75. 14
      frontend/src/app/core/config/configuration.service.ts
  76. 14
      frontend/src/app/core/current-project/current-project.service.spec.ts
  77. 12
      frontend/src/app/core/current-project/current-project.service.ts
  78. 10
      frontend/src/app/core/current-user/current-user.module.ts
  79. 15
      frontend/src/app/core/current-user/current-user.query.ts
  80. 20
      frontend/src/app/core/current-user/current-user.service.spec.ts
  81. 142
      frontend/src/app/core/current-user/current-user.service.ts
  82. 16
      frontend/src/app/core/current-user/current-user.store.ts
  83. 36
      frontend/src/app/core/datetime/timezone.service.spec.ts
  84. 38
      frontend/src/app/core/datetime/timezone.service.ts
  85. 11
      frontend/src/app/core/enterprise/banners.service.ts
  86. 6
      frontend/src/app/core/errors/sentry/sentry-dependency.ts
  87. 35
      frontend/src/app/core/errors/sentry/sentry-reporter.ts
  88. 3
      frontend/src/app/core/expression/expression.service.ts
  89. 74
      frontend/src/app/core/file-upload/op-direct-file-upload.service.ts
  90. 28
      frontend/src/app/core/file-upload/op-file-upload.service.spec.ts
  91. 67
      frontend/src/app/core/file-upload/op-file-upload.service.ts
  92. 101
      frontend/src/app/core/forms/forms.service.spec.ts
  93. 106
      frontend/src/app/core/forms/forms.service.ts
  94. 21
      frontend/src/app/core/forms/typings.d.ts
  95. 6
      frontend/src/app/core/global_search/global-search-work-packages-entry.component.ts
  96. 74
      frontend/src/app/core/global_search/global-search-work-packages.component.ts
  97. 121
      frontend/src/app/core/global_search/input/global-search-input.component.ts
  98. 23
      frontend/src/app/core/global_search/openproject-global-search.module.ts
  99. 32
      frontend/src/app/core/global_search/services/global-search.service.spec.ts
  100. 31
      frontend/src/app/core/global_search/services/global-search.service.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,8 +1,6 @@
module.exports = { module.exports = {
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
], ],
env: { env: {
browser: true, browser: true,
@ -19,19 +17,23 @@ module.exports = {
], ],
overrides: [ overrides: [
{ {
"files": ["*.ts"], files: ["*.ts"],
"parserOptions": { parser: "@typescript-eslint/parser",
"project": [ parserOptions: {
"./src/tsconfig.app.json" project: "./src/tsconfig.app.json",
], tsconfigRootDir: __dirname,
"createDefaultProgram": true sourceType: "module",
createDefaultProgram: true
}, },
"extends": [ extends: [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:@angular-eslint/recommended", "plugin:@angular-eslint/recommended",
// This is required if you use inline templates in Components // This is required if you use inline templates in Components
"plugin:@angular-eslint/template/process-inline-templates" "plugin:@angular-eslint/template/process-inline-templates",
"airbnb-typescript",
], ],
"rules": { rules: {
/** /**
* Any TypeScript source code (NOT TEMPLATE) related rules you wish to use/reconfigure over and above the * Any TypeScript source code (NOT TEMPLATE) related rules you wish to use/reconfigure over and above the
* recommended set provided by the @angular-eslint project would go here. * recommended set provided by the @angular-eslint project would go here.
@ -44,97 +46,32 @@ module.exports = {
"error", "error",
{ "type": "element", "prefix": "op", "style": "kebab-case" } { "type": "element", "prefix": "op", "style": "kebab-case" }
], ],
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-empty-function": "error",
// note you must disable the base rule as it can report incorrect errors
semi: "off",
"@typescript-eslint/semi": ["error"],
"brace-style": [
"error",
"1tbs",
],
curly: "error",
"eol-last": "off",
eqeqeq: [
"error",
"smart",
],
"guard-for-in": "error",
"id-blacklist": "off",
"id-match": "off",
"max-len": [
"off",
{
code: 140,
},
],
"no-bitwise": "off",
"no-caller": "error",
"no-console": [ "no-console": [
"error", "error",
{ {
allow: [ allow: [
"log",
"warn", "warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error", "error",
"groupCollapsed",
"Console",
"profile",
"profileEnd",
"timeStamp",
"context",
], ],
}, },
], ],
"no-debugger": "error",
"no-empty": "error",
"no-eval": "error",
"no-new-wrappers": "error",
"no-redeclare": "error",
"no-trailing-spaces": "error",
"no-underscore-dangle": "off",
"no-unused-labels": "error",
"no-var": "off",
radix: "off",
// Disable required spaces in license comments
"spaced-comment": "off",
// Disable preference on quotes, rely on formatter instead // Who cares about line length
quotes: "off", "max-len": "off",
// Disable consistent return as typescript checks return type // Force single quotes to align with ruby
"consistent-return": "off", quotes: "off",
"@typescript-eslint/quotes": ["error", "single", { avoidEscape: true }],
// Disable forcing arrow function params for one
"arrow-parens": "off",
// Disable enforce class methods use this
"class-methods-use-this": "off",
// Disable webpack loader definitions // Disable webpack loader definitions
"import/no-webpack-loader-syntax": "off", "import/no-webpack-loader-syntax": "off",
/*
// Disable use before define, as irrelevant for TS interfaces // Disable use before define, as irrelevant for TS interfaces
"no-use-before-define": "off", "no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
*/
// Allow object.hasOwnProperty calls
"no-prototype-builtins": "off",
// We need to redeclare interface with the same name
// as a class or constant for type ducking
"no-redeclare": "off",
// Whitespace configuration // Whitespace configuration
"@typescript-eslint/type-annotation-spacing": [ "@typescript-eslint/type-annotation-spacing": [
@ -154,17 +91,34 @@ module.exports = {
// Allow empty interfaces for naming purposes (HAL resources) // Allow empty interfaces for naming purposes (HAL resources)
"@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-interface": "off",
// Force spaces in objects "import/prefer-default-export": "off",
"object-curly-spacing": ["error", "always"],
"no-underscore-dangle": "warn",
"no-return-assign": ["error", "except-parens"],
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
//////////////////////////////////////////////////////////////////////
// Anything below this line should be turned on again at some point //
//////////////////////////////////////////////////////////////////////
// It's common in Angular to wrap even pure functions in classes for injection purposes
// TODO: Should probably be turned off and pure unit tests should be used at some point
"class-methods-use-this": "warn",
// There's too much interop with legacy code that is `any`-typed for this to be an error in any practical sense
// TODO: Actually type everything
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
// Force indent to 2space // This is probably the first rule that should be fixed. It had 309 errors last time we checked
indent: ["error", 2], "@typescript-eslint/no-unsafe-return": "warn",
} }
}, },
{ {
"files": ["*.html"], files: ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"], extends: ["plugin:@angular-eslint/template/recommended"],
"rules": { rules: {
/** /**
* Any template/HTML related rules you wish to use/reconfigure over and above the * Any template/HTML related rules you wish to use/reconfigure over and above the
* recommended set provided by the @angular-eslint project would go here. * recommended set provided by the @angular-eslint project would go here.

@ -0,0 +1,10 @@
{
"paths": ["src/**/*.ts", "src/*.ts"],
"ignored": [
"**/node_modules/**/*",
"src/**/*.spec.ts",
"src/test/*"
],
"port": 5004,
"quiet": true
}

File diff suppressed because it is too large Load Diff

@ -11,6 +11,8 @@
"@angular-eslint/schematics": "12.0.0", "@angular-eslint/schematics": "12.0.0",
"@angular-eslint/template-parser": "^12.0.0", "@angular-eslint/template-parser": "^12.0.0",
"@angular/language-service": "12.0.2", "@angular/language-service": "12.0.2",
"@html-eslint/eslint-plugin": "^0.11.0",
"@html-eslint/parser": "^0.11.0",
"@jsdevtools/coverage-istanbul-loader": "3.0.5", "@jsdevtools/coverage-istanbul-loader": "3.0.5",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/swagger-ui": "^3.47.0", "@types/swagger-ui": "^3.47.0",
@ -19,7 +21,12 @@
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"eslint": "^7.26.0", "eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"esprint": "^3.1.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "~6.3.2", "karma": "~6.3.2",
@ -132,6 +139,7 @@
"test": "ng test --watch=false", "test": "ng test --watch=false",
"test-watch": "ng test --watch=true", "test-watch": "ng test --watch=true",
"lint": "eslint -c .eslintrc.js --ext .ts src/app/", "lint": "eslint -c .eslintrc.js --ext .ts src/app/",
"lint:fix": "esprint check --fix",
"generate-typings": "tsc -d -p src/tsconfig.app.json" "generate-typings": "tsc -d -p src/tsconfig.app.json"
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,60 +26,78 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APP_INITIALIZER, ApplicationRef, Injector, NgModule } from '@angular/core'; import {
APP_INITIALIZER, ApplicationRef, Injector, NgModule,
} from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { OpContextMenuTrigger } from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive'; import { OpContextMenuTrigger } from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive';
import { States } from 'core-app/core/states/states.service'; import { States } from 'core-app/core/states/states.service';
import { OpenprojectFieldsModule } from "core-app/shared/components/fields/openproject-fields.module"; import { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openproject-fields.module';
import { OPSharedModule } from "core-app/shared/shared.module"; import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpDragScrollDirective } from "core-app/shared/directives/op-drag-scroll/op-drag-scroll.directive"; import { OpDragScrollDirective } from 'core-app/shared/directives/op-drag-scroll/op-drag-scroll.directive';
import { OpenProjectDirectFileUploadService } from './core/file-upload/op-direct-file-upload.service'; import { DynamicBootstrapper } from 'core-app/core/setup/globals/dynamic-bootstrapper';
import { DynamicBootstrapper } from "core-app/core/setup/globals/dynamic-bootstrapper";
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module'; import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { OpenprojectAttachmentsModule } from 'core-app/shared/components/attachments/openproject-attachments.module'; import { OpenprojectAttachmentsModule } from 'core-app/shared/components/attachments/openproject-attachments.module';
import { OpenprojectEditorModule } from 'core-app/shared/components/editor/openproject-editor.module'; import { OpenprojectEditorModule } from 'core-app/shared/components/editor/openproject-editor.module';
import { OpenprojectGridsModule } from "core-app/shared/components/grids/openproject-grids.module"; import { OpenprojectGridsModule } from 'core-app/shared/components/grids/openproject-grids.module';
import { OpenprojectRouterModule } from "core-app/core/routing/openproject-router.module"; import { OpenprojectRouterModule } from 'core-app/core/routing/openproject-router.module';
import { OpenprojectWorkPackageRoutesModule } from "core-app/features/work-packages/openproject-work-package-routes.module"; import { OpenprojectWorkPackageRoutesModule } from 'core-app/features/work-packages/openproject-work-package-routes.module';
import { BrowserModule } from "@angular/platform-browser"; import { BrowserModule } from '@angular/platform-browser';
import { OpenprojectCalendarModule } from "core-app/shared/components/calendar/openproject-calendar.module"; import { OpenprojectCalendarModule } from 'core-app/shared/components/calendar/openproject-calendar.module';
import { OpenprojectGlobalSearchModule } from "core-app/core/global_search/openproject-global-search.module"; import { OpenprojectGlobalSearchModule } from 'core-app/core/global_search/openproject-global-search.module';
import { OpenprojectDashboardsModule } from "core-app/features/dashboards/openproject-dashboards.module"; import { OpenprojectDashboardsModule } from 'core-app/features/dashboards/openproject-dashboards.module';
import { OpenprojectWorkPackageGraphsModule } from "core-app/shared/components/work-package-graphs/openproject-work-package-graphs.module"; import { OpenprojectWorkPackageGraphsModule } from 'core-app/shared/components/work-package-graphs/openproject-work-package-graphs.module';
import { PreviewTriggerService } from "core-app/core/setup/globals/global-listeners/preview-trigger.service"; import { PreviewTriggerService } from 'core-app/core/setup/globals/global-listeners/preview-trigger.service';
import { OpenprojectOverviewModule } from "core-app/features/overview/openproject-overview.module"; import { OpenprojectOverviewModule } from 'core-app/features/overview/openproject-overview.module';
import { OpenprojectMyPageModule } from "core-app/features/my-page/openproject-my-page.module"; import { OpenprojectMyPageModule } from 'core-app/features/my-page/openproject-my-page.module';
import { OpenprojectProjectsModule } from "core-app/features/projects/openproject-projects.module"; import { OpenprojectProjectsModule } from 'core-app/features/projects/openproject-projects.module';
import { KeyboardShortcutService } from "core-app/shared/directives/a11y/keyboard-shortcut-service"; import { KeyboardShortcutService } from 'core-app/shared/directives/a11y/keyboard-shortcut-service';
import { OpenprojectMembersModule } from "core-app/shared/components/autocompleter/members-autocompleter/members.module"; import { OpenprojectMembersModule } from 'core-app/shared/components/autocompleter/members-autocompleter/members.module';
import { OpenprojectAugmentingModule } from "core-app/core/augmenting/openproject-augmenting.module"; import { OpenprojectAugmentingModule } from 'core-app/core/augmenting/openproject-augmenting.module';
import { OpenprojectInviteUserModalModule } from "core-app/features/invite-user-modal/invite-user-modal.module"; import { OpenprojectInviteUserModalModule } from 'core-app/features/invite-user-modal/invite-user-modal.module';
import { OpenprojectModalModule } from "core-app/shared/components/modal/modal.module"; import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { RevitAddInSettingsButtonService } from "core-app/features/bim/revit_add_in/revit-add-in-settings-button.service"; import { RevitAddInSettingsButtonService } from 'core-app/features/bim/revit_add_in/revit-add-in-settings-button.service';
import { OpenprojectAutocompleterModule } from "core-app/shared/components/autocompleter/openproject-autocompleter.module"; import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autocompleter/openproject-autocompleter.module';
import { OpenProjectFileUploadService } from 'core-app/core/file-upload/op-file-upload.service';
import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openproject-enterprise.module';
import { MainMenuToggleComponent } from 'core-app/core/main-menu/main-menu-toggle.component';
import { MainMenuNavigationService } from 'core-app/core/main-menu/main-menu-navigation.service';
import { ConfirmDialogService } from 'core-app/shared/components/modals/confirm-dialog/confirm-dialog.service';
import { ConfirmDialogModalComponent } from 'core-app/shared/components/modals/confirm-dialog/confirm-dialog.modal';
import { DynamicContentModalComponent } from 'core-app/shared/components/modals/modal-wrapper/dynamic-content.modal';
import { PasswordConfirmationModalComponent } from 'core-app/shared/components/modals/request-for-confirmation/password-confirmation.modal';
import { WpPreviewModalComponent } from 'core-app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal';
import { ConfirmFormSubmitController } from 'core-app/shared/components/modals/confirm-form-submit/confirm-form-submit.directive';
import { ProjectMenuAutocompleteComponent } from 'core-app/shared/components/autocompleter/project-menu-autocomplete/project-menu-autocomplete.component';
import { PaginationService } from 'core-app/shared/components/table-pagination/pagination-service';
import { MainMenuResizerComponent } from 'core-app/shared/components/resizer/resizer/main-menu-resizer.component';
import { CommentService } from 'core-app/features/work-packages/components/wp-activity/comment-service';
import { OpenprojectTabsModule } from 'core-app/shared/components/tabs/openproject-tabs.module';
import { OpenprojectAdminModule } from 'core-app/features/admin/openproject-admin.module';
import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.module';
import { globalDynamicComponents } from 'core-app/core/setup/global-dynamic-components.const';
import { HookService } from 'core-app/features/plugins/hook-service';
import { OpenprojectPluginsModule } from 'core-app/features/plugins/openproject-plugins.module';
import { LinkedPluginsModule } from 'core-app/features/plugins/linked-plugins.module';
import { OpenProjectInAppNotificationsModule } from 'core-app/features/in-app-notifications/in-app-notifications.module';
import { OpenProjectBackupService } from './core/backup/op-backup.service'; import { OpenProjectBackupService } from './core/backup/op-backup.service';
import { OpenProjectFileUploadService } from "core-app/core/file-upload/op-file-upload.service"; import { OpenProjectDirectFileUploadService } from './core/file-upload/op-direct-file-upload.service';
import { OpenprojectEnterpriseModule } from "core-app/features/enterprise/openproject-enterprise.module";
import { MainMenuToggleComponent } from "core-app/core/main-menu/main-menu-toggle.component"; export function initializeServices(injector:Injector) {
import { MainMenuNavigationService } from "core-app/core/main-menu/main-menu-navigation.service"; return () => {
import { ConfirmDialogService } from "core-app/shared/components/modals/confirm-dialog/confirm-dialog.service"; const PreviewTrigger = injector.get(PreviewTriggerService);
import { ConfirmDialogModal } from "core-app/shared/components/modals/confirm-dialog/confirm-dialog.modal"; const mainMenuNavigationService = injector.get(MainMenuNavigationService);
import { DynamicContentModal } from "core-app/shared/components/modals/modal-wrapper/dynamic-content.modal"; const keyboardShortcuts = injector.get(KeyboardShortcutService);
import { PasswordConfirmationModal } from "core-app/shared/components/modals/request-for-confirmation/password-confirmation.modal"; // Conditionally add the Revit Add-In settings button
import { WpPreviewModal } from "core-app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal"; injector.get(RevitAddInSettingsButtonService);
import { ConfirmFormSubmitController } from "core-app/shared/components/modals/confirm-form-submit/confirm-form-submit.directive";
import { ProjectMenuAutocompleteComponent } from "core-app/shared/components/autocompleter/project-menu-autocomplete/project-menu-autocomplete.component"; mainMenuNavigationService.register();
import { PaginationService } from "core-app/shared/components/table-pagination/pagination-service";
import { MainMenuResizerComponent } from "core-app/shared/components/resizer/resizer/main-menu-resizer.component"; PreviewTrigger.setupListener();
import { CommentService } from "core-app/features/work-packages/components/wp-activity/comment-service";
import { OpenprojectTabsModule } from "core-app/shared/components/tabs/openproject-tabs.module"; keyboardShortcuts.register();
import { OpenprojectAdminModule } from "core-app/features/admin/openproject-admin.module"; };
import { OpenprojectHalModule } from "core-app/features/hal/openproject-hal.module"; }
import { globalDynamicComponents } from "core-app/core/setup/global-dynamic-components.const";
import { HookService } from "core-app/features/plugins/hook-service";
import { OpenprojectPluginsModule } from "core-app/features/plugins/openproject-plugins.module";
import { LinkedPluginsModule } from "core-app/features/plugins/linked-plugins.module";
import { OpenProjectInAppNotificationsModule } from "core-app/features/in-app-notifications/in-app-notifications.module";
@NgModule({ @NgModule({
imports: [ imports: [
@ -159,7 +177,9 @@ import { OpenProjectInAppNotificationsModule } from "core-app/features/in-app-no
], ],
providers: [ providers: [
{ provide: States, useValue: new States() }, { provide: States, useValue: new States() },
{ provide: APP_INITIALIZER, useFactory: initializeServices, deps: [Injector], multi: true }, {
provide: APP_INITIALIZER, useFactory: initializeServices, deps: [Injector], multi: true,
},
PaginationService, PaginationService,
OpenProjectBackupService, OpenProjectBackupService,
OpenProjectFileUploadService, OpenProjectFileUploadService,
@ -173,10 +193,10 @@ import { OpenProjectInAppNotificationsModule } from "core-app/features/in-app-no
OpContextMenuTrigger, OpContextMenuTrigger,
// Modals // Modals
ConfirmDialogModal, ConfirmDialogModalComponent,
DynamicContentModal, DynamicContentModalComponent,
PasswordConfirmationModal, PasswordConfirmationModalComponent,
WpPreviewModal, WpPreviewModalComponent,
// Main menu // Main menu
MainMenuResizerComponent, MainMenuResizerComponent,
@ -188,13 +208,11 @@ import { OpenProjectInAppNotificationsModule } from "core-app/features/in-app-no
// Form configuration // Form configuration
OpDragScrollDirective, OpDragScrollDirective,
ConfirmFormSubmitController, ConfirmFormSubmitController,
] ],
}) })
export class OpenProjectModule { export class OpenProjectModule {
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
ngDoBootstrap(appRef:ApplicationRef) { ngDoBootstrap(appRef:ApplicationRef) {
// Register global dynamic components // Register global dynamic components
// this is necessary to ensure they are not tree-shaken // this is necessary to ensure they are not tree-shaken
// (if they are not used anywhere in Angular, they would be removed) // (if they are not used anywhere in Angular, they would be removed)
@ -214,19 +232,3 @@ export class OpenProjectModule {
}); });
} }
} }
export function initializeServices(injector:Injector) {
return () => {
const PreviewTrigger = injector.get(PreviewTriggerService);
const mainMenuNavigationService = injector.get(MainMenuNavigationService);
const keyboardShortcuts = injector.get(KeyboardShortcutService);
// Conditionally add the Revit Add-In settings button
injector.get(RevitAddInSettingsButtonService);
mainMenuNavigationService.register();
PreviewTrigger.setupListener();
keyboardShortcuts.register();
};
}

@ -1,17 +1,16 @@
import { Inject, Injectable } from "@angular/core"; import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from "@angular/common"; import { DOCUMENT } from '@angular/common';
import { BehaviorSubject, Observable, Subject } from "rxjs"; import { BehaviorSubject, Observable } from 'rxjs';
import { debugLog } from "core-app/shared/helpers/debug_output"; import { debugLog } from 'core-app/shared/helpers/debug_output';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ActiveWindowService { export class ActiveWindowService {
private activeState$ = new BehaviorSubject<boolean>(true); private activeState$ = new BehaviorSubject<boolean>(true);
constructor(@Inject(DOCUMENT) document:Document) { constructor(@Inject(DOCUMENT) document:Document) {
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState) { if (document.visibilityState) {
debugLog("Browser window has visibility state changed to " + document.visibilityState); debugLog(`Browser window has visibility state changed to ${document.visibilityState}`);
this.activeState$.next(document.visibilityState === 'visible'); this.activeState$.next(document.visibilityState === 'visible');
} }
}); });
@ -30,4 +29,4 @@ export class ActiveWindowService {
public get active$():Observable<boolean> { public get active$():Observable<boolean> {
return this.activeState$.asObservable(); return this.activeState$.asObservable();
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,12 +26,12 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { TestBed, waitForAsync } from "@angular/core/testing"; import { TestBed, waitForAsync } from '@angular/core/testing';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { States } from "core-app/core/states/states.service"; import { States } from 'core-app/core/states/states.service';
describe('APIv3Service', function() { describe('APIv3Service', () => {
let service:APIV3Service; let service:APIV3Service;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
@ -40,8 +40,8 @@ describe('APIv3Service', function() {
providers: [ providers: [
States, States,
PathHelperService, PathHelperService,
APIV3Service APIV3Service,
] ],
}) })
.compileComponents() .compileComponents()
.then(() => { .then(() => {
@ -53,34 +53,34 @@ describe('APIv3Service', function() {
return new URLSearchParams(object).toString(); return new URLSearchParams(object).toString();
} }
describe('apiV3', function() { describe('apiV3', () => {
var projectIdentifier = 'majora'; const projectIdentifier = 'majora';
it('should provide the project\'s path', function() { it("should provide the project's path", () => {
expect(service.projects.id(projectIdentifier).path).toEqual('/api/v3/projects/majora'); expect(service.projects.id(projectIdentifier).path).toEqual('/api/v3/projects/majora');
}); });
it('should provide a path to work package query on subject or ID ', function() { it('should provide a path to work package query on subject or ID ', () => {
let params = { let params = {
filters: '[{"subjectOrId":{"operator":"**","values":["bogus"]}}]', filters: '[{"subjectOrId":{"operator":"**","values":["bogus"]}}]',
sortBy: '[["updatedAt","desc"]]', sortBy: '[["updatedAt","desc"]]',
offset: '1', offset: '1',
pageSize: '10' pageSize: '10',
}; };
expect( expect(
service.work_packages.filterBySubjectOrId("bogus").path service.work_packages.filterBySubjectOrId('bogus').path,
).toEqual('/api/v3/work_packages?' + encodeParams(params)); ).toEqual(`/api/v3/work_packages?${encodeParams(params)}`);
params = { params = {
filters: '[{"id":{"operator":"=","values":["1234"]}}]', filters: '[{"id":{"operator":"=","values":["1234"]}}]',
sortBy: '[["updatedAt","desc"]]', sortBy: '[["updatedAt","desc"]]',
offset: '1', offset: '1',
pageSize: '10' pageSize: '10',
}; };
expect( expect(
service.work_packages.filterBySubjectOrId("1234", true).path service.work_packages.filterBySubjectOrId('1234', true).path,
).toEqual('/api/v3/work_packages?' + encodeParams(params)); ).toEqual(`/api/v3/work_packages?${encodeParams(params)}`);
}); });
}); });
}); });

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,39 +26,33 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injectable, Injector } from "@angular/core"; import { Injectable, Injector } from '@angular/core';
import { import { APIv3GettableResource, APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
APIv3GettableResource, import { Constructor } from '@angular/cdk/table';
APIv3ResourceCollection, import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
APIv3ResourcePath import { Apiv3GridsPaths } from 'core-app/core/apiv3/endpoints/grids/apiv3-grids-paths';
} from "core-app/core/apiv3/paths/apiv3-resource"; import { Apiv3TimeEntriesPaths } from 'core-app/core/apiv3/endpoints/time-entries/apiv3-time-entries-paths';
import { Constructor } from "@angular/cdk/table"; import { Apiv3CapabilitiesPaths } from 'core-app/core/apiv3/endpoints/capabilities/apiv3-capabilities-paths';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; import { Apiv3MembershipsPaths } from 'core-app/core/apiv3/endpoints/memberships/apiv3-memberships-paths';
import { Apiv3GridsPaths } from "core-app/core/apiv3/endpoints/grids/apiv3-grids-paths"; import { Apiv3UsersPaths } from 'core-app/core/apiv3/endpoints/users/apiv3-users-paths';
import { Apiv3TimeEntriesPaths } from "core-app/core/apiv3/endpoints/time-entries/apiv3-time-entries-paths"; import { APIv3TypesPaths } from 'core-app/core/apiv3/endpoints/types/apiv3-types-paths';
import { Apiv3CapabilitiesPaths } from "core-app/core/apiv3/endpoints/capabilities/apiv3-capabilities-paths"; import { APIv3QueriesPaths } from 'core-app/core/apiv3/endpoints/queries/apiv3-queries-paths';
import { Apiv3MembershipsPaths } from "core-app/core/apiv3/endpoints/memberships/apiv3-memberships-paths"; import { APIV3WorkPackagesPaths } from 'core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths';
import { Apiv3UsersPaths } from "core-app/core/apiv3/endpoints/users/apiv3-users-paths"; import { APIv3ProjectPaths } from 'core-app/core/apiv3/endpoints/projects/apiv3-project-paths';
import { APIv3TypesPaths } from "core-app/core/apiv3/endpoints/types/apiv3-types-paths"; import { APIv3ProjectsPaths } from 'core-app/core/apiv3/endpoints/projects/apiv3-projects-paths';
import { APIv3QueriesPaths } from "core-app/core/apiv3/endpoints/queries/apiv3-queries-paths"; import { APIv3StatusesPaths } from 'core-app/core/apiv3/endpoints/statuses/apiv3-statuses-paths';
import { APIV3WorkPackagesPaths } from "core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths"; import { APIv3RolesPaths } from 'core-app/core/apiv3/endpoints/roles/apiv3-roles-paths';
import { APIv3ProjectPaths } from "core-app/core/apiv3/endpoints/projects/apiv3-project-paths"; import { APIv3VersionsPaths } from 'core-app/core/apiv3/endpoints/versions/apiv3-versions-paths';
import { APIv3ProjectsPaths } from "core-app/core/apiv3/endpoints/projects/apiv3-projects-paths"; import { Apiv3RelationsPaths } from 'core-app/core/apiv3/endpoints/relations/apiv3-relations-paths';
import { APIv3StatusesPaths } from "core-app/core/apiv3/endpoints/statuses/apiv3-statuses-paths"; import { Apiv3NewsPaths } from 'core-app/core/apiv3/endpoints/news/apiv3-news-paths';
import { APIv3RolesPaths } from "core-app/core/apiv3/endpoints/roles/apiv3-roles-paths"; import { Apiv3HelpTextsPaths } from 'core-app/core/apiv3/endpoints/help_texts/apiv3-help-texts-paths';
import { APIv3VersionsPaths } from "core-app/core/apiv3/endpoints/versions/apiv3-versions-paths"; import { Apiv3ConfigurationPath } from 'core-app/core/apiv3/endpoints/configuration/apiv3-configuration-path';
import { Apiv3RelationsPaths } from "core-app/core/apiv3/endpoints/relations/apiv3-relations-paths"; import { Apiv3BoardsPaths } from 'core-app/core/apiv3/virtual/apiv3-boards-paths';
import { Apiv3NewsPaths } from "core-app/core/apiv3/endpoints/news/apiv3-news-paths"; import { RootResource } from 'core-app/features/hal/resources/root-resource';
import { Apiv3HelpTextsPaths } from "core-app/core/apiv3/endpoints/help_texts/apiv3-help-texts-paths"; import { Apiv3PlaceholderUsersPaths } from 'core-app/core/apiv3/endpoints/placeholder-users/apiv3-placeholder-users-paths';
import { Apiv3ConfigurationPath } from "core-app/core/apiv3/endpoints/configuration/apiv3-configuration-path"; import { Apiv3GroupsPaths } from 'core-app/core/apiv3/endpoints/groups/apiv3-groups-paths';
import { Apiv3BoardsPaths } from "core-app/core/apiv3/virtual/apiv3-boards-paths"; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { RootResource } from "core-app/features/hal/resources/root-resource"; import { Apiv3NotificationsPaths } from 'core-app/core/apiv3/endpoints/notifications/apiv3-notifications-paths';
import * as ts from "typescript/lib/tsserverlibrary";
import Project = ts.server.Project;
import { Apiv3PlaceholderUsersPaths } from "core-app/core/apiv3/endpoints/placeholder-users/apiv3-placeholder-users-paths";
import { Apiv3GroupsPaths } from "core-app/core/apiv3/endpoints/groups/apiv3-groups-paths";
import { HalResource } from "core-app/features/hal/resources/hal-resource";
import { Apiv3NotificationsPaths } from "core-app/core/apiv3/endpoints/notifications/apiv3-notifications-paths";
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class APIV3Service { export class APIV3Service {
@ -144,7 +138,7 @@ export class APIV3Service {
public readonly boards = this.apiV3CustomEndpoint(Apiv3BoardsPaths); public readonly boards = this.apiV3CustomEndpoint(Apiv3BoardsPaths);
constructor(readonly injector:Injector, constructor(readonly injector:Injector,
readonly pathHelper:PathHelperService) { readonly pathHelper:PathHelperService) {
} }
/** /**
@ -159,13 +153,12 @@ export class APIV3Service {
public withOptionalProject(projectIdentifier:string|number|null|undefined):APIv3ProjectPaths|this { public withOptionalProject(projectIdentifier:string|number|null|undefined):APIv3ProjectPaths|this {
if (_.isNil(projectIdentifier)) { if (_.isNil(projectIdentifier)) {
return this; return this;
} else {
return this.projects.id(projectIdentifier);
} }
return this.projects.id(projectIdentifier);
} }
public collectionFromString(fullPath:string) { public collectionFromString(fullPath:string) {
const path = fullPath.replace(this.pathHelper.api.v3.apiV3Base + '/', ''); const path = fullPath.replace(`${this.pathHelper.api.v3.apiV3Base}/`, '');
return this.apiV3CollectionEndpoint(path); return this.apiV3CollectionEndpoint(path);
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,19 +26,19 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource, APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource, APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { States } from "core-app/core/states/states.service"; import { States } from 'core-app/core/states/states.service';
import { HasId, StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { HasId, StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { tap } from "rxjs/operators"; import { tap } from 'rxjs/operators';
import { HalResource } from "core-app/features/hal/resources/hal-resource"; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
export abstract class CachableAPIV3Collection< export abstract class CachableAPIV3Collection<
T extends HasId = HalResource, T extends HasId = HalResource,
V extends APIv3GettableResource<T> = APIv3GettableResource<T>, V extends APIv3GettableResource<T> = APIv3GettableResource<T>,
X extends StateCacheService<T> = StateCacheService<T> X extends StateCacheService<T> = StateCacheService<T>,
> >
extends APIv3ResourceCollection<T, V> { extends APIv3ResourceCollection<T, V> {
@InjectField() states:States; @InjectField() states:States;
@ -51,23 +51,22 @@ export abstract class CachableAPIV3Collection<
public observeAll():Observable<T[]> { public observeAll():Observable<T[]> {
return this.cache.observeAll(); return this.cache.observeAll();
} }
/** /**
* Inserts a collection or single response to cache as an rxjs tap function * Inserts a collection or single response to cache as an rxjs tap function
*/ */
protected cacheResponse<R>():(source:Observable<R>) => Observable<R> { protected cacheResponse<R>():(source:Observable<R>) => Observable<R> {
return (source$) => { return (source$) => source$.pipe(
return source$.pipe( tap(
tap( (response:R) => {
(response:R) => { if (response instanceof CollectionResource) {
if (response instanceof CollectionResource) { response.elements.forEach(this.touch.bind(this));
response.elements.forEach(this.touch.bind(this)); } else if (response instanceof HalResource) {
} else if (response instanceof HalResource) { this.touch(response as any);
this.touch(response as any);
}
} }
) },
); ),
}; );
} }
/** /**

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,18 +26,21 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { States } from "core-app/core/states/states.service"; import { States } from 'core-app/core/states/states.service';
import { HasId, StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { HasId, StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { concat, from, merge, Observable, of } from "rxjs"; import { concat, Observable, of } from 'rxjs';
import { mapTo, publish, share, shareReplay, switchMap, take, tap } from "rxjs/operators"; import {
import { SchemaCacheService } from "core-app/core/schemas/schema-cache.service"; mapTo, shareReplay, switchMap, take, tap,
import { HalResource } from "core-app/features/hal/resources/hal-resource"; } from 'rxjs/operators';
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
export abstract class CachableAPIV3Resource<T extends HasId = HalResource> export abstract class CachableAPIV3Resource<T extends HasId = HalResource>
extends APIv3GettableResource<T> { extends APIv3GettableResource<T> {
@InjectField() states:States; @InjectField() states:States;
@InjectField() schemaCache:SchemaCacheService; @InjectField() schemaCache:SchemaCacheService;
readonly cache = this.createCache(); readonly cache = this.createCache();
@ -59,12 +62,12 @@ export abstract class CachableAPIV3Resource<T extends HasId = HalResource>
.load() .load()
.pipe( .pipe(
take(1), take(1),
shareReplay(1) shareReplay(1),
); );
this.cache.clearAndLoad( this.cache.clearAndLoad(
id, id,
observable observable,
); );
// Return concat of the loading observable // Return concat of the loading observable
@ -72,14 +75,13 @@ export abstract class CachableAPIV3Resource<T extends HasId = HalResource>
// but then continue with the streamed cache // but then continue with the streamed cache
return concat<T>( return concat<T>(
observable, observable,
this.cache.state(id).values$() this.cache.state(id).values$(),
); );
} }
return this.cache.state(id).values$(); return this.cache.state(id).values$();
} }
/** /**
* Observe the values of this resource, * Observe the values of this resource,
* but do not request it actively. * but do not request it actively.
@ -90,7 +92,6 @@ export abstract class CachableAPIV3Resource<T extends HasId = HalResource>
.observe(this.id.toString()); .observe(this.id.toString());
} }
/** /**
* Returns a (potentially cached) observable. * Returns a (potentially cached) observable.
* *
@ -102,7 +103,7 @@ export abstract class CachableAPIV3Resource<T extends HasId = HalResource>
return this return this
.requireAndStream(false) .requireAndStream(false)
.pipe( .pipe(
take(1) take(1),
); );
} }
@ -141,10 +142,9 @@ export abstract class CachableAPIV3Resource<T extends HasId = HalResource>
take(1), take(1),
mapTo(resource), mapTo(resource),
); );
} else {
return of(resource);
} }
}) return of(resource);
}),
) as any; // T does not extend HalResource for virtual endpoints such as board, thus we need to cast here ) as any; // T does not extend HalResource for virtual endpoints such as board, thus we need to cast here
} }
@ -159,13 +159,11 @@ export abstract class CachableAPIV3Resource<T extends HasId = HalResource>
* Inserts a collection response to cache as an rxjs tap function * Inserts a collection response to cache as an rxjs tap function
*/ */
protected cacheResponse():(source:Observable<T>) => Observable<T> { protected cacheResponse():(source:Observable<T>) => Observable<T> {
return (source$:Observable<T>) => { return (source$:Observable<T>) => source$.pipe(
return source$.pipe( tap(
tap( (resource:T) => this.touch(resource),
(resource:T) => this.touch(resource) ),
) );
);
};
} }
/** /**

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,8 +27,10 @@
//++ //++
import { MultiInputState, State } from 'reactivestates'; import { MultiInputState, State } from 'reactivestates';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { auditTime, map, share, startWith, take } from "rxjs/operators"; import {
auditTime, map, share, startWith, take,
} from 'rxjs/operators';
export interface HasId { export interface HasId {
id:string|null; id:string|null;
@ -36,6 +38,7 @@ export interface HasId {
export class StateCacheService<T> { export class StateCacheService<T> {
protected cacheDurationInMs:number; protected cacheDurationInMs:number;
protected multiState:MultiInputState<T>; protected multiState:MultiInputState<T>;
constructor(state:MultiInputState<T>, holdValuesForSeconds = 3600) { constructor(state:MultiInputState<T>, holdValuesForSeconds = 3600) {
@ -66,12 +69,11 @@ export class StateCacheService<T> {
* Sets a promise to the state * Sets a promise to the state
*/ */
public clearAndLoad(id:string, loader:Observable<T>):Observable<T> { public clearAndLoad(id:string, loader:Observable<T>):Observable<T> {
const observable = const observable = loader
loader .pipe(
.pipe( take(1),
take(1), share(),
share() );
);
this this
.multiState.get(id) .multiState.get(id)
@ -102,7 +104,6 @@ export class StateCacheService<T> {
return this.updateValue(resource.id!, resource as any); return this.updateValue(resource.id!, resource as any);
} }
/** /**
* Observe the value of the given id * Observe the value of the given id
*/ */
@ -135,7 +136,7 @@ export class StateCacheService<T> {
}); });
return mapped; return mapped;
}) }),
); );
} }
@ -144,7 +145,7 @@ export class StateCacheService<T> {
* @param ids * @param ids
*/ */
public clearSome(...ids:string[]) { public clearSome(...ids:string[]) {
ids.forEach(id => this.multiState.get(id).clear()); ids.forEach((id) => this.multiState.get(id).clear());
} }
/** /**
@ -173,4 +174,3 @@ export class StateCacheService<T> {
this.multiState.get(id).putValue(val); this.multiState.get(id).putValue(val);
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,27 +26,25 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Apiv3CapabilityPaths } from 'core-app/core/apiv3/endpoints/capabilities/apiv3-capability-paths';
import { Apiv3CapabilityPaths } from "core-app/core/apiv3/endpoints/capabilities/apiv3-capability-paths"; import { CapabilityResource } from 'core-app/features/hal/resources/capability-resource';
import { CapabilityResource } from "core-app/features/hal/resources/capability-resource"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { Observable } from 'rxjs';
import { Observable } from "rxjs"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CachableAPIV3Collection } from 'core-app/core/apiv3/cache/cachable-apiv3-collection';
import { CachableAPIV3Collection } from "core-app/core/apiv3/cache/cachable-apiv3-collection";
import { MultiInputState } from "reactivestates";
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, Apiv3ListResourceInterface,
listParamsString listParamsString,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { CapabilityCacheService } from "core-app/core/apiv3/endpoints/capabilities/capability-cache.service"; import { CapabilityCacheService } from 'core-app/core/apiv3/endpoints/capabilities/capability-cache.service';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
export class Apiv3CapabilitiesPaths export class Apiv3CapabilitiesPaths
extends CachableAPIV3Collection<CapabilityResource, Apiv3CapabilityPaths> extends CachableAPIV3Collection<CapabilityResource, Apiv3CapabilityPaths>
implements Apiv3ListResourceInterface<CapabilityResource> { implements Apiv3ListResourceInterface<CapabilityResource> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'capabilities', Apiv3CapabilityPaths); super(apiRoot, basePath, 'capabilities', Apiv3CapabilityPaths);
} }
@ -59,7 +57,7 @@ export class Apiv3CapabilitiesPaths
.halResourceService .halResourceService
.get<CollectionResource<CapabilityResource>>(this.path + listParamsString(params)) .get<CollectionResource<CapabilityResource>>(this.path + listParamsString(params))
.pipe( .pipe(
this.cacheResponse() this.cacheResponse(),
); );
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,10 +26,10 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { CapabilityResource } from "core-app/features/hal/resources/capability-resource"; import { CapabilityResource } from 'core-app/features/hal/resources/capability-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { Apiv3CapabilitiesPaths } from "core-app/core/apiv3/endpoints/capabilities/apiv3-capabilities-paths"; import { Apiv3CapabilitiesPaths } from 'core-app/core/apiv3/endpoints/capabilities/apiv3-capabilities-paths';
export class Apiv3CapabilityPaths extends CachableAPIV3Resource<CapabilityResource> { export class Apiv3CapabilityPaths extends CachableAPIV3Resource<CapabilityResource> {
protected createCache():StateCacheService<CapabilityResource> { protected createCache():StateCacheService<CapabilityResource> {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,13 +26,12 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { CapabilityResource } from "core-app/features/hal/resources/capability-resource"; import { CapabilityResource } from 'core-app/features/hal/resources/capability-resource';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { SchemaCacheService } from "core-app/core/schemas/schema-cache.service"; import { States } from 'core-app/core/states/states.service';
import { States } from "core-app/core/states/states.service"; import { Injector } from '@angular/core';
import { Injector } from "@angular/core"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { MultiInputState } from 'reactivestates';
import { MultiInputState } from "reactivestates";
export class CapabilityCacheService extends StateCacheService<CapabilityResource> { export class CapabilityCacheService extends StateCacheService<CapabilityResource> {
@InjectField() readonly states:States; @InjectField() readonly states:States;

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,24 +26,20 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource, APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { GridResource } from "core-app/features/hal/resources/grid-resource"; import { ConfigurationResource } from 'core-app/features/hal/resources/configuration-resource';
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { Observable } from 'rxjs';
import { ConfigurationResource } from "core-app/features/hal/resources/configuration-resource"; import { shareReplay } from 'rxjs/operators';
import { Observable } from "rxjs"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { shareReplay } from "rxjs/operators";
import { APIV3Service } from "core-app/core/apiv3/api-v3.service";
export class Apiv3ConfigurationPath extends APIv3GettableResource<ConfigurationResource> { export class Apiv3ConfigurationPath extends APIv3GettableResource<ConfigurationResource> {
private $configuration:Observable<ConfigurationResource>; private $configuration:Observable<ConfigurationResource>;
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
readonly basePath:string) { readonly basePath:string) {
super(apiRoot, basePath, 'configuration'); super(apiRoot, basePath, 'configuration');
} }
public get():Observable<ConfigurationResource> { public get():Observable<ConfigurationResource> {
if (this.$configuration) { if (this.$configuration) {
return this.$configuration; return this.$configuration;
@ -52,7 +48,7 @@ export class Apiv3ConfigurationPath extends APIv3GettableResource<ConfigurationR
return this.$configuration = this.halResourceService return this.$configuration = this.halResourceService
.get<ConfigurationResource>(this.path) .get<ConfigurationResource>(this.path)
.pipe( .pipe(
shareReplay() shareReplay(),
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,14 +26,13 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { HalPayloadHelper } from "core-app/features/hal/schemas/hal-payload.helper"; import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper';
import { GridWidgetResource } from "core-app/features/hal/resources/grid-widget-resource"; import { GridWidgetResource } from 'core-app/features/hal/resources/grid-widget-resource';
import { HalResource } from "core-app/features/hal/resources/hal-resource"; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
export class Apiv3GridForm extends APIv3FormResource { export class Apiv3GridForm extends APIv3FormResource {
/** /**
* We need to override the grid widget extraction * We need to override the grid widget extraction
* to pass the correct payload to the API. * to pass the correct payload to the API.
@ -43,23 +42,21 @@ export class Apiv3GridForm extends APIv3FormResource {
*/ */
public static extractPayload(resource:HalResource|Object, schema:SchemaResource|null = null):Object { public static extractPayload(resource:HalResource|Object, schema:SchemaResource|null = null):Object {
if (resource instanceof HalResource && schema) { if (resource instanceof HalResource && schema) {
const grid = resource as HalResource; const grid = resource;
const payload = HalPayloadHelper.extractPayloadFromSchema(grid, schema); const payload = HalPayloadHelper.extractPayloadFromSchema(grid, schema);
// The widget only states the type of the widget resource but does not explain // The widget only states the type of the widget resource but does not explain
// the widget itself. We therefore have to do that by hand. // the widget itself. We therefore have to do that by hand.
if (payload.widgets) { if (payload.widgets) {
payload.widgets = grid.widgets.map((widget:GridWidgetResource) => { payload.widgets = grid.widgets.map((widget:GridWidgetResource) => ({
return { id: widget.id,
id: widget.id, startRow: widget.startRow,
startRow: widget.startRow, endRow: widget.endRow,
endRow: widget.endRow, startColumn: widget.startColumn,
startColumn: widget.startColumn, endColumn: widget.endColumn,
endColumn: widget.endColumn, identifier: widget.identifier,
identifier: widget.identifier, options: widget.options,
options: widget.options }));
};
});
} }
return payload; return payload;
@ -77,5 +74,4 @@ export class Apiv3GridForm extends APIv3FormResource {
public extractPayload(request:HalResource|Object, schema:SchemaResource|null = null) { public extractPayload(request:HalResource|Object, schema:SchemaResource|null = null) {
return Apiv3GridForm.extractPayload(request, schema); return Apiv3GridForm.extractPayload(request, schema);
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,11 +26,11 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { GridResource } from "core-app/features/hal/resources/grid-resource"; import { GridResource } from 'core-app/features/hal/resources/grid-resource';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { Apiv3GridForm } from "core-app/core/apiv3/endpoints/grids/apiv3-grid-form"; import { Apiv3GridForm } from 'core-app/core/apiv3/endpoints/grids/apiv3-grid-form';
export class Apiv3GridPaths extends APIv3GettableResource<GridResource> { export class Apiv3GridPaths extends APIv3GettableResource<GridResource> {
// Static paths // Static paths

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,25 +26,25 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { Apiv3GridPaths } from "core-app/core/apiv3/endpoints/grids/apiv3-grid-paths"; import { Apiv3GridPaths } from 'core-app/core/apiv3/endpoints/grids/apiv3-grid-paths';
import { GridResource } from "core-app/features/hal/resources/grid-resource"; import { GridResource } from 'core-app/features/hal/resources/grid-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { Apiv3GridForm } from "core-app/core/apiv3/endpoints/grids/apiv3-grid-form"; import { Apiv3GridForm } from 'core-app/core/apiv3/endpoints/grids/apiv3-grid-form';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, Apiv3ListResourceInterface,
listParamsString listParamsString,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
export class Apiv3GridsPaths export class Apiv3GridsPaths
extends APIv3ResourceCollection<GridResource, Apiv3GridPaths> extends APIv3ResourceCollection<GridResource, Apiv3GridPaths>
implements Apiv3ListResourceInterface<GridResource> { implements Apiv3ListResourceInterface<GridResource> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'grids', Apiv3GridPaths); super(apiRoot, basePath, 'grids', Apiv3GridPaths);
} }
@ -70,8 +70,8 @@ export class Apiv3GridsPaths
return this return this
.halResourceService .halResourceService
.post<GridResource>( .post<GridResource>(
this.path, this.path,
this.form.extractPayload(resource, schema) this.form.extractPayload(resource, schema),
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,9 +26,9 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { GroupResource } from "core-app/features/hal/resources/group-resource"; import { GroupResource } from 'core-app/features/hal/resources/group-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
export class Apiv3GroupPaths extends APIv3GettableResource<GroupResource> { export class Apiv3GroupPaths extends APIv3GettableResource<GroupResource> {
/** /**
@ -39,8 +39,8 @@ export class Apiv3GroupPaths extends APIv3GettableResource<GroupResource> {
return this return this
.halResourceService .halResourceService
.patch<GroupResource>(this.path, { .patch<GroupResource>(this.path, {
name: resource.name, name: resource.name,
}); });
} }
/** /**

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,23 +26,23 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { Apiv3GroupPaths } from "core-app/core/apiv3/endpoints/groups/apiv3-group-paths"; import { Apiv3GroupPaths } from 'core-app/core/apiv3/endpoints/groups/apiv3-group-paths';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, Apiv3ListResourceInterface,
listParamsString listParamsString,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { GroupResource } from "core-app/features/hal/resources/group-resource"; import { GroupResource } from 'core-app/features/hal/resources/group-resource';
export class Apiv3GroupsPaths export class Apiv3GroupsPaths
extends APIv3ResourceCollection<GroupResource, Apiv3GroupPaths> extends APIv3ResourceCollection<GroupResource, Apiv3GroupPaths>
implements Apiv3ListResourceInterface<GroupResource> { implements Apiv3ListResourceInterface<GroupResource> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'groups', Apiv3GroupPaths); super(apiRoot, basePath, 'groups', Apiv3GroupPaths);
} }
@ -65,8 +65,8 @@ export class Apiv3GroupsPaths
return this return this
.halResourceService .halResourceService
.post<GroupResource>( .post<GroupResource>(
this.path, this.path,
resource, resource,
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,16 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource, APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource, APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { HelpTextResource } from "core-app/features/hal/resources/help-text-resource"; import { HelpTextResource } from 'core-app/features/hal/resources/help-text-resource';
export class Apiv3HelpTextsPaths export class Apiv3HelpTextsPaths
extends APIv3ResourceCollection<HelpTextResource, APIv3GettableResource<HelpTextResource>> { extends APIv3ResourceCollection<HelpTextResource, APIv3GettableResource<HelpTextResource>> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'help_texts'); super(apiRoot, basePath, 'help_texts');
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,14 +26,10 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import {APIv3FormResource} from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import {SchemaResource} from "core-app/features/hal/resources/schema-resource"; import { MembershipResourceEmbedded } from 'core-app/features/hal/resources/membership-resource';
import {HalPayloadHelper} from "core-app/features/hal/schemas/hal-payload.helper";
import {HalResource} from "core-app/features/hal/resources/hal-resource";
import {MembershipResource, MembershipResourceEmbedded} from "core-app/features/hal/resources/membership-resource";
export class Apiv3MembershipsForm extends APIv3FormResource { export class Apiv3MembershipsForm extends APIv3FormResource {
/** /**
* We need to override the grid widget extraction * We need to override the grid widget extraction
* to pass the correct payload to the API. * to pass the correct payload to the API.
@ -46,14 +42,14 @@ export class Apiv3MembershipsForm extends APIv3FormResource {
_links: { _links: {
project: { href: resource.project.href }, project: { href: resource.project.href },
principal: { href: resource.principal.href }, principal: { href: resource.principal.href },
roles: resource.roles.map(role => ({ href: role.href })), roles: resource.roles.map((role) => ({ href: role.href })),
}, },
_meta: { _meta: {
notificationMessage: { notificationMessage: {
raw: resource.notificationMessage.raw raw: resource.notificationMessage.raw,
} },
} },
} };
} }
/** /**
@ -65,5 +61,4 @@ export class Apiv3MembershipsForm extends APIv3FormResource {
public extractPayload(request:MembershipResourceEmbedded) { public extractPayload(request:MembershipResourceEmbedded) {
return Apiv3MembershipsForm.extractPayload(request); return Apiv3MembershipsForm.extractPayload(request);
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,28 +26,27 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import {APIv3GettableResource, APIv3ResourceCollection} from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource, APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import {APIV3Service} from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import {Apiv3AvailableProjectsPaths} from "core-app/core/apiv3/endpoints/projects/apiv3-available-projects-paths"; import { Apiv3AvailableProjectsPaths } from 'core-app/core/apiv3/endpoints/projects/apiv3-available-projects-paths';
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, listParamsString Apiv3ListResourceInterface,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; listParamsString,
import {Observable} from "rxjs"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import {Apiv3MembershipsForm} from "core-app/core/apiv3/endpoints/memberships/apiv3-memberships-form"; import { Observable } from 'rxjs';
import { MembershipResource, MembershipResourceEmbedded } from "core-app/features/hal/resources/membership-resource"; import { Apiv3MembershipsForm } from 'core-app/core/apiv3/endpoints/memberships/apiv3-memberships-form';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { MembershipResource, MembershipResourceEmbedded } from 'core-app/features/hal/resources/membership-resource';
import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
export class Apiv3MembershipsPaths export class Apiv3MembershipsPaths
extends APIv3ResourceCollection<MembershipResource, APIv3GettableResource<MembershipResource>> extends APIv3ResourceCollection<MembershipResource, APIv3GettableResource<MembershipResource>>
implements Apiv3ListResourceInterface<MembershipResource> { implements Apiv3ListResourceInterface<MembershipResource> {
// Static paths // Static paths
readonly form = this.subResource('form', Apiv3MembershipsForm); readonly form = this.subResource('form', Apiv3MembershipsForm);
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'memberships'); super(apiRoot, basePath, 'memberships');
} }
@ -61,7 +60,6 @@ export class Apiv3MembershipsPaths
.get<CollectionResource<MembershipResource>>(this.path + listParamsString(params)); .get<CollectionResource<MembershipResource>>(this.path + listParamsString(params));
} }
// /api/v3/memberships/available_projects // /api/v3/memberships/available_projects
readonly available_projects = this.subResource('available_projects', Apiv3AvailableProjectsPaths); readonly available_projects = this.subResource('available_projects', Apiv3AvailableProjectsPaths);
@ -75,9 +73,8 @@ export class Apiv3MembershipsPaths
return this return this
.halResourceService .halResourceService
.post<MembershipResource>( .post<MembershipResource>(
this.path, this.path,
payload, payload,
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,24 +26,23 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource, APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { APIv3GettableResource, APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { TimeEntryResource } from 'core-app/features/hal/resources/time-entry-resource';
import { TimeEntryResource } from "core-app/features/hal/resources/time-entry-resource"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { Observable } from 'rxjs';
import { Observable } from "rxjs"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource";
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, Apiv3ListResourceInterface,
listParamsString listParamsString,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { NewsResource } from "core-app/features/hal/resources/news-resource"; import { NewsResource } from 'core-app/features/hal/resources/news-resource';
export class Apiv3NewsPaths export class Apiv3NewsPaths
extends APIv3ResourceCollection<NewsResource, APIv3GettableResource<NewsResource>> extends APIv3ResourceCollection<NewsResource, APIv3GettableResource<NewsResource>>
implements Apiv3ListResourceInterface<NewsResource> { implements Apiv3ListResourceInterface<NewsResource> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'news'); super(apiRoot, basePath, 'news');
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,11 +26,11 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { InAppNotification } from "core-app/features/in-app-notifications/store/in-app-notification.model"; import { InAppNotification } from 'core-app/features/in-app-notifications/store/in-app-notification.model';
import { HttpClient } from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
export class Apiv3NotificationPaths extends APIv3GettableResource<InAppNotification> { export class Apiv3NotificationPaths extends APIv3GettableResource<InAppNotification> {
@InjectField() http:HttpClient; @InjectField() http:HttpClient;
@ -39,12 +39,12 @@ export class Apiv3NotificationPaths extends APIv3GettableResource<InAppNotificat
return this return this
.http .http
.post( .post(
this.path + '/read_ian', `${this.path}/read_ian`,
{}, {},
{ {
withCredentials: true, withCredentials: true,
responseType: 'json' responseType: 'json',
} },
); );
} }
@ -52,12 +52,12 @@ export class Apiv3NotificationPaths extends APIv3GettableResource<InAppNotificat
return this return this
.http .http
.post( .post(
this.path + '/unread_ian', `${this.path}/unread_ian`,
{}, {},
{ {
withCredentials: true, withCredentials: true,
responseType: 'json' responseType: 'json',
} },
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,32 +26,31 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { Apiv3ListParameters, listParamsString } from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; import { Apiv3ListParameters, listParamsString } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { InAppNotification } from "core-app/features/in-app-notifications/store/in-app-notification.model"; import { InAppNotification } from 'core-app/features/in-app-notifications/store/in-app-notification.model';
import { Apiv3NotificationPaths } from "core-app/core/apiv3/endpoints/notifications/apiv3-notification-paths"; import { Apiv3NotificationPaths } from 'core-app/core/apiv3/endpoints/notifications/apiv3-notification-paths';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { HttpClient } from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import { IHALCollection } from "core-app/core/apiv3/types/hal-collection.type"; import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
import { ID } from "@datorama/akita"; import { ID } from '@datorama/akita';
export class Apiv3NotificationsPaths export class Apiv3NotificationsPaths
extends APIv3ResourceCollection<InAppNotification, Apiv3NotificationPaths> { extends APIv3ResourceCollection<InAppNotification, Apiv3NotificationPaths> {
@InjectField() http:HttpClient; @InjectField() http:HttpClient;
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'notifications', Apiv3NotificationPaths); super(apiRoot, basePath, 'notifications', Apiv3NotificationPaths);
} }
public facet(facet:string, params?:Apiv3ListParameters):Observable<IHALCollection<InAppNotification>> { public facet(facet:string, params?:Apiv3ListParameters):Observable<IHALCollection<InAppNotification>> {
if(facet === 'unread') { if (facet === 'unread') {
return this.unread(params); return this.unread(params);
} else { }
return this.list(params); return this.list(params);
};
} }
/** /**
@ -70,7 +69,7 @@ export class Apiv3NotificationsPaths
public unread(additional?:Apiv3ListParameters):Observable<IHALCollection<InAppNotification>> { public unread(additional?:Apiv3ListParameters):Observable<IHALCollection<InAppNotification>> {
const params:Apiv3ListParameters = { const params:Apiv3ListParameters = {
...additional, ...additional,
filters: [["readIAN", "=", false]] filters: [['readIAN', '=', false]],
}; };
return this.list(params); return this.list(params);
@ -84,12 +83,12 @@ export class Apiv3NotificationsPaths
return this return this
.http .http
.post( .post(
this.path + '/read_ian' + listParamsString({ filters: [['id', "=", ids.map(id => id.toString())]] }), `${this.path}/read_ian${listParamsString({ filters: [['id', '=', ids.map((id) => id.toString())]] })}`,
{}, {},
{ {
withCredentials: true, withCredentials: true,
responseType: 'json' responseType: 'json',
} },
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,9 +26,9 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { PlaceholderUserResource } from "core-app/features/hal/resources/placeholder-user-resource"; import { PlaceholderUserResource } from 'core-app/features/hal/resources/placeholder-user-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
export class Apiv3PlaceholderUserPaths extends APIv3GettableResource<PlaceholderUserResource> { export class Apiv3PlaceholderUserPaths extends APIv3GettableResource<PlaceholderUserResource> {
/** /**
@ -39,8 +39,8 @@ export class Apiv3PlaceholderUserPaths extends APIv3GettableResource<Placeholder
return this return this
.halResourceService .halResourceService
.patch<PlaceholderUserResource>(this.path, { .patch<PlaceholderUserResource>(this.path, {
name: resource.name name: resource.name,
}); });
} }
/** /**

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,23 +26,23 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { Apiv3PlaceholderUserPaths } from "core-app/core/apiv3/endpoints/placeholder-users/apiv3-placeholder-user-paths"; import { Apiv3PlaceholderUserPaths } from 'core-app/core/apiv3/endpoints/placeholder-users/apiv3-placeholder-user-paths';
import { PlaceholderUserResource } from "core-app/features/hal/resources/placeholder-user-resource"; import { PlaceholderUserResource } from 'core-app/features/hal/resources/placeholder-user-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, Apiv3ListResourceInterface,
listParamsString listParamsString,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
export class Apiv3PlaceholderUsersPaths export class Apiv3PlaceholderUsersPaths
extends APIv3ResourceCollection<PlaceholderUserResource, Apiv3PlaceholderUserPaths> extends APIv3ResourceCollection<PlaceholderUserResource, Apiv3PlaceholderUserPaths>
implements Apiv3ListResourceInterface<PlaceholderUserResource> { implements Apiv3ListResourceInterface<PlaceholderUserResource> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'placeholder_users', Apiv3PlaceholderUserPaths); super(apiRoot, basePath, 'placeholder_users', Apiv3PlaceholderUserPaths);
} }
@ -65,8 +65,8 @@ export class Apiv3PlaceholderUsersPaths
return this return this
.halResourceService .halResourceService
.post<PlaceholderUserResource>( .post<PlaceholderUserResource>(
this.path, this.path,
resource, resource,
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,21 +26,21 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { ProjectResource } from "core-app/features/hal/resources/project-resource"; import { ProjectResource } from 'core-app/features/hal/resources/project-resource';
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { map } from "rxjs/operators"; import { map } from 'rxjs/operators';
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, listParamsString Apiv3ListResourceInterface,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; listParamsString,
import { buildApiV3Filter } from "core-app/shared/helpers/api-v3/api-v3-filter-builder"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { buildApiV3Filter } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
export class Apiv3AvailableProjectsPaths export class Apiv3AvailableProjectsPaths
extends APIv3GettableResource<CollectionResource<ProjectResource>> extends APIv3GettableResource<CollectionResource<ProjectResource>>
implements Apiv3ListResourceInterface<ProjectResource> { implements Apiv3ListResourceInterface<ProjectResource> {
/** /**
* Load a list of available projects with a given list parameter filter * Load a list of available projects with a given list parameter filter
* @param params * @param params
@ -64,12 +64,11 @@ export class Apiv3AvailableProjectsPaths
return this return this
.halResourceService .halResourceService
.get<CollectionResource<ProjectResource>>( .get<CollectionResource<ProjectResource>>(
this.path, this.path,
{ filters: buildApiV3Filter('id', '=', [projectId]).toJson() } { filters: buildApiV3Filter('id', '=', [projectId]).toJson() },
) )
.pipe( .pipe(
map(collection => collection.count > 0) map((collection) => collection.count > 0),
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,13 +26,13 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { SimpleResource } from "core-app/core/apiv3/paths/path-resources"; import { SimpleResource } from 'core-app/core/apiv3/paths/path-resources';
export class APIv3ProjectCopyPaths extends SimpleResource { export class APIv3ProjectCopyPaths extends SimpleResource {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
public basePath:string) { public basePath:string) {
super(basePath, 'copy'); super(basePath, 'copy');
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,15 +26,15 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3QueriesPaths } from "core-app/core/apiv3/endpoints/queries/apiv3-queries-paths"; import { APIv3QueriesPaths } from 'core-app/core/apiv3/endpoints/queries/apiv3-queries-paths';
import { APIv3TypesPaths } from "core-app/core/apiv3/endpoints/types/apiv3-types-paths"; import { APIv3TypesPaths } from 'core-app/core/apiv3/endpoints/types/apiv3-types-paths';
import { APIV3WorkPackagesPaths } from "core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths"; import { APIV3WorkPackagesPaths } from 'core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths';
import { ProjectResource } from "core-app/features/hal/resources/project-resource"; import { ProjectResource } from 'core-app/features/hal/resources/project-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { APIv3VersionsPaths } from "core-app/core/apiv3/endpoints/versions/apiv3-versions-paths"; import { APIv3VersionsPaths } from 'core-app/core/apiv3/endpoints/versions/apiv3-versions-paths';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { APIv3ProjectsPaths } from "core-app/core/apiv3/endpoints/projects/apiv3-projects-paths"; import { APIv3ProjectsPaths } from 'core-app/core/apiv3/endpoints/projects/apiv3-projects-paths';
import { APIv3ProjectCopyPaths } from "core-app/core/apiv3/endpoints/projects/apiv3-project-copy-paths"; import { APIv3ProjectCopyPaths } from 'core-app/core/apiv3/endpoints/projects/apiv3-project-copy-paths';
export class APIv3ProjectPaths extends CachableAPIV3Resource<ProjectResource> { export class APIv3ProjectPaths extends CachableAPIV3Resource<ProjectResource> {
// /api/v3/projects/:project_id/available_assignees // /api/v3/projects/:project_id/available_assignees

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,26 +26,26 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ProjectPaths } from "core-app/core/apiv3/endpoints/projects/apiv3-project-paths"; import { APIv3ProjectPaths } from 'core-app/core/apiv3/endpoints/projects/apiv3-project-paths';
import { ProjectResource } from "core-app/features/hal/resources/project-resource"; import { ProjectResource } from 'core-app/features/hal/resources/project-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, Apiv3ListResourceInterface,
listParamsString listParamsString,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { CachableAPIV3Collection } from "core-app/core/apiv3/cache/cachable-apiv3-collection"; import { CachableAPIV3Collection } from 'core-app/core/apiv3/cache/cachable-apiv3-collection';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { ProjectCache } from "core-app/core/apiv3/endpoints/projects/project.cache"; import { ProjectCache } from 'core-app/core/apiv3/endpoints/projects/project.cache';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
export class APIv3ProjectsPaths export class APIv3ProjectsPaths
extends CachableAPIV3Collection<ProjectResource, APIv3ProjectPaths> extends CachableAPIV3Collection<ProjectResource, APIv3ProjectPaths>
implements Apiv3ListResourceInterface<ProjectResource> { implements Apiv3ListResourceInterface<ProjectResource> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'projects', APIv3ProjectPaths); super(apiRoot, basePath, 'projects', APIv3ProjectPaths);
} }
@ -62,7 +62,7 @@ export class APIv3ProjectsPaths
.halResourceService .halResourceService
.get<CollectionResource<ProjectResource>>(this.path + listParamsString(params)) .get<CollectionResource<ProjectResource>>(this.path + listParamsString(params))
.pipe( .pipe(
this.cacheResponse() this.cacheResponse(),
); );
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,13 +27,11 @@
//++ //++
import { MultiInputState } from 'reactivestates'; import { MultiInputState } from 'reactivestates';
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource";
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { debugLog } from "core-app/shared/helpers/debug_output"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import { SchemaCacheService } from "core-app/core/schemas/schema-cache.service"; import { ProjectResource } from 'core-app/features/hal/resources/project-resource';
import { ProjectResource } from "core-app/features/hal/resources/project-resource";
@Injectable() @Injectable()
export class ProjectCache extends StateCacheService<ProjectResource> { export class ProjectCache extends StateCacheService<ProjectResource> {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,24 +26,24 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource, APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource, APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { APIv3QueryPaths } from "core-app/core/apiv3/endpoints/queries/apiv3-query-paths"; import { APIv3QueryPaths } from 'core-app/core/apiv3/endpoints/queries/apiv3-query-paths';
import { QueryResource } from "core-app/features/hal/resources/query-resource"; import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { Apiv3QueryForm } from "core-app/core/apiv3/endpoints/queries/apiv3-query-form"; import { Apiv3QueryForm } from 'core-app/core/apiv3/endpoints/queries/apiv3-query-form';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { QueryFormResource } from "core-app/features/hal/resources/query-form-resource"; import { QueryFormResource } from 'core-app/features/hal/resources/query-form-resource';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { Apiv3ListParameters, listParamsString } from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; import { Apiv3ListParameters, listParamsString } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { QueryFiltersService } from "core-app/features/work-packages/components/wp-query/query-filters.service"; import { QueryFiltersService } from 'core-app/features/work-packages/components/wp-query/query-filters.service';
import { HalPayloadHelper } from "core-app/features/hal/schemas/hal-payload.helper"; import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper';
export class APIv3QueriesPaths extends APIv3ResourceCollection<QueryResource, APIv3QueryPaths> { export class APIv3QueriesPaths extends APIv3ResourceCollection<QueryResource, APIv3QueryPaths> {
@InjectField() private queryFilters:QueryFiltersService; @InjectField() private queryFilters:QueryFiltersService;
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'queries', APIv3QueryPaths); super(apiRoot, basePath, 'queries', APIv3QueryPaths);
} }
@ -88,7 +88,6 @@ export class APIv3QueriesPaths extends APIv3ResourceCollection<QueryResource, AP
.get<QueryResource>(path, queryData); .get<QueryResource>(path, queryData);
} }
/** /**
* Stream the response for the given query request * Stream the response for the given query request
* *
@ -97,9 +96,9 @@ export class APIv3QueriesPaths extends APIv3ResourceCollection<QueryResource, AP
public parameterised(params:Object):Observable<QueryResource> { public parameterised(params:Object):Observable<QueryResource> {
return this.halResourceService return this.halResourceService
.get<QueryResource>( .get<QueryResource>(
this.default.path, this.default.path,
params params,
); );
} }
/** /**
@ -118,8 +117,8 @@ export class APIv3QueriesPaths extends APIv3ResourceCollection<QueryResource, AP
return this return this
.halResourceService .halResourceService
.post<QueryResource>( .post<QueryResource>(
this.apiRoot.queries.path, payload this.apiRoot.queries.path, payload,
); );
} }
/** /**
@ -130,9 +129,8 @@ export class APIv3QueriesPaths extends APIv3ResourceCollection<QueryResource, AP
public toggleStarred(query:QueryResource):Promise<unknown> { public toggleStarred(query:QueryResource):Promise<unknown> {
if (query.starred) { if (query.starred) {
return query.unstar(); return query.unstar();
} else {
return query.star();
} }
return query.star();
} }
/** /**
@ -142,7 +140,7 @@ export class APIv3QueriesPaths extends APIv3ResourceCollection<QueryResource, AP
*/ */
public filterNonHidden(projectIdentifier?:string|null):Observable<CollectionResource<QueryResource>> { public filterNonHidden(projectIdentifier?:string|null):Observable<CollectionResource<QueryResource>> {
const listParams:Apiv3ListParameters = { const listParams:Apiv3ListParameters = {
filters: [['hidden', '=', ['f']]] filters: [['hidden', '=', ['f']]],
}; };
if (projectIdentifier) { if (projectIdentifier) {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,14 +26,14 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { QueryResource } from "core-app/features/hal/resources/query-resource"; import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import { QueryFormResource } from "core-app/features/hal/resources/query-form-resource"; import { QueryFormResource } from 'core-app/features/hal/resources/query-form-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import * as URI from "urijs"; import * as URI from 'urijs';
import { map, tap } from "rxjs/operators"; import { map, tap } from 'rxjs/operators';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { QueryFiltersService } from "core-app/features/work-packages/components/wp-query/query-filters.service"; import { QueryFiltersService } from 'core-app/features/work-packages/components/wp-query/query-filters.service';
export class Apiv3QueryForm extends APIv3FormResource<QueryFormResource> { export class Apiv3QueryForm extends APIv3FormResource<QueryFormResource> {
@InjectField() private queryFilters:QueryFiltersService; @InjectField() private queryFilters:QueryFiltersService;
@ -47,23 +47,23 @@ export class Apiv3QueryForm extends APIv3FormResource<QueryFormResource> {
// can check whether form saving is possible. // can check whether form saving is possible.
// The query needs a name to be valid. // The query needs a name to be valid.
const payload:any = { const payload:any = {
'name': query.name || '!!!__O__o__O__!!!' name: query.name || '!!!__O__o__O__!!!',
}; };
if (query.project) { if (query.project) {
payload['_links'] = { payload._links = {
'project': { project: {
'href': query.project.href href: query.project.href,
} },
}; };
} }
const path = this.apiRoot.queries.withOptionalId(query.id).form.path; const { path } = this.apiRoot.queries.withOptionalId(query.id).form;
return this.halResourceService return this.halResourceService
.post<QueryFormResource>(path, payload) .post<QueryFormResource>(path, payload)
.pipe( .pipe(
tap(form => this.queryFilters.setSchemas(form.$embedded.schema.$embedded.filtersSchemas)), tap((form) => this.queryFilters.setSchemas(form.$embedded.schema.$embedded.filtersSchemas)),
map(form => [form, this.buildQueryResource(form)]) map((form) => [form, this.buildQueryResource(form)]),
); );
} }
@ -75,7 +75,7 @@ export class Apiv3QueryForm extends APIv3FormResource<QueryFormResource> {
* @param projectIdentifier * @param projectIdentifier
* @param payload * @param payload
*/ */
public loadWithParams(params:{[key:string]:unknown}, queryId:string|undefined, projectIdentifier:string|undefined|null, payload:any = {}):Observable<[QueryFormResource, QueryResource]> { public loadWithParams(params:{ [key:string]:unknown }, queryId:string|undefined, projectIdentifier:string|undefined|null, payload:any = {}):Observable<[QueryFormResource, QueryResource]> {
// We need a valid payload so that we // We need a valid payload so that we
// can check whether form saving is possible. // can check whether form saving is possible.
// The query needs a name to be valid. // The query needs a name to be valid.
@ -86,18 +86,17 @@ export class Apiv3QueryForm extends APIv3FormResource<QueryFormResource> {
if (projectIdentifier) { if (projectIdentifier) {
payload._links = payload._links || {}; payload._links = payload._links || {};
payload._links.project = { payload._links.project = {
'href': this.apiRoot.projects.id(projectIdentifier).toString() href: this.apiRoot.projects.id(projectIdentifier).toString(),
}; };
} }
const path = this.apiRoot.queries.withOptionalId(queryId).form.path; const { path } = this.apiRoot.queries.withOptionalId(queryId).form;
const href = URI(path).search(params).toString(); const href = URI(path).search(params).toString();
return this.halResourceService return this.halResourceService
.post<QueryFormResource>(href, payload) .post<QueryFormResource>(href, payload)
.pipe( .pipe(
tap(form => this.queryFilters.setSchemas(form.$embedded.schema.$embedded.filtersSchemas)), tap((form) => this.queryFilters.setSchemas(form.$embedded.schema.$embedded.filtersSchemas)),
map(form => [form, this.buildQueryResource(form)]) map((form) => [form, this.buildQueryResource(form)]),
); );
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,10 +26,10 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injector } from "@angular/core"; import { Injector } from '@angular/core';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { HttpClient } from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import { SimpleResource } from "core-app/core/apiv3/paths/path-resources"; import { SimpleResource } from 'core-app/core/apiv3/paths/path-resources';
export type QueryOrder = { [wpId:string]:number }; export type QueryOrder = { [wpId:string]:number };
@ -37,34 +37,34 @@ export class APIV3QueryOrder extends SimpleResource {
@InjectField() http:HttpClient; @InjectField() http:HttpClient;
constructor(readonly injector:Injector, constructor(readonly injector:Injector,
readonly basePath:string, readonly basePath:string,
readonly id:string|number) { readonly id:string|number) {
super(basePath, id); super(basePath, id);
} }
public get():Promise<QueryOrder> { public get():Promise<QueryOrder> {
return this.http return this.http
.get<QueryOrder>( .get<QueryOrder>(
this.path this.path,
) )
.toPromise() .toPromise()
.then(result => result || {}); .then((result) => result || {});
} }
public update(delta:QueryOrder):Promise<string> { public update(delta:QueryOrder):Promise<string> {
return this.http return this.http
.patch( .patch(
this.path, this.path,
{ delta: delta }, { delta },
{ withCredentials: true } { withCredentials: true },
) )
.toPromise() .toPromise()
.then((response:{t:string}) => response.t); .then((response:{ t:string }) => response.t);
} }
public delete(id:string, ...wpIds:string[]) { public delete(id:string, ...wpIds:string[]) {
const delta:QueryOrder = {}; const delta:QueryOrder = {};
wpIds.forEach(id => delta[id] = -1); wpIds.forEach((id) => delta[id] = -1);
return this.update(delta); return this.update(delta);
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,16 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { QueryResource } from "core-app/features/hal/resources/query-resource"; import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { APIV3QueryOrder } from "core-app/core/apiv3/endpoints/queries/apiv3-query-order"; import { APIV3QueryOrder } from 'core-app/core/apiv3/endpoints/queries/apiv3-query-order';
import { Apiv3QueryForm } from "core-app/core/apiv3/endpoints/queries/apiv3-query-form"; import { Apiv3QueryForm } from 'core-app/core/apiv3/endpoints/queries/apiv3-query-form';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { QueryFormResource } from "core-app/features/hal/resources/query-form-resource"; import { QueryFormResource } from 'core-app/features/hal/resources/query-form-resource';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { QueryFiltersService } from "core-app/features/work-packages/components/wp-query/query-filters.service"; import { QueryFiltersService } from 'core-app/features/work-packages/components/wp-query/query-filters.service';
import { HalPayloadHelper } from "core-app/features/hal/schemas/hal-payload.helper"; import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper';
import { PaginationObject } from "core-app/shared/components/table-pagination/pagination-service"; import { PaginationObject } from 'core-app/shared/components/table-pagination/pagination-service';
export class APIv3QueryPaths extends APIv3GettableResource<QueryResource> { export class APIv3QueryPaths extends APIv3GettableResource<QueryResource> {
@InjectField() private queryFilters:QueryFiltersService; @InjectField() private queryFilters:QueryFiltersService;
@ -88,5 +88,4 @@ export class APIv3QueryPaths extends APIv3GettableResource<QueryResource> {
public paginated(pagination:PaginationObject):Observable<QueryResource> { public paginated(pagination:PaginationObject):Observable<QueryResource> {
return this.parameterised(pagination); return this.parameterised(pagination);
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,17 +26,17 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource, APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource, APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { from, Observable } from "rxjs"; import { from, Observable } from 'rxjs';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { RelationResource } from "core-app/features/hal/resources/relation-resource"; import { RelationResource } from 'core-app/features/hal/resources/relation-resource';
import { map } from "rxjs/operators"; import { map } from 'rxjs/operators';
import { buildApiV3Filter } from "core-app/shared/helpers/api-v3/api-v3-filter-builder"; import { buildApiV3Filter } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
export class Apiv3RelationsPaths extends APIv3ResourceCollection<RelationResource, APIv3GettableResource<RelationResource>> { export class Apiv3RelationsPaths extends APIv3ResourceCollection<RelationResource, APIv3GettableResource<RelationResource>> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'relations'); super(apiRoot, basePath, 'relations');
} }
@ -50,7 +50,7 @@ export class Apiv3RelationsPaths extends APIv3ResourceCollection<RelationResourc
} }
public loadInvolved(workPackageIds:string[]):Observable<RelationResource[]> { public loadInvolved(workPackageIds:string[]):Observable<RelationResource[]> {
const validIds = _.filter(workPackageIds, id => /\d+/.test(id)); const validIds = _.filter(workPackageIds, (id) => /\d+/.test(id));
if (validIds.length === 0) { if (validIds.length === 0) {
return from([]); return from([]);
@ -60,7 +60,7 @@ export class Apiv3RelationsPaths extends APIv3ResourceCollection<RelationResourc
.filtered(buildApiV3Filter('involved', '=', validIds)) .filtered(buildApiV3Filter('involved', '=', validIds))
.get() .get()
.pipe( .pipe(
map(collection => collection.elements) map((collection) => collection.elements),
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,12 +26,11 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { RoleResource } from "core-app/features/hal/resources/role-resource"; import { RoleResource } from 'core-app/features/hal/resources/role-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
export class APIv3RolePaths extends CachableAPIV3Resource<RoleResource> { export class APIv3RolePaths extends CachableAPIV3Resource<RoleResource> {
protected createCache():StateCacheService<RoleResource> { protected createCache():StateCacheService<RoleResource> {
return new StateCacheService<RoleResource>(this.states.roles); return new StateCacheService<RoleResource>(this.states.roles);
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,18 +26,17 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection, APIv3ResourcePath } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { Injector } from "@angular/core"; import { RoleResource } from 'core-app/features/hal/resources/role-resource';
import { RoleResource } from "core-app/features/hal/resources/role-resource"; import { APIv3RolePaths } from 'core-app/core/apiv3/endpoints/roles/apiv3-role-paths';
import { APIv3RolePaths } from "core-app/core/apiv3/endpoints/roles/apiv3-role-paths"; import { Observable } from 'rxjs';
import { Observable } from "rxjs"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { tap } from 'rxjs/operators';
import { tap } from "rxjs/operators"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service";
export class APIv3RolesPaths extends APIv3ResourceCollection<RoleResource, APIv3RolePaths> { export class APIv3RolesPaths extends APIv3ResourceCollection<RoleResource, APIv3RolePaths> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'roles', APIv3RolePaths); super(apiRoot, basePath, 'roles', APIv3RolePaths);
} }
@ -49,12 +48,11 @@ export class APIv3RolesPaths extends APIv3ResourceCollection<RoleResource, APIv3
.halResourceService .halResourceService
.get<CollectionResource<RoleResource>>(this.path) .get<CollectionResource<RoleResource>>(this.path)
.pipe( .pipe(
tap(collection => { tap((collection) => {
collection.elements.forEach((resource, id) => { collection.elements.forEach((resource, id) => {
this.id(resource.id!).cache.updateValue(resource.id!, resource); this.id(resource.id!).cache.updateValue(resource.id!, resource);
}); });
}) }),
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,12 +26,11 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { StatusResource } from "core-app/features/hal/resources/status-resource"; import { StatusResource } from 'core-app/features/hal/resources/status-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
export class APIv3StatusPaths extends CachableAPIV3Resource<StatusResource> { export class APIv3StatusPaths extends CachableAPIV3Resource<StatusResource> {
protected createCache():StateCacheService<StatusResource> { protected createCache():StateCacheService<StatusResource> {
return new StateCacheService<StatusResource>(this.states.statuses); return new StateCacheService<StatusResource>(this.states.statuses);
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,18 +26,17 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection, APIv3ResourcePath } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { Injector } from "@angular/core"; import { StatusResource } from 'core-app/features/hal/resources/status-resource';
import { StatusResource } from "core-app/features/hal/resources/status-resource"; import { APIv3StatusPaths } from 'core-app/core/apiv3/endpoints/statuses/apiv3-status-paths';
import { APIv3StatusPaths } from "core-app/core/apiv3/endpoints/statuses/apiv3-status-paths"; import { Observable } from 'rxjs';
import { Observable } from "rxjs"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { tap } from 'rxjs/operators';
import { tap } from "rxjs/operators"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service";
export class APIv3StatusesPaths extends APIv3ResourceCollection<StatusResource, APIv3StatusPaths> { export class APIv3StatusesPaths extends APIv3ResourceCollection<StatusResource, APIv3StatusPaths> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'statuses', APIv3StatusPaths); super(apiRoot, basePath, 'statuses', APIv3StatusPaths);
} }
@ -49,12 +48,11 @@ export class APIv3StatusesPaths extends APIv3ResourceCollection<StatusResource,
.halResourceService .halResourceService
.get<CollectionResource<StatusResource>>(this.path) .get<CollectionResource<StatusResource>>(this.path)
.pipe( .pipe(
tap(collection => { tap((collection) => {
collection.elements.forEach((resource, id) => { collection.elements.forEach((resource, id) => {
this.id(resource.id!).cache.updateValue(resource.id!, resource); this.id(resource.id!).cache.updateValue(resource.id!, resource);
}); });
}) }),
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,28 +26,26 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Apiv3TimeEntryPaths } from 'core-app/core/apiv3/endpoints/time-entries/apiv3-time-entry-paths';
import { Apiv3TimeEntryPaths } from "core-app/core/apiv3/endpoints/time-entries/apiv3-time-entry-paths"; import { TimeEntryResource } from 'core-app/features/hal/resources/time-entry-resource';
import { TimeEntryResource } from "core-app/features/hal/resources/time-entry-resource"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { Observable } from 'rxjs';
import { Observable } from "rxjs"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CachableAPIV3Collection } from 'core-app/core/apiv3/cache/cachable-apiv3-collection';
import { CachableAPIV3Collection } from "core-app/core/apiv3/cache/cachable-apiv3-collection";
import { MultiInputState } from "reactivestates";
import { import {
Apiv3ListParameters, Apiv3ListParameters,
Apiv3ListResourceInterface, Apiv3ListResourceInterface,
listParamsString listParamsString,
} from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { TimeEntryCacheService } from "core-app/core/apiv3/endpoints/time-entries/time-entry-cache.service"; import { TimeEntryCacheService } from 'core-app/core/apiv3/endpoints/time-entries/time-entry-cache.service';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
export class Apiv3TimeEntriesPaths export class Apiv3TimeEntriesPaths
extends CachableAPIV3Collection<TimeEntryResource, Apiv3TimeEntryPaths> extends CachableAPIV3Collection<TimeEntryResource, Apiv3TimeEntryPaths>
implements Apiv3ListResourceInterface<TimeEntryResource> { implements Apiv3ListResourceInterface<TimeEntryResource> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'time_entries', Apiv3TimeEntryPaths); super(apiRoot, basePath, 'time_entries', Apiv3TimeEntryPaths);
} }
@ -63,7 +61,7 @@ export class Apiv3TimeEntriesPaths
.halResourceService .halResourceService
.get<CollectionResource<TimeEntryResource>>(this.path + listParamsString(params)) .get<CollectionResource<TimeEntryResource>>(this.path + listParamsString(params))
.pipe( .pipe(
this.cacheResponse() this.cacheResponse(),
); );
} }
@ -76,7 +74,7 @@ export class Apiv3TimeEntriesPaths
.halResourceService .halResourceService
.post<TimeEntryResource>(this.path, payload) .post<TimeEntryResource>(this.path, payload)
.pipe( .pipe(
this.cacheResponse() this.cacheResponse(),
); );
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,16 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { TimeEntryResource } from "core-app/features/hal/resources/time-entry-resource"; import { TimeEntryResource } from 'core-app/features/hal/resources/time-entry-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { tap } from "rxjs/operators"; import { tap } from 'rxjs/operators';
import { Apiv3TimeEntriesPaths } from "core-app/core/apiv3/endpoints/time-entries/apiv3-time-entries-paths"; import { Apiv3TimeEntriesPaths } from 'core-app/core/apiv3/endpoints/time-entries/apiv3-time-entries-paths';
import { HalPayloadHelper } from "core-app/features/hal/schemas/hal-payload.helper"; import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper';
import { HalResource } from "core-app/features/hal/resources/hal-resource"; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
export class Apiv3TimeEntryPaths extends CachableAPIV3Resource<TimeEntryResource> { export class Apiv3TimeEntryPaths extends CachableAPIV3Resource<TimeEntryResource> {
// Static paths // Static paths
@ -54,7 +54,7 @@ export class Apiv3TimeEntryPaths extends CachableAPIV3Resource<TimeEntryResource
.halResourceService .halResourceService
.patch<TimeEntryResource>(this.path, this.extractPayload(payload, schema)) .patch<TimeEntryResource>(this.path, this.extractPayload(payload, schema))
.pipe( .pipe(
tap(resource => this.touch(resource)) tap((resource) => this.touch(resource)),
); );
} }
@ -66,7 +66,7 @@ export class Apiv3TimeEntryPaths extends CachableAPIV3Resource<TimeEntryResource
.halResourceService .halResourceService
.delete<TimeEntryResource>(this.path) .delete<TimeEntryResource>(this.path)
.pipe( .pipe(
tap(() => this.cache.clearSome(this.id.toString())) tap(() => this.cache.clearSome(this.id.toString())),
); );
} }
@ -84,10 +84,9 @@ export class Apiv3TimeEntryPaths extends CachableAPIV3Resource<TimeEntryResource
protected extractPayload(resource:HalResource|Object|null, schema:SchemaResource|null = null) { protected extractPayload(resource:HalResource|Object|null, schema:SchemaResource|null = null) {
if (resource instanceof HalResource && schema) { if (resource instanceof HalResource && schema) {
return HalPayloadHelper.extractPayloadFromSchema(resource, schema); return HalPayloadHelper.extractPayloadFromSchema(resource, schema);
} else if (!(resource instanceof HalResource)) { } if (!(resource instanceof HalResource)) {
return resource; return resource;
} else {
return {};
} }
return {};
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,17 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { TimeEntryResource } from "core-app/features/hal/resources/time-entry-resource"; import { TimeEntryResource } from 'core-app/features/hal/resources/time-entry-resource';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { SchemaCacheService } from "core-app/core/schemas/schema-cache.service"; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import { States } from "core-app/core/states/states.service"; import { States } from 'core-app/core/states/states.service';
import { Injector } from "@angular/core"; import { Injector } from '@angular/core';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { MultiInputState } from "reactivestates"; import { MultiInputState } from 'reactivestates';
export class TimeEntryCacheService extends StateCacheService<TimeEntryResource> { export class TimeEntryCacheService extends StateCacheService<TimeEntryResource> {
@InjectField() readonly states:States; @InjectField() readonly states:States;
@InjectField() readonly schemaCache:SchemaCacheService; @InjectField() readonly schemaCache:SchemaCacheService;
constructor(readonly injector:Injector, state:MultiInputState<TimeEntryResource>) { constructor(readonly injector:Injector, state:MultiInputState<TimeEntryResource>) {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,13 +26,12 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { TypeResource } from "core-app/features/hal/resources/type-resource"; import { TypeResource } from 'core-app/features/hal/resources/type-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { APIv3TypesPaths } from "core-app/core/apiv3/endpoints/types/apiv3-types-paths"; import { APIv3TypesPaths } from 'core-app/core/apiv3/endpoints/types/apiv3-types-paths';
export class APIv3TypePaths extends CachableAPIV3Resource<TypeResource> { export class APIv3TypePaths extends CachableAPIV3Resource<TypeResource> {
protected createCache():StateCacheService<TypeResource> { protected createCache():StateCacheService<TypeResource> {
return (this.parent as APIv3TypesPaths).cache; return (this.parent as APIv3TypesPaths).cache;
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,15 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { TypeResource } from 'core-app/features/hal/resources/type-resource';
import { TypeResource } from "core-app/features/hal/resources/type-resource"; import { APIv3TypePaths } from 'core-app/core/apiv3/endpoints/types/apiv3-type-paths';
import { APIv3TypePaths } from "core-app/core/apiv3/endpoints/types/apiv3-type-paths"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { CachableAPIV3Collection } from 'core-app/core/apiv3/cache/cachable-apiv3-collection';
import { CachableAPIV3Collection } from "core-app/core/apiv3/cache/cachable-apiv3-collection"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service";
export class APIv3TypesPaths extends CachableAPIV3Collection<TypeResource, APIv3TypePaths> { export class APIv3TypesPaths extends CachableAPIV3Collection<TypeResource, APIv3TypePaths> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'types', APIv3TypePaths); super(apiRoot, basePath, 'types', APIv3TypePaths);
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,13 +26,12 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { UserResource } from "core-app/features/hal/resources/user-resource"; import { UserResource } from 'core-app/features/hal/resources/user-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { Apiv3UserPreferencesPaths } from "core-app/core/apiv3/endpoints/users/apiv3-user-preferences-paths"; import { Apiv3UserPreferencesPaths } from 'core-app/core/apiv3/endpoints/users/apiv3-user-preferences-paths';
export class APIv3UserPaths extends CachableAPIV3Resource<UserResource> { export class APIv3UserPaths extends CachableAPIV3Resource<UserResource> {
readonly avatar = this.subResource('avatar'); readonly avatar = this.subResource('avatar');
readonly preferences = this.subResource('preferences', Apiv3UserPreferencesPaths); readonly preferences = this.subResource('preferences', Apiv3UserPreferencesPaths);

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,15 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourcePath } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourcePath } from 'core-app/core/apiv3/paths/apiv3-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { HttpClient } from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import { UserPreferencesModel } from "core-app/features/user-preferences/state/user-preferences.model"; import { UserPreferencesModel } from 'core-app/features/user-preferences/state/user-preferences.model';
export class Apiv3UserPreferencesPaths extends APIv3ResourcePath<UserPreferencesModel> { export class Apiv3UserPreferencesPaths extends APIv3ResourcePath<UserPreferencesModel> {
@InjectField() http:HttpClient; @InjectField() http:HttpClient;
/** /**
* Perform a request to the backend to load preferences * Perform a request to the backend to load preferences
*/ */
@ -43,8 +42,8 @@ export class Apiv3UserPreferencesPaths extends APIv3ResourcePath<UserPreferences
return this return this
.http .http
.get<UserPreferencesModel>( .get<UserPreferencesModel>(
this.path, this.path,
); );
} }
/** /**
@ -54,9 +53,9 @@ export class Apiv3UserPreferencesPaths extends APIv3ResourcePath<UserPreferences
return this return this
.http .http
.patch<UserPreferencesModel>( .patch<UserPreferencesModel>(
this.path, this.path,
payload, payload,
{ withCredentials: true, responseType: 'json' } { withCredentials: true, responseType: 'json' },
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,16 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { APIv3UserPaths } from "core-app/core/apiv3/endpoints/users/apiv3-user-paths"; import { APIv3UserPaths } from 'core-app/core/apiv3/endpoints/users/apiv3-user-paths';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { UserResource } from "core-app/features/hal/resources/user-resource"; import { UserResource } from 'core-app/features/hal/resources/user-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
export class Apiv3UsersPaths extends APIv3ResourceCollection<UserResource, APIv3UserPaths> { export class Apiv3UsersPaths extends APIv3ResourceCollection<UserResource, APIv3UserPaths> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'users', APIv3UserPaths); super(apiRoot, basePath, 'users', APIv3UserPaths);
} }
@ -68,8 +68,8 @@ export class Apiv3UsersPaths extends APIv3ResourceCollection<UserResource, APIv3
return this return this
.halResourceService .halResourceService
.post<UserResource>( .post<UserResource>(
this.path, this.path,
resource, resource,
); );
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,15 +26,13 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { VersionResource } from "core-app/features/hal/resources/version-resource"; import { VersionResource } from 'core-app/features/hal/resources/version-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { MultiInputState } from "reactivestates"; import { tap } from 'rxjs/operators';
import { tap } from "rxjs/operators"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service";
export class APIv3VersionPaths extends CachableAPIV3Resource<VersionResource> { export class APIv3VersionPaths extends CachableAPIV3Resource<VersionResource> {
/** /**
* Update a version resource with the given payload * Update a version resource with the given payload
* *
@ -45,11 +43,11 @@ export class APIv3VersionPaths extends CachableAPIV3Resource<VersionResource> {
return this return this
.halResourceService .halResourceService
.patch<VersionResource>( .patch<VersionResource>(
this.path, this.path,
payload payload,
) )
.pipe( .pipe(
tap(version => this.touch(version)) tap((version) => this.touch(version)),
); );
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,19 +26,19 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource, APIv3ResourceCollection } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourceCollection } from 'core-app/core/apiv3/paths/apiv3-resource';
import { VersionResource } from "core-app/features/hal/resources/version-resource"; import { VersionResource } from 'core-app/features/hal/resources/version-resource';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource"; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { Apiv3AvailableProjectsPaths } from "core-app/core/apiv3/endpoints/projects/apiv3-available-projects-paths"; import { Apiv3AvailableProjectsPaths } from 'core-app/core/apiv3/endpoints/projects/apiv3-available-projects-paths';
import { APIv3VersionPaths } from "core-app/core/apiv3/endpoints/versions/apiv3-version-paths"; import { APIv3VersionPaths } from 'core-app/core/apiv3/endpoints/versions/apiv3-version-paths';
export class APIv3VersionsPaths extends APIv3ResourceCollection<VersionResource, APIv3VersionPaths> { export class APIv3VersionsPaths extends APIv3ResourceCollection<VersionResource, APIv3VersionPaths> {
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'versions', APIv3VersionPaths); super(apiRoot, basePath, 'versions', APIv3VersionPaths);
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,16 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { WorkPackageCollectionResource } from "core-app/features/hal/resources/wp-collection-resource"; import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { APIV3WorkPackagesPaths } from "core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths"; import { APIV3WorkPackagesPaths } from 'core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths';
import { take, tap } from "rxjs/operators"; import { take, tap } from 'rxjs/operators';
import { WorkPackageCache } from "core-app/core/apiv3/endpoints/work_packages/work-package.cache"; import { WorkPackageCache } from 'core-app/core/apiv3/endpoints/work_packages/work-package.cache';
import { States } from "core-app/core/states/states.service"; import { States } from 'core-app/core/states/states.service';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
export class ApiV3WorkPackageCachedSubresource extends APIv3GettableResource<WorkPackageCollectionResource> { export class ApiV3WorkPackageCachedSubresource extends APIv3GettableResource<WorkPackageCollectionResource> {
@InjectField() private states:States; @InjectField() private states:States;
@ -45,9 +45,9 @@ export class ApiV3WorkPackageCachedSubresource extends APIv3GettableResource<Wor
.halResourceService .halResourceService
.get<WorkPackageCollectionResource>(this.path) .get<WorkPackageCollectionResource>(this.path)
.pipe( .pipe(
tap(collection => collection.schemas && this.updateSchemas(collection.schemas)), tap((collection) => collection.schemas && this.updateSchemas(collection.schemas)),
tap(collection => this.cache.updateWorkPackageList(collection.elements)), tap((collection) => this.cache.updateWorkPackageList(collection.elements)),
take(1) take(1),
); );
} }
@ -56,7 +56,7 @@ export class ApiV3WorkPackageCachedSubresource extends APIv3GettableResource<Wor
} }
private updateSchemas(schemas:CollectionResource<SchemaResource>) { private updateSchemas(schemas:CollectionResource<SchemaResource>) {
schemas.elements.forEach(schema => { schemas.elements.forEach((schema) => {
this.states.schemas.get(schema.href as string).putValue(schema); this.states.schemas.get(schema.href as string).putValue(schema);
}); });
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,14 +26,13 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource"; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { Apiv3RelationsPaths } from "core-app/core/apiv3/endpoints/relations/apiv3-relations-paths"; import { Apiv3RelationsPaths } from 'core-app/core/apiv3/endpoints/relations/apiv3-relations-paths';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { APIV3WorkPackagesPaths } from "core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths"; import { APIV3WorkPackagesPaths } from 'core-app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
export class APIV3WorkPackagePaths extends CachableAPIV3Resource<WorkPackageResource> { export class APIV3WorkPackagePaths extends CachableAPIV3Resource<WorkPackageResource> {
// /api/v3/(?:projectPath)/work_packages/(:workPackageId)/relations // /api/v3/(?:projectPath)/work_packages/(:workPackageId)/relations
public readonly relations = this.subResource('relations', Apiv3RelationsPaths); public readonly relations = this.subResource('relations', Apiv3RelationsPaths);

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,26 +26,29 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { APIV3WorkPackagePaths } from "core-app/core/apiv3/endpoints/work_packages/api-v3-work-package-paths"; import { Observable } from 'rxjs';
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource"; import { APIV3WorkPackagePaths } from 'core-app/core/apiv3/endpoints/work_packages/api-v3-work-package-paths';
import { WorkPackageCollectionResource } from "core-app/features/hal/resources/wp-collection-resource"; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { Observable } from "rxjs"; import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource';
import { APIv3WorkPackageForm } from "core-app/core/apiv3/endpoints/work_packages/apiv3-work-package-form"; import { APIv3WorkPackageForm } from 'core-app/core/apiv3/endpoints/work_packages/apiv3-work-package-form';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { CachableAPIV3Collection } from "core-app/core/apiv3/cache/cachable-apiv3-collection"; import { CachableAPIV3Collection } from 'core-app/core/apiv3/cache/cachable-apiv3-collection';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { WorkPackageCache } from "core-app/core/apiv3/endpoints/work_packages/work-package.cache"; import { WorkPackageCache } from 'core-app/core/apiv3/endpoints/work_packages/work-package.cache';
import { APIv3GettableResource } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3GettableResource } from 'core-app/core/apiv3/paths/apiv3-resource';
import { ApiV3WorkPackageCachedSubresource } from "core-app/core/apiv3/endpoints/work_packages/api-v3-work-package-cached-subresource"; import { ApiV3WorkPackageCachedSubresource } from 'core-app/core/apiv3/endpoints/work_packages/api-v3-work-package-cached-subresource';
import { ApiV3FilterBuilder, buildApiV3Filter } from "core-app/shared/helpers/api-v3/api-v3-filter-builder"; import {
ApiV3FilterBuilder,
ApiV3FilterValueType,
buildApiV3Filter,
} from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
export class APIV3WorkPackagesPaths extends CachableAPIV3Collection<WorkPackageResource, APIV3WorkPackagePaths, WorkPackageCache> { export class APIV3WorkPackagesPaths extends CachableAPIV3Collection<WorkPackageResource, APIV3WorkPackagePaths, WorkPackageCache> {
// Base path // Base path
public readonly path:string; public readonly path:string;
constructor(readonly apiRoot:APIV3Service, constructor(readonly apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'work_packages', APIV3WorkPackagePaths); super(apiRoot, basePath, 'work_packages', APIV3WorkPackagePaths);
} }
@ -75,7 +78,6 @@ export class APIV3WorkPackagesPaths extends CachableAPIV3Collection<WorkPackageR
if (results.elements) { if (results.elements) {
this.cache.updateWorkPackageList(results.elements); this.cache.updateWorkPackageList(results.elements);
} }
}); });
resolve(undefined); resolve(undefined);
@ -94,7 +96,7 @@ export class APIV3WorkPackagesPaths extends CachableAPIV3Collection<WorkPackageR
.halResourceService .halResourceService
.post<WorkPackageResource>(this.path, payload) .post<WorkPackageResource>(this.path, payload)
.pipe( .pipe(
this.cacheResponse() this.cacheResponse(),
); );
} }
@ -121,7 +123,7 @@ export class APIV3WorkPackagesPaths extends CachableAPIV3Collection<WorkPackageR
sortBy: '[["updatedAt","desc"]]', sortBy: '[["updatedAt","desc"]]',
offset: '1', offset: '1',
pageSize: '10', pageSize: '10',
...additionalParams ...additionalParams,
}; };
return this.filtered(filters, params); return this.filtered(filters, params);
@ -132,14 +134,14 @@ export class APIV3WorkPackagesPaths extends CachableAPIV3Collection<WorkPackageR
* @param ids work package IDs to filter for * @param ids work package IDs to filter for
* @param timestamp The timestamp to clip at * @param timestamp The timestamp to clip at
*/ */
public filterUpdatedSince(ids:(string|null)[], timestamp:unknown):ApiV3WorkPackageCachedSubresource { public filterUpdatedSince(ids:(string|null)[], timestamp:ApiV3FilterValueType):ApiV3WorkPackageCachedSubresource {
const filters = new ApiV3FilterBuilder() const filters = new ApiV3FilterBuilder()
.add('id', '=', ids.filter((n:string|null) => n)) // no null values .add('id', '=', (ids.filter((n) => n) as string[]))
.add('updatedAt', '<>d', [timestamp, '']); .add('updatedAt', '<>d', [timestamp, '']);
const params = { const params = {
offset: '1', offset: '1',
pageSize: '10' pageSize: '10',
}; };
return this.filtered(filters, params); return this.filtered(filters, params);
@ -156,12 +158,12 @@ export class APIV3WorkPackagesPaths extends CachableAPIV3Collection<WorkPackageR
return this return this
.halResourceService .halResourceService
.getAllPaginated<WorkPackageCollectionResource[]>( .getAllPaginated<WorkPackageCollectionResource[]>(
this.path, this.path,
ids.length, ids.length,
{ {
filters: buildApiV3Filter('id', '=', ids).toJson(), filters: buildApiV3Filter('id', '=', ids).toJson(),
} },
); );
} }
protected createCache():WorkPackageCache { protected createCache():WorkPackageCache {

@ -1,7 +1,7 @@
import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource"; import { APIv3FormResource } from 'core-app/core/apiv3/forms/apiv3-form-resource';
import { FormResource } from "core-app/features/hal/resources/form-resource"; import { FormResource } from 'core-app/features/hal/resources/form-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { HalSource } from "core-app/features/hal/resources/hal-resource"; import { HalSource } from 'core-app/features/hal/resources/hal-resource';
export class APIv3WorkPackageForm extends APIv3FormResource { export class APIv3WorkPackageForm extends APIv3FormResource {
/** /**
@ -12,10 +12,11 @@ export class APIv3WorkPackageForm extends APIv3FormResource {
* @returns A work package form resource prefilled with the provided payload. * @returns A work package form resource prefilled with the provided payload.
*/ */
public forTypePayload(payload:HalSource):Observable<FormResource> { public forTypePayload(payload:HalSource):Observable<FormResource> {
const typePayload = payload._links['type'] ? { _links: { type: payload['_links']['type'] } } : { _links: {} } ; const typePayload = payload._links.type ? { _links: { type: payload._links.type } } : { _links: {} };
return this.post(payload); return this.post(payload);
} }
/** /**
* Returns a promise to post `/api/v3/work_packages/form` where the * Returns a promise to post `/api/v3/work_packages/form` where the
* payload sent to the backend has been provided. * payload sent to the backend has been provided.
@ -27,4 +28,3 @@ export class APIv3WorkPackageForm extends APIv3FormResource {
return this.post(payload); return this.post(payload);
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -31,20 +31,20 @@ import { TestBed } from '@angular/core/testing';
import { I18nService } from 'core-app/core/i18n/i18n.service'; import { I18nService } from 'core-app/core/i18n/i18n.service';
import { NotificationsService } from 'core-app/shared/components/notifications/notifications.service'; import { NotificationsService } from 'core-app/shared/components/notifications/notifications.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource"; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service"; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import { States } from 'core-app/core/states/states.service'; import { States } from 'core-app/core/states/states.service';
import { take, takeWhile } from 'rxjs/operators'; import { take, takeWhile } from 'rxjs/operators';
import { WorkPackagesActivityService } from "core-app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service"; import { WorkPackagesActivityService } from 'core-app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service';
import { ConfigurationService } from "core-app/core/config/configuration.service"; import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { WorkPackageNotificationService } from "core-app/features/work-packages/services/notifications/work-package-notification.service"; import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service';
import { WorkPackageCache } from "core-app/core/apiv3/endpoints/work_packages/work-package.cache"; import { WorkPackageCache } from 'core-app/core/apiv3/endpoints/work_packages/work-package.cache';
import { OpenProjectFileUploadService } from "core-app/core/file-upload/op-file-upload.service"; import { OpenProjectFileUploadService } from 'core-app/core/file-upload/op-file-upload.service';
import { OpenProjectDirectFileUploadService } from "core-app/core/file-upload/op-direct-file-upload.service"; import { OpenProjectDirectFileUploadService } from 'core-app/core/file-upload/op-direct-file-upload.service';
import { TimezoneService } from "core-app/core/datetime/timezone.service"; import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { HalResourceNotificationService } from "core-app/features/hal/services/hal-resource-notification.service"; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
import { OpenprojectHalModule } from "core-app/features/hal/openproject-hal.module"; import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.module';
describe('WorkPackageCache', () => { describe('WorkPackageCache', () => {
let injector:Injector; let injector:Injector;
@ -73,7 +73,7 @@ describe('WorkPackageCache', () => {
{ provide: WorkPackageNotificationService, useValue: {} }, { provide: WorkPackageNotificationService, useValue: {} },
{ provide: OpenProjectFileUploadService, useValue: {} }, { provide: OpenProjectFileUploadService, useValue: {} },
{ provide: OpenProjectDirectFileUploadService, useValue: {} }, { provide: OpenProjectDirectFileUploadService, useValue: {} },
] ],
}); });
injector = TestBed.inject(Injector); injector = TestBed.inject(Injector);
@ -84,27 +84,26 @@ describe('WorkPackageCache', () => {
// sinon.stub(schemaCacheService, 'ensureLoaded').returns(Promise.resolve(true)); // sinon.stub(schemaCacheService, 'ensureLoaded').returns(Promise.resolve(true));
spyOn(schemaCacheService, 'ensureLoaded').and.returnValue(Promise.resolve(true as any)); spyOn(schemaCacheService, 'ensureLoaded').and.returnValue(Promise.resolve(true as any));
const workPackage1 = new WorkPackageResource( const workPackage1 = new WorkPackageResource(
injector, injector,
{ {
id: '1', id: '1',
_links: { _links: {
self: '' self: '',
} },
}, },
true, true,
(wp:WorkPackageResource) => undefined, (wp:WorkPackageResource) => undefined,
'WorkPackage' 'WorkPackage',
); );
dummyWorkPackages = [workPackage1 as any]; dummyWorkPackages = [workPackage1 as any];
}); });
it('returns a work package after the list has been initialized', function (done:any) { it('returns a work package after the list has been initialized', (done:any) => {
workPackageCache.state('1').values$() workPackageCache.state('1').values$()
.pipe( .pipe(
take(1) take(1),
) )
.subscribe((wp:WorkPackageResource) => { .subscribe((wp:WorkPackageResource) => {
expect(wp.id!).toEqual('1'); expect(wp.id!).toEqual('1');
@ -119,7 +118,7 @@ describe('WorkPackageCache', () => {
workPackageCache.state('1').values$() workPackageCache.state('1').values$()
.pipe( .pipe(
takeWhile((wp) => count < 2) takeWhile((wp) => count < 2),
) )
.subscribe((wp:WorkPackageResource) => { .subscribe((wp:WorkPackageResource) => {
expect(wp.id!).toEqual('1'); expect(wp.id!).toEqual('1');
@ -134,5 +133,4 @@ describe('WorkPackageCache', () => {
workPackageCache.updateWorkPackageList([dummyWorkPackages[0]], false); workPackageCache.updateWorkPackageList([dummyWorkPackages[0]], false);
workPackageCache.updateWorkPackageList([dummyWorkPackages[0]], false); workPackageCache.updateWorkPackageList([dummyWorkPackages[0]], false);
}); });
}); });

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,12 +27,12 @@
//++ //++
import { MultiInputState } from 'reactivestates'; import { MultiInputState } from 'reactivestates';
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource"; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { debugLog } from "core-app/shared/helpers/debug_output"; import { debugLog } from 'core-app/shared/helpers/debug_output';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { SchemaCacheService } from "core-app/core/schemas/schema-cache.service"; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
@Injectable() @Injectable()
export class WorkPackageCache extends StateCacheService<WorkPackageResource> { export class WorkPackageCache extends StateCacheService<WorkPackageResource> {
@ -53,13 +53,12 @@ export class WorkPackageCache extends StateCacheService<WorkPackageResource> {
updateWorkPackage(wp:WorkPackageResource, immediate = false):Promise<WorkPackageResource> { updateWorkPackage(wp:WorkPackageResource, immediate = false):Promise<WorkPackageResource> {
if (immediate || wp.isNew) { if (immediate || wp.isNew) {
return super.updateValue(wp.id!, wp); return super.updateValue(wp.id!, wp);
} else {
return this.updateValue(wp.id!, wp);
} }
return this.updateValue(wp.id!, wp);
} }
updateWorkPackageList(list:WorkPackageResource[], skipOnIdentical = true) { updateWorkPackageList(list:WorkPackageResource[], skipOnIdentical = true) {
for (var i of list) { for (const i of list) {
const wp = i; const wp = i;
const workPackageId = wp.id!; const workPackageId = wp.id!;
const state = this.multiState.get(workPackageId); const state = this.multiState.get(workPackageId);

@ -1,8 +1,8 @@
import { APIv3ResourcePath } from "core-app/core/apiv3/paths/apiv3-resource"; import { APIv3ResourcePath } from 'core-app/core/apiv3/paths/apiv3-resource';
import { FormResource } from "core-app/features/hal/resources/form-resource"; import { FormResource } from 'core-app/features/hal/resources/form-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { HalPayloadHelper } from "core-app/features/hal/schemas/hal-payload.helper"; import { HalPayloadHelper } from 'core-app/features/hal/schemas/hal-payload.helper';
export class APIv3FormResource<T extends FormResource = FormResource> extends APIv3ResourcePath<T> { export class APIv3FormResource<T extends FormResource = FormResource> extends APIv3ResourcePath<T> {
/** /**
@ -13,9 +13,9 @@ export class APIv3FormResource<T extends FormResource = FormResource> extends AP
return this return this
.halResourceService .halResourceService
.post<T>( .post<T>(
this.path, this.path,
this.extractPayload(request, schema) this.extractPayload(request, schema),
); );
} }
/** /**
@ -27,4 +27,4 @@ export class APIv3FormResource<T extends FormResource = FormResource> extends AP
public extractPayload(request:T|Object, schema:SchemaResource|null = null) { public extractPayload(request:T|Object, schema:SchemaResource|null = null) {
return HalPayloadHelper.extractPayload(request, schema); return HalPayloadHelper.extractPayload(request, schema);
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -28,14 +28,14 @@
import { OPSharedModule } from 'core-app/shared/shared.module'; import { OPSharedModule } from 'core-app/shared/shared.module';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { OpenprojectHalModule } from "core-app/features/hal/openproject-hal.module"; import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.module';
@NgModule({ @NgModule({
imports: [ imports: [
// Commons // Commons
OPSharedModule, OPSharedModule,
OpenprojectHalModule, OpenprojectHalModule,
] ],
}) })
export class OpenprojectApiV3Module { export class OpenprojectApiV3Module {
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,9 +26,9 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { ApiV3FilterBuilder, FilterOperator } from "core-app/shared/helpers/api-v3/api-v3-filter-builder"; import { ApiV3FilterBuilder, FilterOperator } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
export type ApiV3ListFilter = [string, FilterOperator, boolean|string[]]; export type ApiV3ListFilter = [string, FilterOperator, boolean|string[]];

@ -1,25 +1,25 @@
import { Constructor } from "@angular/cdk/table"; import { Constructor } from '@angular/cdk/table';
import { SimpleResource, SimpleResourceCollection } from "core-app/core/apiv3/paths/path-resources"; import { SimpleResource, SimpleResourceCollection } from 'core-app/core/apiv3/paths/path-resources';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service"; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { ApiV3FilterBuilder } from "core-app/shared/helpers/api-v3/api-v3-filter-builder"; import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
import { HalResource } from "core-app/features/hal/resources/hal-resource"; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
export class APIv3ResourcePath<T = HalResource> extends SimpleResource { export class APIv3ResourcePath<T = HalResource> extends SimpleResource {
readonly injector = this.apiRoot.injector; readonly injector = this.apiRoot.injector;
@InjectField() halResourceService:HalResourceService; @InjectField() halResourceService:HalResourceService;
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
readonly basePath:string, readonly basePath:string,
readonly id:string|number, readonly id:string|number,
protected parent?:APIv3ResourcePath|APIv3ResourceCollection<any, any>) { protected parent?:APIv3ResourcePath|APIv3ResourceCollection<any, any>) {
super(basePath, id); super(basePath, id);
} }
/** /**
* Build a singular resource from the current segment * Build a singular resource from the current segment
* *
@ -30,7 +30,6 @@ export class APIv3ResourcePath<T = HalResource> extends SimpleResource {
} }
} }
export class APIv3GettableResource<T = HalResource> extends APIv3ResourcePath<T> { export class APIv3GettableResource<T = HalResource> extends APIv3ResourcePath<T> {
/** /**
* Perform a request to the HalResourceService with the current path * Perform a request to the HalResourceService with the current path
@ -44,12 +43,13 @@ export class APIv3GettableResource<T = HalResource> extends APIv3ResourcePath<T>
export class APIv3ResourceCollection<V, T extends APIv3GettableResource<V>> extends SimpleResourceCollection { export class APIv3ResourceCollection<V, T extends APIv3GettableResource<V>> extends SimpleResourceCollection {
readonly injector = this.apiRoot.injector; readonly injector = this.apiRoot.injector;
@InjectField() halResourceService:HalResourceService; @InjectField() halResourceService:HalResourceService;
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string, protected basePath:string,
segment:string, segment:string,
protected resource?:Constructor<T>) { protected resource?:Constructor<T>) {
super(basePath, segment, resource); super(basePath, segment, resource);
} }
@ -69,13 +69,11 @@ export class APIv3ResourceCollection<V, T extends APIv3GettableResource<V>> exte
return new (this.resource || APIv3GettableResource)(this.apiRoot, this.path, id, this) as T; return new (this.resource || APIv3GettableResource)(this.apiRoot, this.path, id, this) as T;
} }
public withOptionalId(id?:string|number|null):this|T { public withOptionalId(id?:string|number|null):this|T {
if (_.isNil(id)) { if (_.isNil(id)) {
return this; return this;
} else {
return this.id(id);
} }
return this.id(id);
} }
/** /**
@ -100,7 +98,7 @@ export class APIv3ResourceCollection<V, T extends APIv3GettableResource<V>> exte
* @param params additional URL params to append * @param params additional URL params to append
*/ */
public filtered<R = APIv3GettableResource<CollectionResource<V>>>(filters:ApiV3FilterBuilder, params:{ [key:string]:string } = {}, resourceClass?:Constructor<R>):R { public filtered<R = APIv3GettableResource<CollectionResource<V>>>(filters:ApiV3FilterBuilder, params:{ [key:string]:string } = {}, resourceClass?:Constructor<R>):R {
return this.subResource<R>('?' + filters.toParams(params), resourceClass) as R; return this.subResource<R>(`?${filters.toParams(params)}`, resourceClass);
} }
/** /**
@ -111,4 +109,4 @@ export class APIv3ResourceCollection<V, T extends APIv3GettableResource<V>> exte
protected subResource<R = APIv3GettableResource<HalResource>>(segment:string, cls:Constructor<R> = APIv3GettableResource as any):R { protected subResource<R = APIv3GettableResource<HalResource>>(segment:string, cls:Constructor<R> = APIv3GettableResource as any):R {
return new cls(this.apiRoot, this.path, segment, this); return new cls(this.apiRoot, this.path, segment, this);
} }
} }

@ -1,4 +1,4 @@
import { Constructor } from "@angular/cdk/table"; import { Constructor } from '@angular/cdk/table';
/** /**
* Simple resource collection to construct paths for RESTful resources. * Simple resource collection to construct paths for RESTful resources.
@ -28,9 +28,8 @@ export class SimpleResourceCollection<T = SimpleResource> {
public withOptionalId(id?:string|number):this|T { public withOptionalId(id?:string|number):this|T {
if (_.isNil(id)) { if (_.isNil(id)) {
return this; return this;
} else {
return this.id(id);
} }
return this.id(id);
} }
public toString():string { public toString():string {

@ -7,4 +7,4 @@ export interface IHALCollection<T> {
_embedded:{ _embedded:{
elements:T[]; elements:T[];
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,17 +26,15 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Board } from "core-app/features/boards/board/board"; import { Board } from 'core-app/features/boards/board/board';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { map, switchMap, tap } from "rxjs/operators"; import { map, switchMap, tap } from 'rxjs/operators';
import { SchemaResource } from "core-app/features/hal/resources/schema-resource"; import { SchemaResource } from 'core-app/features/hal/resources/schema-resource';
import { CachableAPIV3Resource } from "core-app/core/apiv3/cache/cachable-apiv3-resource"; import { CachableAPIV3Resource } from 'core-app/core/apiv3/cache/cachable-apiv3-resource';
import { MultiInputState } from "reactivestates"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service"; import { Apiv3BoardsPaths } from 'core-app/core/apiv3/virtual/apiv3-boards-paths';
import { Apiv3BoardsPaths } from "core-app/core/apiv3/virtual/apiv3-boards-paths";
export class APIv3BoardPath extends CachableAPIV3Resource<Board> { export class APIv3BoardPath extends CachableAPIV3Resource<Board> {
/** /**
* Perform a request to the HalResourceService with the current path * Perform a request to the HalResourceService with the current path
*/ */
@ -47,13 +45,13 @@ export class APIv3BoardPath extends CachableAPIV3Resource<Board> {
.id(this.id) .id(this.id)
.get() .get()
.pipe( .pipe(
map(grid => { map((grid) => {
const newBoard = new Board(grid); const newBoard = new Board(grid);
newBoard.sortWidgets(); newBoard.sortWidgets();
return newBoard; return newBoard;
}) }),
); );
} }
@ -68,14 +66,13 @@ export class APIv3BoardPath extends CachableAPIV3Resource<Board> {
.apiRoot .apiRoot
.grids .grids
.id(board.grid) .id(board.grid)
.patch(board.grid, schema) .patch(board.grid, schema)),
), map((grid) => {
map(grid => {
board.grid = grid; board.grid = grid;
board.sortWidgets(); board.sortWidgets();
return board; return board;
}), }),
this.cacheResponse() this.cacheResponse(),
); );
} }
@ -86,7 +83,7 @@ export class APIv3BoardPath extends CachableAPIV3Resource<Board> {
.id(this.id) .id(this.id)
.delete() .delete()
.pipe( .pipe(
tap(() => this.cache.clearSome(this.id.toString())) tap(() => this.cache.clearSome(this.id.toString())),
); );
} }
@ -98,7 +95,7 @@ export class APIv3BoardPath extends CachableAPIV3Resource<Board> {
.form .form
.post({}) .post({})
.pipe( .pipe(
map(form => form.schema) map((form) => form.schema),
); );
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,30 +26,27 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Constructor } from "@angular/cdk/table"; import { GridResource } from 'core-app/features/hal/resources/grid-resource';
import { GridResource } from "core-app/features/hal/resources/grid-resource"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { Observable } from 'rxjs';
import { Observable } from "rxjs"; import { Apiv3ListParameters, listParamsString } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { Apiv3ListParameters, listParamsString } from "core-app/core/apiv3/paths/apiv3-list-resource.interface"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { Board, BoardType } from 'core-app/features/boards/board/board';
import { Board, BoardType } from "core-app/features/boards/board/board"; import { map, switchMap, tap } from 'rxjs/operators';
import { map, switchMap, tap } from "rxjs/operators"; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { InjectField } from "core-app/shared/helpers/angular/inject-field.decorator"; import { AuthorisationService } from 'core-app/core/model-auth/model-auth.service';
import { CurrentProjectService } from "core-app/core/current-project/current-project.service"; import { CachableAPIV3Collection } from 'core-app/core/apiv3/cache/cachable-apiv3-collection';
import { AuthorisationService } from "core-app/core/model-auth/model-auth.service"; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { CachableAPIV3Collection } from "core-app/core/apiv3/cache/cachable-apiv3-collection"; import { APIv3BoardPath } from 'core-app/core/apiv3/virtual/apiv3-board-path';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; import { StateCacheService } from 'core-app/core/apiv3/cache/state-cache.service';
import { MultiInputState } from "reactivestates";
import { APIv3BoardPath } from "core-app/core/apiv3/virtual/apiv3-board-path";
import { StateCacheService } from "core-app/core/apiv3/cache/state-cache.service";
export class Apiv3BoardsPaths extends CachableAPIV3Collection<Board, APIv3BoardPath> { export class Apiv3BoardsPaths extends CachableAPIV3Collection<Board, APIv3BoardPath> {
@InjectField() private authorisationService:AuthorisationService; @InjectField() private authorisationService:AuthorisationService;
@InjectField() private PathHelper:PathHelperService; @InjectField() private PathHelper:PathHelperService;
constructor(protected apiRoot:APIV3Service, constructor(protected apiRoot:APIV3Service,
protected basePath:string) { protected basePath:string) {
super(apiRoot, basePath, 'grids', APIv3BoardPath); super(apiRoot, basePath, 'grids', APIv3BoardPath);
} }
@ -62,16 +59,14 @@ export class Apiv3BoardsPaths extends CachableAPIV3Collection<Board, APIv3BoardP
.halResourceService .halResourceService
.get<CollectionResource<GridResource>>(this.path + listParamsString(params)) .get<CollectionResource<GridResource>>(this.path + listParamsString(params))
.pipe( .pipe(
tap(collection => this.authorisationService.initModelAuth('boards', collection.$links)), tap((collection) => this.authorisationService.initModelAuth('boards', collection.$links)),
map(collection => map((collection) => collection.elements.map((grid) => {
collection.elements.map(grid => { const board = new Board(grid);
const board = new Board(grid); board.sortWidgets();
board.sortWidgets(); this.touch(board);
this.touch(board);
return board; return board;
}) })),
)
); );
} }
@ -96,7 +91,7 @@ export class Apiv3BoardsPaths extends CachableAPIV3Collection<Board, APIv3BoardP
return this return this
.createGrid(type, name, scope, actionAttribute) .createGrid(type, name, scope, actionAttribute)
.pipe( .pipe(
map(grid => new Board(grid)) map((grid) => new Board(grid)),
); );
} }
@ -115,9 +110,9 @@ export class Apiv3BoardsPaths extends CachableAPIV3Collection<Board, APIv3BoardP
} }
private createGrid(type:BoardType, name:string, scope:string, actionAttribute?:string):Observable<GridResource> { private createGrid(type:BoardType, name:string, scope:string, actionAttribute?:string):Observable<GridResource> {
const payload:any = _.set({ name: name }, '_links.scope.href', scope); const payload:any = _.set({ name }, '_links.scope.href', scope);
payload.options = { payload.options = {
type: type, type,
}; };
if (actionAttribute) { if (actionAttribute) {
@ -130,12 +125,10 @@ export class Apiv3BoardsPaths extends CachableAPIV3Collection<Board, APIv3BoardP
.form .form
.post(payload) .post(payload)
.pipe( .pipe(
switchMap((form) => { switchMap((form) => this
return this .apiRoot
.apiRoot .grids
.grids .post(form.payload.$source)),
.post(form.payload.$source);
})
); );
} }
} }

@ -300,7 +300,7 @@ RB.Model = (function ($) {
}, },
getType: function () { getType: function () {
throw "Child objects must override getType()"; throw new Error("Child objects must override getType()");
}, },
handleClick: function (e) { handleClick: function (e) {
@ -346,7 +346,7 @@ RB.Model = (function ($) {
}, },
markIfClosed: function () { markIfClosed: function () {
throw "Child objects must override markIfClosed()"; throw new Error("Child objects must override markIfClosed()");
}, },
markSaving: function () { markSaving: function () {
@ -393,7 +393,7 @@ RB.Model = (function ($) {
}, },
saveDirectives: function () { saveDirectives: function () {
throw "Child object must implement saveDirectives()"; throw new Error("Child object must implement saveDirectives()");
}, },
saveEdits: function () { saveEdits: function () {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,90 +27,90 @@
//++ //++
// Loaded dynamically when path matches // Loaded dynamically when path matches
(function ($, undefined) { (function ($) {
var global_roles = { const globalRoles = {
init: function () { init() {
if (global_roles.script_applicable()) { if (globalRoles.script_applicable()) {
global_roles.toggle_forms_on_click(); globalRoles.toggle_forms_on_click();
global_roles.activation_and_visibility_based_on_checked($('#global_role')); globalRoles.activation_and_visibility_based_on_checked($('#global_role'));
} }
}, },
toggle_forms_on_click: function () { toggle_forms_on_click() {
$('#global_role').on("click", global_roles.toggle_forms); $('#global_role').on('click', this.toggle_forms);
}, },
toggle_forms: function (event:any) { toggle_forms() {
global_roles.activation_and_visibility_based_on_checked(this); globalRoles.activation_and_visibility_based_on_checked(this);
}, },
activation_and_visibility_based_on_checked: function (element:any) { activation_and_visibility_based_on_checked(element:any) {
if ($(element).prop("checked")) { if ($(element).prop('checked')) {
global_roles.show_global_forms(); globalRoles.show_global_forms();
global_roles.hide_member_forms(); globalRoles.hide_member_forms();
global_roles.enable_global_forms(); globalRoles.enable_global_forms();
global_roles.disable_member_forms(); globalRoles.disable_member_forms();
} else { } else {
global_roles.show_member_forms(); globalRoles.show_member_forms();
global_roles.hide_global_forms(); globalRoles.hide_global_forms();
global_roles.disable_global_forms(); globalRoles.disable_global_forms();
global_roles.enable_member_forms(); globalRoles.enable_member_forms();
} }
}, },
show_global_forms: function () { show_global_forms() {
$('#global_permissions').show(); $('#global_permissions').show();
}, },
show_member_forms: function () { show_member_forms() {
$('#member_attributes').show(); $('#member_attributes').show();
$('#member_permissions').show(); $('#member_permissions').show();
}, },
hide_global_forms: function () { hide_global_forms() {
$('#global_permissions').hide(); $('#global_permissions').hide();
}, },
hide_member_forms: function () { hide_member_forms() {
$('#member_attributes').hide(); $('#member_attributes').hide();
$('#member_permissions').hide(); $('#member_permissions').hide();
}, },
enable_global_forms: function () { enable_global_forms() {
$('#global_attributes input, #global_attributes input, #global_permissions input').each(function (ix, el) { $('#global_attributes input, #global_attributes input, #global_permissions input').each((ix, el) => {
global_roles.enable_element(el); globalRoles.enable_element(el);
}); });
}, },
enable_member_forms: function () { enable_member_forms() {
$('#member_attributes input, #member_attributes input, #member_permissions input').each(function (ix, el) { $('#member_attributes input, #member_attributes input, #member_permissions input').each((ix, el) => {
global_roles.enable_element(el); globalRoles.enable_element(el);
}); });
}, },
enable_element: function (element:any) { enable_element(element:any) {
$(element).prop('disabled', false); $(element).prop('disabled', false);
}, },
disable_global_forms: function () { disable_global_forms() {
$('#global_attributes input, #global_attributes input, #global_permissions input').each(function (ix, el) { $('#global_attributes input, #global_attributes input, #global_permissions input').each((ix, el) => {
global_roles.disable_element(el); globalRoles.disable_element(el);
}); });
}, },
disable_member_forms: function () { disable_member_forms() {
$('#member_attributes input, #member_attributes input, #member_permissions input').each(function (ix, el) { $('#member_attributes input, #member_attributes input, #member_permissions input').each((ix, el) => {
global_roles.disable_element(el); globalRoles.disable_element(el);
}); });
}, },
disable_element: function (element:any) { disable_element(element:any) {
$(element).prop('disabled', true); $(element).prop('disabled', true);
}, },
script_applicable: function () { script_applicable() {
return $('body.controller-roles.action-new, body.controller-roles.action-create').length === 1; return $('body.controller-roles.action-new, body.controller-roles.action-create').length === 1;
} },
}; };
$(document).ready(global_roles.init); $(document).ready(globalRoles.init);
}(jQuery)); }(jQuery));

@ -1,63 +1,64 @@
import 'core-vendor/qrcode-min'; import 'core-vendor/qrcode-min';
declare var QRCode:any; declare let QRCode:any;
jQuery(function ($) { jQuery(($) => {
$('#submit_otp').submit(function() { $('#submit_otp').submit(() => {
$('.ajax_form').find("input, radio").attr('disabled', 'disabled'); $('.ajax_form').find('input, radio').attr('disabled', 'disabled');
}); });
$('#toggle_resend_form').click(function(){ $('#toggle_resend_form').click(() => {
$('#resend_otp_container').toggle(); $('#resend_otp_container').toggle();
return false; return false;
}); });
$('.qr-code-element').each(function() { $('.qr-code-element').each(function () {
var el = $(this); const el = $(this);
new QRCode( new QRCode(
el[0], el[0],
{ {
text: el.data('value'), text: el.data('value'),
width: 220, width: 220,
height: 220 height: 220,
} },
); );
}); });
$('.ajax_form').submit(function() { $('.ajax_form').submit(function () {
$('#submit_otp').find("input").attr('disabled', 'disabled'); $('#submit_otp').find('input').attr('disabled', 'disabled');
var form = $(this), const form = $(this);
submit_button = form.find("input[type=submit]"); const submit_button = form.find('input[type=submit]');
$.ajax({ url: form.attr('action'), $.ajax({
url: form.attr('action'),
type: 'post', type: 'post',
data: form.serialize(), data: form.serialize(),
beforeSend: function() { beforeSend() {
submit_button.attr('disabled', 'disabled'); submit_button.attr('disabled', 'disabled');
submit_button.toggleClass('submitting'); submit_button.toggleClass('submitting');
$('.flash.notice').toggle(); $('.flash.notice').toggle();
}, },
complete: function(response) { complete(response) {
submit_button.removeAttr('disabled'); submit_button.removeAttr('disabled');
$('#submit_otp').find("input").removeAttr('disabled'); $('#submit_otp').find('input').removeAttr('disabled');
$('.flash.notice a').html(response.responseText); $('.flash.notice a').html(response.responseText);
$('form#resend_otp, #toggle_resend_form, .flash.notice').toggle(); $('form#resend_otp, #toggle_resend_form, .flash.notice').toggle();
submit_button.toggleClass('submitting'); submit_button.toggleClass('submitting');
} },
}); });
return false; return false;
}); });
$('#print_2fa_backup_codes').click(function() { $('#print_2fa_backup_codes').click(() => {
window.print(); window.print();
}); });
if ($('#download_2fa_backup_codes').length) { if ($('#download_2fa_backup_codes').length) {
var text = ''; let text = '';
$('.two-factor-authentication--backup-codes li').each(function() { $('.two-factor-authentication--backup-codes li').each(function () {
text += this.textContent + "\n"; text += `${this.textContent}\n`;
}); });
var element = $('#download_2fa_backup_codes'); const element = $('#download_2fa_backup_codes');
element.attr('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); element.attr('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`);
element.attr('download', 'backup-codes.txt'); element.attr('download', 'backup-codes.txt');
} }
}); });

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,13 +27,13 @@
//++ //++
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { OpenprojectModalModule } from "core-app/shared/components/modal/modal.module"; import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { OpModalWrapperAugmentService } from "core-app/shared/components/modal/modal-wrapper-augment.service"; import { OpModalWrapperAugmentService } from 'core-app/shared/components/modal/modal-wrapper-augment.service';
import { PathScriptAugmentService } from "core-app/core/augmenting/services/path-script.augment.service"; import { PathScriptAugmentService } from 'core-app/core/augmenting/services/path-script.augment.service';
@NgModule({ @NgModule({
imports: [ OpenprojectModalModule ], imports: [OpenprojectModalModule],
providers: [ PathScriptAugmentService ], providers: [PathScriptAugmentService],
}) })
export class OpenprojectAugmentingModule { export class OpenprojectAugmentingModule {
constructor(modalWrapper:OpModalWrapperAugmentService, constructor(modalWrapper:OpModalWrapperAugmentService,
@ -43,4 +43,3 @@ export class OpenprojectAugmentingModule {
pathScript.loadRequiredScripts(); pathScript.loadRequiredScripts();
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,14 +26,12 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Inject, Injectable } from '@angular/core';
import { Inject, Injectable } from "@angular/core"; import { DOCUMENT } from '@angular/common';
import { DOCUMENT } from "@angular/common"; import { debugLog } from 'core-app/shared/helpers/debug_output';
import { debugLog } from "core-app/shared/helpers/debug_output";
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class PathScriptAugmentService { export class PathScriptAugmentService {
constructor(@Inject(DOCUMENT) protected documentElement:Document) { constructor(@Inject(DOCUMENT) protected documentElement:Document) {
} }
@ -49,8 +47,8 @@ export class PathScriptAugmentService {
const matches = this.documentElement.querySelectorAll<HTMLMetaElement>('meta[name="required_script"]'); const matches = this.documentElement.querySelectorAll<HTMLMetaElement>('meta[name="required_script"]');
for (let i = 0; i < matches.length; ++i) { for (let i = 0; i < matches.length; ++i) {
const name = matches[i].content; const name = matches[i].content;
debugLog("Loading required script " + name); debugLog(`Loading required script ${name}`);
import('../dynamic-scripts/' + name); import(`../dynamic-scripts/${name}`);
} }
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,29 +26,29 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import {Injectable} from "@angular/core"; import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import {HalResource} from "core-app/features/hal/resources/hal-resource"; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import {Observable} from "rxjs"; import { Observable } from 'rxjs';
import {HalResourceService} from "core-app/features/hal/services/hal-resource.service"; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class OpenProjectBackupService { export class OpenProjectBackupService {
constructor(protected http:HttpClient, constructor(protected http:HttpClient,
protected halResource:HalResourceService) { protected halResource:HalResourceService) {
} }
public triggerBackup(backupToken:string, includeAttachments:boolean=true):Observable<HalResource> { public triggerBackup(backupToken:string, includeAttachments = true):Observable<HalResource> {
return this return this
.http .http
.request<HalResource>( .request<HalResource>(
"post", 'post',
"/api/v3/backups", '/api/v3/backups',
{ {
body: { backupToken: backupToken, attachments: includeAttachments }, body: { backupToken, attachments: includeAttachments },
withCredentials: true, withCredentials: true,
responseType: "json" as any responseType: 'json' as any,
} },
); );
} }
} }

@ -1,10 +1,9 @@
import { Inject, Injectable } from "@angular/core"; import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from "@angular/common"; import { DOCUMENT } from '@angular/common';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class BrowserDetector { export class BrowserDetector {
constructor(@Inject(DOCUMENT) private documentElement:Document) {
constructor (@Inject(DOCUMENT) private documentElement:Document) {
} }
/** /**
@ -25,5 +24,4 @@ export class BrowserDetector {
private hasBodyClass(name:string):boolean { private hasBodyClass(name:string):boolean {
return this.documentElement.body.classList.contains(name); return this.documentElement.body.classList.contains(name);
} }
} }

@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class DeviceService { export class DeviceService {
public mobileWidthTreshold = 680; public mobileWidthTreshold = 680;
public get isMobile():boolean { public get isMobile():boolean {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,9 +27,9 @@
//++ //++
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service'; import { I18nService } from 'core-app/core/i18n/i18n.service';
import { ConfigurationResource } from "core-app/features/hal/resources/configuration-resource"; import { ConfigurationResource } from 'core-app/features/hal/resources/configuration-resource';
import * as moment from "moment"; import * as moment from 'moment';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ConfigurationService { export class ConfigurationService {
@ -37,10 +37,11 @@ export class ConfigurationService {
// TODO: this currently saves the request between page reloads, // TODO: this currently saves the request between page reloads,
// but could easily be stored in localStorage // but could easily be stored in localStorage
private configuration:ConfigurationResource; private configuration:ConfigurationResource;
public initialized:Promise<boolean>; public initialized:Promise<boolean>;
public constructor(readonly I18n:I18nService, public constructor(readonly I18n:I18nService,
readonly apiV3Service:APIV3Service) { readonly apiV3Service:APIV3Service) {
this.initialized = this.loadConfiguration().then(() => true).catch(() => false); this.initialized = this.loadConfiguration().then(() => true).catch(() => false);
} }
@ -103,9 +104,8 @@ export class ConfigurationService {
public startOfWeek() { public startOfWeek() {
if (this.startOfWeekPresent()) { if (this.startOfWeekPresent()) {
return this.systemPreference('startOfWeek'); return this.systemPreference('startOfWeek');
} else {
return moment.localeData(I18n.locale).firstDayOfWeek();
} }
return moment.localeData(I18n.locale).firstDayOfWeek();
} }
private loadConfiguration() { private loadConfiguration() {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,21 +26,19 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
/*jshint expr: true*/ /* jshint expr: true */
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { CurrentProjectService } from './current-project.service'; import { CurrentProjectService } from './current-project.service';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
describe('currentProject service', function() { describe('currentProject service', () => {
let element:JQuery; let element:JQuery;
let currentProject:CurrentProjectService; let currentProject:CurrentProjectService;
const apiV3Stub:any = { const apiV3Stub:any = {
projects: { projects: {
id: (id:string) => { id: (id:string) => ({ toString: () => `/api/v3/projects/${id}` }),
return { toString: () => '/api/v3/projects/' + id }; },
}
}
}; };
beforeEach(() => { beforeEach(() => {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,16 +26,16 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CurrentProjectService { export class CurrentProjectService {
private current:{ id:string, identifier:string, name:string }; private current:{ id:string, identifier:string, name:string };
constructor(private PathHelper:PathHelperService, constructor(private PathHelper:PathHelperService,
private apiV3Service:APIV3Service) { private apiV3Service:APIV3Service) {
this.detect(); this.detect();
} }
@ -88,7 +88,7 @@ export class CurrentProjectService {
this.current = { this.current = {
id: element.dataset.projectId!, id: element.dataset.projectId!,
name: element.dataset.projectName!, name: element.dataset.projectName!,
identifier: element.dataset.projectIdentifier! identifier: element.dataset.projectIdentifier!,
}; };
} }
} }

@ -1,10 +1,8 @@
import { Injector, NgModule } from "@angular/core"; import { Injector, NgModule } from '@angular/core';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource";
import { CapabilityResource } from "core-app/features/hal/resources/capability-resource";
import { CurrentUserService } from "./current-user.service"; import { CurrentUserService } from './current-user.service';
import { CurrentUserStore } from "./current-user.store"; import { CurrentUserStore } from './current-user.store';
import { CurrentUserQuery } from "./current-user.query"; import { CurrentUserQuery } from './current-user.query';
export function bootstrapModule(injector:Injector) { export function bootstrapModule(injector:Injector) {
const currentUserService = injector.get(CurrentUserService); const currentUserService = injector.get(CurrentUserService);

@ -1,21 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { filterNilValue, Query } from '@datorama/akita'; import { filterNilValue, Query } from '@datorama/akita';
import { Observable, combineLatest } from 'rxjs'; import { CurrentUserState, CurrentUserStore } from './current-user.store';
import { map } from 'rxjs/operators';
import {
CurrentUserStore,
CurrentUserState,
CurrentUser,
} from './current-user.store';
import { CapabilityResource } from "core-app/features/hal/resources/capability-resource";
@Injectable() @Injectable()
export class CurrentUserQuery extends Query<CurrentUserState> { export class CurrentUserQuery extends Query<CurrentUserState> {
constructor(protected store: CurrentUserStore) { constructor(protected store:CurrentUserStore) {
super(store); super(store);
} }
isLoggedIn$ = this.select(state => !!state.id); isLoggedIn$ = this.select((state) => !!state.id);
user$ = this.select(({ id, name, mail }) => ({ id, name, mail })); user$ = this.select(({ id, name, mail }) => ({ id, name, mail }));
capabilities$ = this.select('capabilities').pipe(filterNilValue()); capabilities$ = this.select('capabilities').pipe(filterNilValue());
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,17 +26,16 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
/*jshint expr: true*/ /* jshint expr: true */
import { TestBed, getTestBed } from '@angular/core/testing'; import { getTestBed, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { States } from "core-app/core/states/states.service"; import { States } from 'core-app/core/states/states.service';
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service"; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { CurrentUserService } from './current-user.service'; import { CurrentUserService } from './current-user.service';
import { CurrentUserStore } from "./current-user.store"; import { CurrentUser, CurrentUserStore } from './current-user.store';
import { CurrentUserQuery } from "./current-user.query"; import { CurrentUserQuery } from './current-user.query';
import { CurrentUser } from './current-user.store';
const globalCapability = { const globalCapability = {
_type: 'Capability', _type: 'Capability',
@ -122,7 +121,7 @@ const projectCapabilityp53Update = {
}, },
}; };
describe('CurrentUserService', function () { describe('CurrentUserService', () => {
let injector:TestBed; let injector:TestBed;
let currentUserService:CurrentUserService; let currentUserService:CurrentUserService;
let httpMock:HttpTestingController; let httpMock:HttpTestingController;
@ -171,8 +170,7 @@ describe('CurrentUserService', function () {
}, },
}); });
}); });
} };
afterEach(() => { afterEach(() => {
httpMock.verify(); httpMock.verify();

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,30 +26,34 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { of, forkJoin } from 'rxjs'; import { forkJoin, of } from 'rxjs';
import { take, map, mergeMap, distinctUntilChanged, tap } from 'rxjs/operators'; import {
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; distinctUntilChanged, map, mergeMap, take,
import { CapabilityResource } from "core-app/features/hal/resources/capability-resource"; } from 'rxjs/operators';
import { CollectionResource } from "core-app/features/hal/resources/collection-resource"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { CurrentUserStore, CurrentUser } from "./current-user.store"; import { CapabilityResource } from 'core-app/features/hal/resources/capability-resource';
import { CurrentUserQuery } from "./current-user.query"; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import { FilterOperator } from "core-app/shared/helpers/api-v3/api-v3-filter-builder"; import { FilterOperator } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
import { CurrentUser, CurrentUserStore } from './current-user.store';
import { CurrentUserQuery } from './current-user.query';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CurrentUserService { export class CurrentUserService {
private PAGE_FETCH_SIZE = 1000; private PAGE_FETCH_SIZE = 1000;
constructor( constructor(
private apiV3Service: APIV3Service, private apiV3Service:APIV3Service,
private currentUserStore: CurrentUserStore, private currentUserStore:CurrentUserStore,
private currentUserQuery: CurrentUserQuery, private currentUserQuery:CurrentUserQuery,
) { ) {
this.setupLegacyDataListeners(); this.setupLegacyDataListeners();
} }
public capabilities$ = this.currentUserQuery.capabilities$; public capabilities$ = this.currentUserQuery.capabilities$;
public isLoggedIn$ = this.currentUserQuery.isLoggedIn$; public isLoggedIn$ = this.currentUserQuery.isLoggedIn$;
public user$ = this.currentUserQuery.user$; public user$ = this.currentUserQuery.user$;
/** /**
@ -57,8 +61,8 @@ export class CurrentUserService {
* *
* This refetches the global and current project capabilities * This refetches the global and current project capabilities
*/ */
public setUser(user: CurrentUser) { public setUser(user:CurrentUser) {
this.currentUserStore.update(state => ({ this.currentUserStore.update((state) => ({
...state, ...state,
...user, ...user,
})); }));
@ -72,7 +76,7 @@ export class CurrentUserService {
public fetchCapabilities(contexts:string[] = []) { public fetchCapabilities(contexts:string[] = []) {
this.user$.pipe(take(1)).subscribe((user) => { this.user$.pipe(take(1)).subscribe((user) => {
if (!user.id) { if (!user.id) {
this.currentUserStore.update(state => ({ this.currentUserStore.update((state) => ({
...state, ...state,
capabilities: [], capabilities: [],
})); }));
@ -80,56 +84,56 @@ export class CurrentUserService {
return; return;
} }
const filters: [string, FilterOperator, string[]][] = [ ['principal', '=', [user.id]] ]; const filters:[string, FilterOperator, string[]][] = [['principal', '=', [user.id]]];
if (contexts.length) { if (contexts.length) {
filters.push([ 'context', '=', contexts.map(context => context === 'global' ? 'g' : `p${context}`) ]); filters.push(['context', '=', contexts.map((context) => (context === 'global' ? 'g' : `p${context}`))]);
} }
this.apiV3Service.capabilities.list({ this.apiV3Service.capabilities.list({
pageSize: this.PAGE_FETCH_SIZE, pageSize: this.PAGE_FETCH_SIZE,
filters, filters,
}) })
.pipe( .pipe(
mergeMap((data: CollectionResource<CapabilityResource>) => { mergeMap((data:CollectionResource<CapabilityResource>) => {
// The data we've loaded might not contain all capabilities. Some responses might have thousands of // The data we've loaded might not contain all capabilities. Some responses might have thousands of
// capabilites, and our page size is restricted. If this is the case, we branch out and sent out parallel // capabilites, and our page size is restricted. If this is the case, we branch out and sent out parallel
// requests for each of the other pages. // requests for each of the other pages.
if (data.total > this.PAGE_FETCH_SIZE) { if (data.total > this.PAGE_FETCH_SIZE) {
const remaining = data.total - this.PAGE_FETCH_SIZE; const remaining = data.total - this.PAGE_FETCH_SIZE;
const pagesRemaining = Math.ceil(remaining / this.PAGE_FETCH_SIZE); const pagesRemaining = Math.ceil(remaining / this.PAGE_FETCH_SIZE);
const calls = (new Array(pagesRemaining)) const calls = (new Array(pagesRemaining))
.fill(null) .fill(null)
.map((_, i) => this.apiV3Service.capabilities.list({ .map((_, i) => this.apiV3Service.capabilities.list({
pageSize: this.PAGE_FETCH_SIZE, pageSize: this.PAGE_FETCH_SIZE,
offset: i + 2, // Page offsets are 1-indexed, and we already fetched the first page offset: i + 2, // Page offsets are 1-indexed, and we already fetched the first page
filters, filters,
})); }));
// Branch out and fetch all remaining pages in parallel. // Branch out and fetch all remaining pages in parallel.
// Afterwards, merge the resulting list // Afterwards, merge the resulting list
return forkJoin(...calls).pipe( return forkJoin(...calls).pipe(
map( map(
(results: CollectionResource<CapabilityResource>[]) => results.reduce( (results:CollectionResource<CapabilityResource>[]) => results.reduce(
(acc, next) => acc.concat(next.elements), (acc, next) => acc.concat(next.elements),
data.elements, data.elements,
),
), ),
), );
); }
}
// The current page is the only page, return the results.
// The current page is the only page, return the results. return of(data.elements);
return of(data.elements); }),
}), )
) .subscribe((capabilities) => {
.subscribe((capabilities) => { this.currentUserStore.update((state) => ({
this.currentUserStore.update(state => ({ ...state,
...state, capabilities: [
capabilities: [ ...capabilities,
...capabilities, ...(state.capabilities || []).filter((cap) => !!capabilities.find((newCap) => newCap.id === cap.id)),
...(state.capabilities || []).filter(cap => !!capabilities.find(newCap => newCap.id === cap.id)), ],
], }));
})); });
});
}); });
return this.currentUserQuery.capabilities$; return this.currentUserQuery.capabilities$;
@ -138,37 +142,35 @@ export class CurrentUserService {
/** /**
* Returns the users' capabilities filtered by context * Returns the users' capabilities filtered by context
*/ */
public capabilitiesForContext$(contextId: string) { public capabilitiesForContext$(contextId:string) {
return this.capabilities$.pipe( return this.capabilities$.pipe(
map((capabilities) => capabilities.filter(cap => cap.context.href.endsWith(`/${contextId}`))), map((capabilities) => capabilities.filter((cap) => cap.context.href.endsWith(`/${contextId}`))),
distinctUntilChanged(), distinctUntilChanged(),
); );
} }
/** /**
* Returns an Observable<boolean> indicating whether the user has the required capabilities in the provided context. * Returns an Observable<boolean> indicating whether the user has the required capabilities in the provided context.
*/ */
public hasCapabilities$(action: string|string[], contextId: string = 'global') { public hasCapabilities$(action:string|string[], contextId = 'global') {
const actions = _.castArray(action); const actions = _.castArray(action);
return this.capabilitiesForContext$(contextId).pipe( return this.capabilitiesForContext$(contextId).pipe(
map((capabilities) => actions.reduce( map((capabilities) => actions.reduce(
(acc, action) => { (acc, contextAction) => acc && !!capabilities.find((cap) => cap.action.href.endsWith(`/api/v3/actions/${contextAction}`)),
return acc && !!capabilities.find(cap => cap.action.href.endsWith(`/api/v3/actions/${action}`));
},
capabilities.length > 0, capabilities.length > 0,
)), )),
distinctUntilChanged() distinctUntilChanged(),
); );
} }
/** /**
* Returns an Observable<boolean> indicating whether the user has any of the required capabilities in the provided context. * Returns an Observable<boolean> indicating whether the user has any of the required capabilities in the provided context.
*/ */
public hasAnyCapabilityOf$(actions: string|string[], contextId: string = 'global') { public hasAnyCapabilityOf$(actions:string|string[], contextId = 'global') {
const actionsToFilter = _.castArray(actions); const actionsToFilter = _.castArray(actions);
return this.capabilitiesForContext$(contextId).pipe( return this.capabilitiesForContext$(contextId).pipe(
map((capabilities) => capabilities.reduce( map((capabilities) => capabilities.reduce(
(acc, cap) => acc || !!actionsToFilter.find(action => cap.action.href.endsWith(`/api/v3/actions/${action}`)), (acc, cap) => acc || !!actionsToFilter.find((action) => cap.action.href.endsWith(`/api/v3/actions/${action}`)),
false, false,
)), )),
distinctUntilChanged(), distinctUntilChanged(),
@ -177,19 +179,19 @@ export class CurrentUserService {
// Everything below this is deprecated legacy interfacing and should not be used // Everything below this is deprecated legacy interfacing and should not be used
private setupLegacyDataListeners() { private setupLegacyDataListeners() {
this.currentUserQuery.user$.subscribe(user => this._user = user); this.currentUserQuery.user$.subscribe((user) => (this._user = user));
this.currentUserQuery.isLoggedIn$.subscribe(isLoggedIn => this._isLoggedIn = isLoggedIn); this.currentUserQuery.isLoggedIn$.subscribe((isLoggedIn) => (this._isLoggedIn = isLoggedIn));
} }
private _isLoggedIn = false; private _isLoggedIn = false;
/** @deprecated Use the store mechanism `currentUserQuery.isLoggedIn$` */ /** @deprecated Use the store mechanism `currentUserQuery.isLoggedIn$` */
public get isLoggedIn() { public get isLoggedIn() {
return this._isLoggedIn; return this._isLoggedIn;
} }
private _user: CurrentUser = { private _user:CurrentUser = {
id: null, id: null,
name: null, name: null,
mail: null, mail: null,

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,21 +26,21 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { Store, StoreConfig } from '@datorama/akita'; import { Store, StoreConfig } from '@datorama/akita';
import { CapabilityResource } from "core-app/features/hal/resources/capability-resource"; import { CapabilityResource } from 'core-app/features/hal/resources/capability-resource';
export interface CurrentUser { export interface CurrentUser {
id: string|null; id:string|null;
name: string|null; name:string|null;
mail: string|null; mail:string|null;
} }
export interface CurrentUserState extends CurrentUser { export interface CurrentUserState extends CurrentUser {
capabilities: CapabilityResource[]|null; capabilities:CapabilityResource[]|null;
} }
export function createInitialState(): CurrentUserState { export function createInitialState():CurrentUserState {
return { return {
id: null, id: null,
name: null, name: null,

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,7 +26,7 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
/*jshint expr: true*/ /* jshint expr: true */
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
@ -35,8 +35,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service';
import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { TimezoneService } from 'core-app/core/datetime/timezone.service'; import { TimezoneService } from 'core-app/core/datetime/timezone.service';
describe('TimezoneService', function () { describe('TimezoneService', () => {
const TIME = '2013-02-08T09:30:26'; const TIME = '2013-02-08T09:30:26';
const DATE = '2013-02-08'; const DATE = '2013-02-08';
let timezoneService:TimezoneService; let timezoneService:TimezoneService;
@ -44,57 +43,56 @@ describe('TimezoneService', function () {
const compile = (timezone?:string) => { const compile = (timezone?:string) => {
const ConfigurationServiceStub = { const ConfigurationServiceStub = {
isTimezoneSet: () => !!timezone, isTimezoneSet: () => !!timezone,
timezone: () => timezone timezone: () => timezone,
}; };
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
HttpClientModule HttpClientModule,
], ],
providers: [ providers: [
{ provide: I18nService, useValue: {} }, { provide: I18nService, useValue: {} },
{ provide: ConfigurationService, useValue: ConfigurationServiceStub }, { provide: ConfigurationService, useValue: ConfigurationServiceStub },
PathHelperService, PathHelperService,
TimezoneService, TimezoneService,
] ],
}); });
timezoneService = TestBed.inject(TimezoneService); timezoneService = TestBed.inject(TimezoneService);
}; };
describe('without time zone set', function () { describe('without time zone set', () => {
beforeEach(() => { beforeEach(() => {
compile(); compile();
}); });
describe('#parseDatetime', function () { describe('#parseDatetime', () => {
it('is UTC', function () { it('is UTC', () => {
var time = timezoneService.parseDatetime(TIME); const time = timezoneService.parseDatetime(TIME);
expect(time.utcOffset()).toEqual(0); expect(time.utcOffset()).toEqual(0);
expect(time.format('HH:mm')).toEqual('09:30'); expect(time.format('HH:mm')).toEqual('09:30');
}); });
it('has no time information', function () { it('has no time information', () => {
var time = timezoneService.parseDate(DATE); const time = timezoneService.parseDate(DATE);
expect(time.format('HH:mm')).toEqual('00:00'); expect(time.format('HH:mm')).toEqual('00:00');
}); });
}); });
}); });
describe('with time zone set', function () { describe('with time zone set', () => {
beforeEach(() => { beforeEach(() => {
compile('America/Vancouver'); compile('America/Vancouver');
}); });
describe('Non-UTC timezone', function () { describe('Non-UTC timezone', () => {
it('is in the given timezone', () => {
it('is in the given timezone' , function () {
const date = timezoneService.parseDatetime(TIME); const date = timezoneService.parseDatetime(TIME);
expect(date.format('HH:mm')).toEqual('01:30'); expect(date.format('HH:mm')).toEqual('01:30');
}); });
it('has local time zone', function () { it('has local time zone', () => {
expect(timezoneService.ConfigurationService.timezone()).toEqual('America/Vancouver'); expect(timezoneService.configurationService.timezone()).toEqual('America/Vancouver');
}); });
}); });
}); });

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -34,8 +34,8 @@ import { Moment } from 'moment';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TimezoneService { export class TimezoneService {
constructor(readonly ConfigurationService:ConfigurationService, constructor(readonly configurationService:ConfigurationService,
readonly I18n:I18nService) { readonly I18n:I18nService) {
this.setupLocale(); this.setupLocale();
} }
@ -48,11 +48,11 @@ export class TimezoneService {
* a local date time moment object. * a local date time moment object.
*/ */
public parseDatetime(datetime:string, format?:string):Moment { public parseDatetime(datetime:string, format?:string):Moment {
var d = moment.utc(datetime, format); const d = moment.utc(datetime, format);
if (this.ConfigurationService.isTimezoneSet()) { if (this.configurationService.isTimezoneSet()) {
d.local(); d.local();
d.tz(this.ConfigurationService.timezone()); d.tz(this.configurationService.timezone());
} }
return d; return d;
@ -73,13 +73,13 @@ export class TimezoneService {
* @returns {Moment} * @returns {Moment}
*/ */
public parseLocalDateTime(date:string, format?:string) { public parseLocalDateTime(date:string, format?:string) {
var result; let result;
format = format || this.getTimeFormat(); const timeFormat = format || this.getTimeFormat();
if (this.ConfigurationService.isTimezoneSet()) { if (this.configurationService.isTimezoneSet()) {
result = moment.tz(date, format!, this.ConfigurationService.timezone()); result = moment.tz(date, timeFormat, this.configurationService.timezone());
} else { } else {
result = moment(date, format); result = moment(date, timeFormat);
} }
result.utc(); result.utc();
@ -103,7 +103,7 @@ export class TimezoneService {
} }
public formattedDate(date:string) { public formattedDate(date:string) {
var d = this.parseDate(date); const d = this.parseDate(date);
return d.format(this.getDateFormat()); return d.format(this.getDateFormat());
} }
@ -132,8 +132,8 @@ export class TimezoneService {
} }
public formattedDatetime(datetimeString:string) { public formattedDatetime(datetimeString:string) {
var c = this.formattedDatetimeComponents(datetimeString); const c = this.formattedDatetimeComponents(datetimeString);
return c[0] + ' ' + c[1]; return `${c[0]} ${c[1]}`;
} }
public formattedRelativeDateTime(datetimeString:string) { public formattedRelativeDateTime(datetimeString:string) {
@ -142,10 +142,10 @@ export class TimezoneService {
} }
public formattedDatetimeComponents(datetimeString:string) { public formattedDatetimeComponents(datetimeString:string) {
var d = this.parseDatetime(datetimeString); const d = this.parseDatetime(datetimeString);
return [ return [
d.format(this.getDateFormat()), d.format(this.getDateFormat()),
d.format(this.getTimeFormat()) d.format(this.getTimeFormat()),
]; ];
} }
@ -174,15 +174,15 @@ export class TimezoneService {
} }
public isValid(date:string, dateFormat:string) { public isValid(date:string, dateFormat:string) {
var format = dateFormat || this.getDateFormat(); const format = dateFormat || this.getDateFormat();
return moment(date, [format], true).isValid(); return moment(date, [format], true).isValid();
} }
public getDateFormat() { public getDateFormat() {
return this.ConfigurationService.dateFormatPresent() ? this.ConfigurationService.dateFormat() : 'L'; return this.configurationService.dateFormatPresent() ? this.configurationService.dateFormat() : 'L';
} }
public getTimeFormat() { public getTimeFormat() {
return this.ConfigurationService.timeFormatPresent() ? this.ConfigurationService.timeFormat() : 'LT'; return this.configurationService.timeFormatPresent() ? this.configurationService.timeFormat() : 'LT';
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,13 +26,12 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import {Inject, Injectable} from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import {DOCUMENT} from "@angular/common"; import { DOCUMENT } from '@angular/common';
import {enterpriseEditionUrl} from "core-app/core/setup/globals/constants.const"; import { enterpriseEditionUrl } from 'core-app/core/setup/globals/constants.const';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class BannersService { export class BannersService {
private readonly _banners:boolean = true; private readonly _banners:boolean = true;
constructor(@Inject(DOCUMENT) protected documentElement:Document) { constructor(@Inject(DOCUMENT) protected documentElement:Document) {
@ -43,7 +42,7 @@ export class BannersService {
return this._banners; return this._banners;
} }
public getEnterPriseEditionUrl({ referrer, hash }:{referrer?:string, hash?:string} = {}) { public getEnterPriseEditionUrl({ referrer, hash }:{ referrer?:string, hash?:string } = {}) {
const url = new URL(enterpriseEditionUrl); const url = new URL(enterpriseEditionUrl);
if (referrer) { if (referrer) {
url.searchParams.set('op_referrer', referrer); url.searchParams.set('op_referrer', referrer);

@ -1,4 +1,4 @@
import * as Sentry from "@sentry/angular"; import * as Sentry from '@sentry/angular';
import { Integrations } from "@sentry/tracing"; import { Integrations } from '@sentry/tracing';
export { Sentry, Integrations }; export { Sentry, Integrations };

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,8 +26,10 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Hub, Severity, Scope, Event as SentryEvent } from "@sentry/types"; import {
import { environment } from "../../../../environments/environment"; Event as SentryEvent, Hub, Scope, Severity,
} from '@sentry/types';
import { environment } from '../../../../environments/environment';
export type ScopeCallback = (scope:Scope) => void; export type ScopeCallback = (scope:Scope) => void;
export type MessageSeverity = 'fatal'|'error'|'warning'|'log'|'info'|'debug'; export type MessageSeverity = 'fatal'|'error'|'warning'|'log'|'info'|'debug';
@ -57,7 +59,6 @@ interface QueuedMessage {
} }
export class SentryReporter implements ErrorReporter { export class SentryReporter implements ErrorReporter {
private contextCallbacks:ScopeCallback[] = []; private contextCallbacks:ScopeCallback[] = [];
private messageStack:QueuedMessage[] = []; private messageStack:QueuedMessage[] = [];
@ -67,7 +68,7 @@ export class SentryReporter implements ErrorReporter {
private client:Hub; private client:Hub;
constructor() { constructor() {
const sentryElement = document.querySelector('meta[name=openproject_sentry]') as HTMLElement|null; const sentryElement = document.querySelector('meta[name=openproject_sentry]') as HTMLElement;
if (sentryElement !== null) { if (sentryElement !== null) {
this.loadSentry(sentryElement); this.loadSentry(sentryElement);
} else { } else {
@ -84,9 +85,9 @@ export class SentryReporter implements ErrorReporter {
import('./sentry-dependency').then((imported) => { import('./sentry-dependency').then((imported) => {
const sentry = imported.Sentry; const sentry = imported.Sentry;
sentry.init({ sentry.init({
dsn: dsn, dsn,
debug: !environment.production, debug: !environment.production,
release: 'op-frontend@' + version, release: `op-frontend@${version}`,
environment: environment.production ? 'production' : 'development', environment: environment.production ? 'production' : 'development',
// Integrations // Integrations
@ -94,13 +95,13 @@ export class SentryReporter implements ErrorReporter {
tracesSampler: (samplingContext) => { tracesSampler: (samplingContext) => {
switch (samplingContext.transactionContext.op) { switch (samplingContext.transactionContext.op) {
case 'op': case 'op':
case 'navigation': case 'navigation':
// Trace 1% of page loads and navigation events // Trace 1% of page loads and navigation events
return Math.min(0.01 * traceFactor, 1.0); return Math.min(0.01 * traceFactor, 1.0);
default: default:
// Trace 0.1% of requests // Trace 0.1% of requests
return Math.min(0.001 * traceFactor, 1.0); return Math.min(0.001 * traceFactor, 1.0);
} }
}, },
@ -159,7 +160,7 @@ export class SentryReporter implements ErrorReporter {
if (this.client) { if (this.client) {
/** Add to global context as well */ /** Add to global context as well */
callbacks.forEach(cb => this.client.configureScope(cb)); callbacks.forEach((cb) => this.client.configureScope(cb));
} }
} }
@ -172,7 +173,7 @@ export class SentryReporter implements ErrorReporter {
if (this.sentryConfigured) { if (this.sentryConfigured) {
this.messageStack.push({ type, args }); this.messageStack.push({ type, args });
} else { } else {
console.log("[ErrorReporter] Would queue sentry message %O %O, but is not configured.", type, args); console.log('[ErrorReporter] Would queue sentry message %O %O, but is not configured.', type, args);
} }
} }
@ -187,7 +188,7 @@ export class SentryReporter implements ErrorReporter {
scope.setExtra('url_query', window.location.search); scope.setExtra('url_query', window.location.search);
/** Execute callbacks */ /** Execute callbacks */
this.contextCallbacks.forEach(cb => cb(scope)); this.contextCallbacks.forEach((cb) => cb(scope));
} }
/** /**
@ -199,10 +200,10 @@ export class SentryReporter implements ErrorReporter {
private filterEvent(event:SentryEvent):SentryEvent|null { private filterEvent(event:SentryEvent):SentryEvent|null {
const unsupportedBrowser = document.body.classList.contains('-unsupported-browser'); const unsupportedBrowser = document.body.classList.contains('-unsupported-browser');
if (unsupportedBrowser) { if (unsupportedBrowser) {
console.warn("Browser is not supported, skipping sentry reporting completely."); console.warn('Browser is not supported, skipping sentry reporting completely.');
return null; return null;
} }
return event; return event;
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,7 +27,6 @@
//++ //++
export class ExpressionService { export class ExpressionService {
// This is what returned by rails-angular-xss when it discovers double open curly braces // This is what returned by rails-angular-xss when it discovers double open curly braces
// See https://github.com/opf/rails-angular-xss for more information. // See https://github.com/opf/rails-angular-xss for more information.
public static get UNESCAPED_EXPRESSION() { public static get UNESCAPED_EXPRESSION() {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,12 +26,14 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { HttpEvent, HttpResponse } from "@angular/common/http"; import { HttpEvent, HttpResponse } from '@angular/common/http';
import { from, Observable, of } from "rxjs"; import { from, Observable, of } from 'rxjs';
import { share, switchMap } from "rxjs/operators"; import { share, switchMap } from 'rxjs/operators';
import { OpenProjectFileUploadService, UploadBlob, UploadFile, UploadInProgress } from './op-file-upload.service'; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { HalResource } from "core-app/features/hal/resources/hal-resource"; import {
OpenProjectFileUploadService, UploadBlob, UploadFile, UploadInProgress,
} from './op-file-upload.service';
interface PrepareUploadResult { interface PrepareUploadResult {
url:string; url:string;
@ -51,47 +53,47 @@ export class OpenProjectDirectFileUploadService extends OpenProjectFileUploadSer
const observable = from(this.getDirectUploadFormFrom(url, file)) const observable = from(this.getDirectUploadFormFrom(url, file))
.pipe( .pipe(
switchMap(this.uploadToExternal(file, method, responseType)), switchMap(this.uploadToExternal(file, method, responseType)),
share() share(),
); );
return [file, observable] as UploadInProgress; return [file, observable] as UploadInProgress;
} }
private uploadToExternal(file:UploadFile|UploadBlob, method:string, responseType:string):(result:PrepareUploadResult) => Observable<HttpEvent<unknown>> { private uploadToExternal(file:UploadFile|UploadBlob, method:string, responseType:string):(result:PrepareUploadResult) => Observable<HttpEvent<unknown>> {
return result => { return (result) => {
result.form.append('file', file, file.customName || file.name); result.form.append('file', file, file.customName || file.name);
return this return this
.http .http
.request<HalResource>( .request<HalResource>(
method, method,
result.url, result.url,
{ {
body: result.form, body: result.form,
// Observe the response, not the body // Observe the response, not the body
observe: 'events', observe: 'events',
// This is important as the CORS policy for the bucket is * and you can't use credentals then, // This is important as the CORS policy for the bucket is * and you can't use credentals then,
// besides we don't need them here anyway. // besides we don't need them here anyway.
withCredentials: false, withCredentials: false,
responseType: responseType as any, responseType: responseType as any,
// Subscribe to progress events. subscribe() will fire multiple times! // Subscribe to progress events. subscribe() will fire multiple times!
reportProgress: true reportProgress: true,
} },
) )
.pipe(switchMap(this.finishUpload(result))); .pipe(switchMap(this.finishUpload(result)));
}; };
} }
private finishUpload(result:PrepareUploadResult):(result:HttpEvent<unknown>) => Observable<HttpEvent<unknown>> { private finishUpload(result:PrepareUploadResult):(result:HttpEvent<unknown>) => Observable<HttpEvent<unknown>> {
return event => { return (event) => {
if (event instanceof HttpResponse) { if (event instanceof HttpResponse) {
return this return this
.http .http
.get( .get(
result.response._links.completeUpload.href, result.response._links.completeUpload.href,
{ {
observe: 'response' observe: 'response',
} },
); );
} }
@ -106,7 +108,7 @@ export class OpenProjectDirectFileUploadService extends OpenProjectFileUploadSer
description: file.description, description: file.description,
fileName: file.customName || file.name, fileName: file.customName || file.name,
fileSize: file.size, fileSize: file.size,
contentType: file.type contentType: file.type,
}; };
/* /*
@ -124,14 +126,14 @@ export class OpenProjectDirectFileUploadService extends OpenProjectFileUploadSer
const result = this const result = this
.http .http
.request<HalResource>( .request<HalResource>(
"post", 'post',
url, url,
{ {
body: formData, body: formData,
withCredentials: true, withCredentials: true,
responseType: "json" as any responseType: 'json' as any,
} },
) )
.toPromise() .toPromise()
.then((res) => { .then((res) => {
const form = new FormData(); const form = new FormData();
@ -140,7 +142,7 @@ export class OpenProjectDirectFileUploadService extends OpenProjectFileUploadSer
form.append(key, value); form.append(key, value);
}); });
return { url: res._links.addAttachment.href, form: form, response: res }; return { url: res._links.addAttachment.href, form, response: res };
}); });
return result; return result;

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,13 +26,13 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { OpenProjectDirectFileUploadService } from 'core-app/core/file-upload/op-direct-file-upload.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { getTestBed, TestBed } from '@angular/core/testing';
import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { States } from 'core-app/core/states/states.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { OpenProjectFileUploadService, UploadFile, UploadResult } from './op-file-upload.service'; import { OpenProjectFileUploadService, UploadFile, UploadResult } from './op-file-upload.service';
import { OpenProjectDirectFileUploadService } from "core-app/core/file-upload/op-direct-file-upload.service";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { getTestBed, TestBed } from "@angular/core/testing";
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service";
import { States } from "core-app/core/states/states.service";
import { I18nService } from "core-app/core/i18n/i18n.service";
describe('opFileUpload service', () => { describe('opFileUpload service', () => {
let injector:TestBed; let injector:TestBed;
@ -47,8 +47,8 @@ describe('opFileUpload service', () => {
I18nService, I18nService,
OpenProjectFileUploadService, OpenProjectFileUploadService,
OpenProjectDirectFileUploadService, OpenProjectDirectFileUploadService,
HalResourceService HalResourceService,
] ],
}); });
injector = getTestBed(); injector = getTestBed();
@ -61,16 +61,16 @@ describe('opFileUpload service', () => {
}); });
describe('when uploading multiple files', () => { describe('when uploading multiple files', () => {
var result:UploadResult; let result:UploadResult;
const file:UploadFile = new File([ JSON.stringify({ const file:UploadFile = new File([JSON.stringify({
name: 'name', name: 'name',
description: 'description' description: 'description',
})], 'name'); })], 'name');
beforeEach(() => { beforeEach(() => {
result = service.upload('/my/api/path', [file, file]); result = service.upload('/my/api/path', [file, file]);
httpMock.match(`/my/api/path`).forEach((req) => { httpMock.match('/my/api/path').forEach((req) => {
expect(req.request.method).toBe("POST"); expect(req.request.method).toBe('POST');
req.flush({}); req.flush({});
}); });
}); });

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,19 +26,20 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { HttpClient, HttpEvent, HttpEventType, HttpResponse } from "@angular/common/http"; import {
import { Observable } from "rxjs"; HttpClient, HttpEvent, HttpEventType, HttpResponse,
import { filter, map, share } from "rxjs/operators"; } from '@angular/common/http';
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service"; import { Observable } from 'rxjs';
import { HalResource } from "core-app/features/hal/resources/hal-resource"; import { filter, map, share } from 'rxjs/operators';
import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
export interface UploadFile extends File { export interface UploadFile extends File {
description?:string; description?:string;
customName?:string; customName?:string;
} }
export interface UploadBlob extends Blob { export interface UploadBlob extends Blob {
description?:string; description?:string;
customName?:string; customName?:string;
@ -61,7 +62,7 @@ export interface MappedUploadResult {
@Injectable() @Injectable()
export class OpenProjectFileUploadService { export class OpenProjectFileUploadService {
constructor(protected http:HttpClient, constructor(protected http:HttpClient,
protected halResource:HalResourceService) { protected halResource:HalResourceService) {
} }
/** /**
@ -75,11 +76,9 @@ export class OpenProjectFileUploadService {
public uploadAndMapResponse(url:string, files:UploadFile[], method = 'post') { public uploadAndMapResponse(url:string, files:UploadFile[], method = 'post') {
const { uploads, finished } = this.upload(url, files); const { uploads, finished } = this.upload(url, files);
const mapped = finished const mapped = finished
.then((result:HalResource[]) => result.map((el:HalResource) => { .then((result:HalResource[]) => result.map((el:HalResource) => ({ response: el, uploadUrl: el.staticDownloadLocation.href }))) as Promise<{ response:HalResource, uploadUrl:string }[]>;
return { response: el, uploadUrl: el.staticDownloadLocation.href };
})) as Promise<{ response:HalResource, uploadUrl:string }[]>;
return { uploads: uploads, finished: mapped } as MappedUploadResult; return { uploads, finished: mapped } as MappedUploadResult;
} }
/** /**
@ -104,7 +103,7 @@ export class OpenProjectFileUploadService {
const formData = new FormData(); const formData = new FormData();
const metadata = { const metadata = {
description: file.description, description: file.description,
fileName: file.customName || file.name fileName: file.customName || file.name,
}; };
// add the metadata object // add the metadata object
@ -119,20 +118,20 @@ export class OpenProjectFileUploadService {
const observable = this const observable = this
.http .http
.request<HalResource>( .request<HalResource>(
method, method,
url, url,
{ {
body: formData, body: formData,
// Observe the response, not the body // Observe the response, not the body
observe: 'events', observe: 'events',
withCredentials: true, withCredentials: true,
responseType: responseType as any, responseType: responseType as any,
// Subscribe to progress events. subscribe() will fire multiple times! // Subscribe to progress events. subscribe() will fire multiple times!
reportProgress: true reportProgress: true,
} },
) )
.pipe( .pipe(
share() share(),
); );
return [file, observable] as UploadInProgress; return [file, observable] as UploadInProgress;
@ -144,14 +143,12 @@ export class OpenProjectFileUploadService {
* @param {UploadInProgress[]} uploads * @param {UploadInProgress[]} uploads
*/ */
private whenFinished(uploads:UploadInProgress[]):Promise<HalResource[]> { private whenFinished(uploads:UploadInProgress[]):Promise<HalResource[]> {
const promises = uploads.map(([_, observable]) => { const promises = uploads.map(([_, observable]) => observable
return observable .pipe(
.pipe( filter((evt) => evt.type === HttpEventType.Response),
filter((evt) => evt.type === HttpEventType.Response), map((evt:HttpResponse<HalResource>) => this.halResource.createHalResource(evt.body)),
map((evt:HttpResponse<HalResource>) => this.halResource.createHalResource(evt.body)) )
) .toPromise());
.toPromise();
});
return Promise.all(promises); return Promise.all(promises);
} }

@ -1,8 +1,8 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient } from '@angular/common/http';
import { FormBuilder } from '@angular/forms';
import { FormsService } from './forms.service'; import { FormsService } from './forms.service';
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { HttpClient } from "@angular/common/http";
import { FormBuilder } from "@angular/forms";
describe('FormsService', () => { describe('FormsService', () => {
let service:FormsService; let service:FormsService;
@ -10,21 +10,21 @@ describe('FormsService', () => {
let httpTestingController:HttpTestingController; let httpTestingController:HttpTestingController;
const testFormUrl = 'http://op.com/form'; const testFormUrl = 'http://op.com/form';
const formModel = { const formModel = {
"name": "Project 1", name: 'Project 1',
"_links": { _links: {
"parent": { parent: {
"href": "/api/v3/projects/26", href: '/api/v3/projects/26',
"title": "Parent project", title: 'Parent project',
"name": "Parent project" name: 'Parent project',
}, },
"users": [ users: [
{ {
"href": "/api/v3/users/26", href: '/api/v3/users/26',
"title": "User 1", title: 'User 1',
"name": "User 1" name: 'User 1',
} },
] ],
} },
}; };
const formBuilder = new FormBuilder(); const formBuilder = new FormBuilder();
@ -76,57 +76,58 @@ describe('FormsService', () => {
// @ts-ignore // @ts-ignore
const formattedModel = service.formatModelToSubmit(formModel); const formattedModel = service.formatModelToSubmit(formModel);
const expectedResult = { const expectedResult = {
"name": "Project 1", name: 'Project 1',
"_links": { _links: {
"parent": { parent: {
"href": "/api/v3/projects/26", href: '/api/v3/projects/26',
}, },
"users": [ users: [
{ {
"href": "/api/v3/users/26", href: '/api/v3/users/26',
} },
] ],
} },
}; };
expect(formattedModel).toEqual(expectedResult); expect(formattedModel).toEqual(expectedResult);
}); });
it('should set the backend errors in the FormGroup', () => { it('should set the backend errors in the FormGroup', () => {
const form= formBuilder.group({ const form = formBuilder.group({
...formModel, ...formModel,
_links: formBuilder.group(formModel._links), _links: formBuilder.group(formModel._links),
}); });
const backEndErrorResponse = { const backEndErrorResponse = {
error: { error: {
"_type": "Error", _type: 'Error',
"errorIdentifier": "urn:openproject-org:api:v3:errors:MultipleErrors", errorIdentifier: 'urn:openproject-org:api:v3:errors:MultipleErrors',
"message": "Multiple field constraints have been violated.", message: 'Multiple field constraints have been violated.',
"_embedded": { _embedded: {
"errors": [ errors: [
{ {
"_type": "Error", _type: 'Error',
"errorIdentifier": "urn:openproject-org:api:v3:errors:PropertyConstraintViolation", errorIdentifier: 'urn:openproject-org:api:v3:errors:PropertyConstraintViolation',
"message": "Name can't be blank.", message: "Name can't be blank.",
"_embedded": { _embedded: {
"details": { details: {
"attribute": "name" attribute: 'name',
} },
} },
}, },
{ {
"_type": "Error", _type: 'Error',
"errorIdentifier": "urn:openproject-org:api:v3:errors:PropertyConstraintViolation", errorIdentifier: 'urn:openproject-org:api:v3:errors:PropertyConstraintViolation',
"message": "Identifier can't be blank.", message: "Identifier can't be blank.",
"_embedded": { _embedded: {
"details": { details: {
"attribute": "parent" attribute: 'parent',
} },
} },
}, },
] ],
} },
}, status: 422 },
status: 422,
}; };
// @ts-ignore // @ts-ignore

@ -1,40 +1,39 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { FormGroup } from "@angular/forms"; import { FormGroup } from '@angular/forms';
import { catchError, map } from "rxjs/operators"; import { catchError, map } from 'rxjs/operators';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class FormsService { export class FormsService {
constructor( constructor(
private _httpClient:HttpClient, private _httpClient:HttpClient,
) { } ) { }
submit$(form:FormGroup, resourceEndpoint:string, resourceId?:string, formHttpMethod?: 'post' | 'patch', formSchema?:IOPFormSchema):Observable<any> { submit$(form:FormGroup, resourceEndpoint:string, resourceId?:string, formHttpMethod?:'post' | 'patch', formSchema?:IOPFormSchema):Observable<any> {
const modelToSubmit = this.formatModelToSubmit(form.getRawValue(), formSchema); const modelToSubmit = this.formatModelToSubmit(form.getRawValue(), formSchema);
const httpMethod = resourceId ? 'patch' : (formHttpMethod || 'post'); const httpMethod = resourceId ? 'patch' : (formHttpMethod || 'post');
const url = resourceId ? `${resourceEndpoint}/${resourceId}` : resourceEndpoint; const url = resourceId ? `${resourceEndpoint}/${resourceId}` : resourceEndpoint;
return this._httpClient return this._httpClient
[httpMethod]( [httpMethod](
url, url,
modelToSubmit, modelToSubmit,
{ {
withCredentials: true, withCredentials: true,
responseType: 'json' responseType: 'json',
} },
) )
.pipe( .pipe(
catchError((error:HttpErrorResponse) => { catchError((error:HttpErrorResponse) => {
if (error.status == 422 ) { if (error.status == 422) {
this.handleBackendFormValidationErrors(error, form); this.handleBackendFormValidationErrors(error, form);
} }
throw error; throw error;
}) }),
); );
} }
@ -45,34 +44,14 @@ export class FormsService {
.post( .post(
`${resourceEndpoint}/form`, `${resourceEndpoint}/form`,
modelToSubmit, modelToSubmit,
{
withCredentials: true,
responseType: 'json'
}
)
.pipe(
map((response: HalSource) => this.getFormattedErrors(Object.values(response?._embedded?.validationErrors))),
map((formattedErrors: IFormattedValidationError[]) => this.setFormValidationErrors(formattedErrors, form)),
);
}
getFormBackendValidationError$(formValue: {[key:string]: any}, resourceEndpoint:string, limitValidationToKeys?:string | string[], formSchema?:IOPFormSchema) {
const modelToSubmit = this.formatModelToSubmit(formValue, formSchema);
return this._httpClient
.post(
resourceEndpoint,
modelToSubmit,
{ {
withCredentials: true, withCredentials: true,
responseType: 'json', responseType: 'json',
headers: { },
'content-type': 'application/json; charset=utf-8'
}
}
) )
.pipe( .pipe(
map((response: HalSource) => this.getAllFormValidationErrors(response._embedded.validationErrors, limitValidationToKeys)) map((response:HalSource) => this.getFormattedErrors(Object.values(response?._embedded?.validationErrors))),
map((formattedErrors:IFormattedValidationError[]) => this.setFormValidationErrors(formattedErrors, form)),
); );
} }
@ -84,14 +63,14 @@ export class FormsService {
* in the shape of '{href:hrefValue}' in order to fit the backend expectations. * in the shape of '{href:hrefValue}' in order to fit the backend expectations.
* */ * */
private formatModelToSubmit(formModel:IOPFormModel, formSchema:IOPFormSchema = {}):IOPFormModel { private formatModelToSubmit(formModel:IOPFormModel, formSchema:IOPFormSchema = {}):IOPFormModel {
let {_links:linksModel, ...mainModel} = formModel; let { _links: linksModel, ...mainModel } = formModel;
const resourcesModel = linksModel || Object.keys(formSchema) const resourcesModel = linksModel || Object.keys(formSchema)
.filter(formSchemaKey => !!formSchema[formSchemaKey]?.type && formSchema[formSchemaKey]?.location === '_links') .filter((formSchemaKey) => !!formSchema[formSchemaKey]?.type && formSchema[formSchemaKey]?.location === '_links')
.reduce((result, formSchemaKey) => { .reduce((result, formSchemaKey) => {
const {[formSchemaKey]:keyToRemove, ...mainModelWithoutResource} = mainModel; const { [formSchemaKey]: keyToRemove, ...mainModelWithoutResource } = mainModel;
mainModel = mainModelWithoutResource; mainModel = mainModelWithoutResource;
return {...result, [formSchemaKey]: formModel[formSchemaKey]}; return { ...result, [formSchemaKey]: formModel[formSchemaKey] };
}, {}); }, {});
const formattedResourcesModel = Object const formattedResourcesModel = Object
@ -100,9 +79,9 @@ export class FormsService {
const resourceModel = resourcesModel[resourceKey]; const resourceModel = resourcesModel[resourceKey];
// Form.payload resources have a HalLinkSource interface while // Form.payload resources have a HalLinkSource interface while
// API resource options have a IAllowedValue interface // API resource options have a IAllowedValue interface
const formattedResourceModel = Array.isArray(resourceModel) ? const formattedResourceModel = Array.isArray(resourceModel)
resourceModel.map(resourceElement => ({ href: resourceElement?.href || resourceElement?._links?.self?.href || null })) : ? resourceModel.map((resourceElement) => ({ href: resourceElement?.href || resourceElement?._links?.self?.href || null }))
{ href: resourceModel?.href || resourceModel?._links?.self?.href || null }; : { href: resourceModel?.href || resourceModel?._links?.self?.href || null };
return { return {
...result, ...result,
@ -113,7 +92,7 @@ export class FormsService {
return { return {
...mainModel, ...mainModel,
_links: formattedResourcesModel, _links: formattedResourcesModel,
} };
} }
/** HAL resources formatting /** HAL resources formatting
@ -125,8 +104,8 @@ export class FormsService {
formatModelToEdit(formModel:IOPFormModel = {}):IOPFormModel { formatModelToEdit(formModel:IOPFormModel = {}):IOPFormModel {
const { _links: resourcesModel, _meta: metaModel, ...otherElements } = formModel; const { _links: resourcesModel, _meta: metaModel, ...otherElements } = formModel;
const otherElementsModel = Object.keys(otherElements) const otherElementsModel = Object.keys(otherElements)
.filter(key => this.isValue(otherElements[key])) .filter((key) => this.isValue(otherElements[key]))
.reduce((model, key) => ({...model, [key]:otherElements[key]}), {}); .reduce((model, key) => ({ ...model, [key]: otherElements[key] }), {});
const model = { const model = {
...otherElementsModel, ...otherElementsModel,
@ -138,8 +117,8 @@ export class FormsService {
} }
private handleBackendFormValidationErrors(error:HttpErrorResponse, form:FormGroup):void { private handleBackendFormValidationErrors(error:HttpErrorResponse, form:FormGroup):void {
const errors:IOPFormError[] = error?.error?._embedded?.errors ? const errors:IOPFormError[] = error?.error?._embedded?.errors
error?.error?._embedded?.errors : [error.error]; ? error?.error?._embedded?.errors : [error.error];
const formErrors = this.getFormattedErrors(errors); const formErrors = this.getFormattedErrors(errors);
this.setFormValidationErrors(formErrors, form); this.setFormValidationErrors(formErrors, form);
@ -149,35 +128,32 @@ export class FormsService {
errors.forEach((err:any) => { errors.forEach((err:any) => {
const formControl = form.get(err.key) || form.get('_links')?.get(err.key); const formControl = form.get(err.key) || form.get('_links')?.get(err.key);
formControl?.setErrors({[err.key]: {message: err.message}}); formControl?.setErrors({ [err.key]: { message: err.message } });
}); });
} }
private getAllFormValidationErrors(validationErrors:IOPValidationErrors, formControlKeys?:string | string[]): {[key:string]: {message:string}} { private getAllFormValidationErrors(validationErrors:IOPValidationErrors, formControlKeys?:string | string[]):{ [key:string]:{ message:string } } {
const errors = Object.values(validationErrors); const errors = Object.values(validationErrors);
const keysToValidate = Array.isArray(formControlKeys) ? formControlKeys : [formControlKeys]; const keysToValidate = Array.isArray(formControlKeys) ? formControlKeys : [formControlKeys];
const formErrors = this.getFormattedErrors(errors) const formErrors = this.getFormattedErrors(errors)
.filter(error => { .filter((error) => {
if (!formControlKeys) { if (!formControlKeys) {
return true; return true;
} else {
return keysToValidate.includes(error.key);
} }
return keysToValidate.includes(error.key);
}) })
.reduce((result, { key, message }) => { .reduce((result, { key, message }) => ({
return { ...result,
...result, [key]: { message },
[key]: {message} }), {});
}
}, {})
return formErrors return formErrors;
} }
private getFormattedErrors(errors:IOPFormError[]):IFormattedValidationError[] { private getFormattedErrors(errors:IOPFormError[]):IFormattedValidationError[] {
const formattedErrors = errors.map(err => ({ const formattedErrors = errors.map((err) => ({
key: err._embedded.details.attribute, key: err._embedded.details.attribute,
message: err.message message: err.message,
})); }));
return formattedErrors; return formattedErrors;
@ -188,13 +164,13 @@ export class FormsService {
const resource = resourcesModel[resourceKey]; const resource = resourcesModel[resourceKey];
// ng-select needs a 'name' in order to show the label // ng-select needs a 'name' in order to show the label
// We need to add it in case of the form payload (HalLinkSource) // We need to add it in case of the form payload (HalLinkSource)
const resourceModel = Array.isArray(resource) ? const resourceModel = Array.isArray(resource)
resource.map(resourceElement => ({...resourceElement, name: resourceElement?.name || resourceElement?.title})) : ? resource.map((resourceElement) => ({ ...resourceElement, name: resourceElement?.name || resourceElement?.title }))
{...resource, name: resource?.name || resource?.title}; : { ...resource, name: resource?.name || resource?.title };
result = { result = {
...result, ...result,
...this.isValue(resourceModel) && {[resourceKey]: resourceModel}, ...this.isValue(resourceModel) && { [resourceKey]: resourceModel },
}; };
return result; return result;

@ -1,5 +1,5 @@
interface IOPFormSettingsResource { interface IOPFormSettingsResource {
_type?:"Form"; _type?:'Form';
_embedded:IOPFormSettings; _embedded:IOPFormSettings;
_links?:{ _links?:{
self:IOPApiCall; self:IOPApiCall;
@ -17,7 +17,7 @@ interface IOPFormSettings {
} }
interface IOPFormSchema { interface IOPFormSchema {
_type?:"Schema"; _type?:'Schema';
_dependencies?:unknown[]; _dependencies?:unknown[];
_attributeGroups?:IOPAttributeGroup[]; _attributeGroups?:IOPAttributeGroup[];
lockVersion?:number; lockVersion?:number;
@ -85,11 +85,10 @@ interface IOPApiOption {
} }
interface IOPAttributeGroup { interface IOPAttributeGroup {
_type: _type:| 'WorkPackageFormAttributeGroup'
| "WorkPackageFormAttributeGroup" | 'WorkPackageFormChildrenQueryGroup'
| "WorkPackageFormChildrenQueryGroup" | 'WorkPackageFormRelationQueryGroup'
| "WorkPackageFormRelationQueryGroup" | unknown;
| unknown;
name:string; name:string;
attributes:string[]; attributes:string[];
} }
@ -105,8 +104,8 @@ interface IOPAllowedValue {
} }
type OPFieldType = 'String' | 'Integer' | 'Float' | 'Boolean' | 'Date' | 'DateTime' | 'Formattable' | type OPFieldType = 'String' | 'Integer' | 'Float' | 'Boolean' | 'Date' | 'DateTime' | 'Formattable' |
'Priority' | 'Status' | 'Type' | 'User' | 'Version' | 'TimeEntriesActivity' | 'Category' | 'Priority' | 'Status' | 'Type' | 'User' | 'Version' | 'TimeEntriesActivity' | 'Category' |
'CustomOption' | 'Project' | 'ProjectStatus' | 'Password'; 'CustomOption' | 'Project' | 'ProjectStatus' | 'Password';
interface IOPFormError { interface IOPFormError {
errorIdentifier:string; errorIdentifier:string;
@ -137,7 +136,3 @@ interface IOPFormErrors {
interface IOPValidationErrors { interface IOPValidationErrors {
[key:string]:IOPFormError; [key:string]:IOPFormError;
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -40,9 +40,7 @@ export const globalSearchWorkPackagesSelectorEntry = 'global-search-work-package
<ng-container wp-isolated-query-space> <ng-container wp-isolated-query-space>
<global-search-work-packages></global-search-work-packages> <global-search-work-packages></global-search-work-packages>
</ng-container> </ng-container>
` `,
}) })
export class GlobalSearchWorkPackagesEntryComponent { export class GlobalSearchWorkPackagesEntryComponent {
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,19 +26,21 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import {
AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, Renderer2,
} from '@angular/core';
import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper'; import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper';
import { I18nService } from 'core-app/core/i18n/i18n.service'; import { I18nService } from 'core-app/core/i18n/i18n.service';
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service"; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { GlobalSearchService } from "core-app/core/global_search/services/global-search.service"; import { GlobalSearchService } from 'core-app/core/global_search/services/global-search.service';
import { UrlParamsHelperService } from "core-app/features/work-packages/components/wp-query/url-params-helper"; import { UrlParamsHelperService } from 'core-app/features/work-packages/components/wp-query/url-params-helper';
import { WorkPackageTableConfigurationObject } from "core-app/features/work-packages/components/wp-table/wp-table-configuration"; import { WorkPackageTableConfigurationObject } from 'core-app/features/work-packages/components/wp-table/wp-table-configuration';
import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space'; import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space';
import { WorkPackageViewFiltersService } from "core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service"; import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
import { debounceTime, distinctUntilChanged, skip } from "rxjs/operators"; import { debounceTime, distinctUntilChanged, skip } from 'rxjs/operators';
import { combineLatest } from "rxjs"; import { combineLatest } from 'rxjs';
import { UntilDestroyedMixin } from "core-app/shared/helpers/angular/until-destroyed.mixin"; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { WorkPackageFiltersService } from "core-app/features/work-packages/components/filters/wp-filters/wp-filters.service"; import { WorkPackageFiltersService } from 'core-app/features/work-packages/components/filters/wp-filters/wp-filters.service';
export const globalSearchWorkPackagesSelector = 'global-search-work-packages'; export const globalSearchWorkPackagesSelector = 'global-search-work-packages';
@ -49,11 +51,12 @@ export const globalSearchWorkPackagesSelector = 'global-search-work-packages';
[queryProps]="queryProps" [queryProps]="queryProps"
[configuration]="tableConfiguration"> [configuration]="tableConfiguration">
</wp-embedded-table> </wp-embedded-table>
` `,
}) })
export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin implements OnInit, OnDestroy, AfterViewInit { export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin implements OnInit, OnDestroy, AfterViewInit {
public queryProps:{ [key:string]:any }; public queryProps:{ [key:string]:any };
public resultsHidden = false; public resultsHidden = false;
public tableConfiguration:WorkPackageTableConfigurationObject = { public tableConfiguration:WorkPackageTableConfigurationObject = {
@ -63,35 +66,35 @@ export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin imple
inlineCreateEnabled: false, inlineCreateEnabled: false,
withFilters: true, withFilters: true,
showFilterButton: true, showFilterButton: true,
filterButtonText: this.I18n.t('js.button_advanced_filter') filterButtonText: this.I18n.t('js.button_advanced_filter'),
}; };
constructor(readonly FocusHelper:FocusHelperService, constructor(readonly FocusHelper:FocusHelperService,
readonly elementRef:ElementRef, readonly elementRef:ElementRef,
readonly renderer:Renderer2, readonly renderer:Renderer2,
readonly I18n:I18nService, readonly I18n:I18nService,
readonly halResourceService:HalResourceService, readonly halResourceService:HalResourceService,
readonly globalSearchService:GlobalSearchService, readonly globalSearchService:GlobalSearchService,
readonly wpTableFilters:WorkPackageViewFiltersService, readonly wpTableFilters:WorkPackageViewFiltersService,
readonly querySpace:IsolatedQuerySpace, readonly querySpace:IsolatedQuerySpace,
readonly wpFilters:WorkPackageFiltersService, readonly wpFilters:WorkPackageFiltersService,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
private UrlParamsHelper:UrlParamsHelperService) { private UrlParamsHelper:UrlParamsHelperService) {
super(); super();
} }
ngAfterViewInit() { ngAfterViewInit() {
combineLatest([ combineLatest([
this.globalSearchService.searchTerm$, this.globalSearchService.searchTerm$,
this.globalSearchService.projectScope$ this.globalSearchService.projectScope$,
]) ])
.pipe( .pipe(
skip(1), skip(1),
distinctUntilChanged(), distinctUntilChanged(),
debounceTime(10), debounceTime(10),
this.untilDestroyed() this.untilDestroyed(),
) )
.subscribe(([newSearchTerm, newProjectScope]) => { .subscribe(([]) => {
this.wpFilters.visible = false; this.wpFilters.visible = false;
this.setQueryProps(); this.setQueryProps();
}); });
@ -99,9 +102,9 @@ export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin imple
this.globalSearchService this.globalSearchService
.resultsHidden$ .resultsHidden$
.pipe( .pipe(
this.untilDestroyed() this.untilDestroyed(),
) )
.subscribe((resultsHidden:boolean) => this.resultsHidden = resultsHidden); .subscribe((resultsHidden:boolean) => (this.resultsHidden = resultsHidden));
} }
ngOnInit():void { ngOnInit():void {
@ -116,8 +119,8 @@ export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin imple
filters.push({ filters.push({
search: { search: {
operator: '**', operator: '**',
values: [this.globalSearchService.searchTerm] values: [this.globalSearchService.searchTerm],
} },
}); });
} }
@ -125,8 +128,8 @@ export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin imple
filters.push({ filters.push({
subprojectId: { subprojectId: {
operator: '!*', operator: '!*',
values: [] values: [],
} },
}); });
columns = ['id', 'subject', 'type', 'status', 'updatedAt']; columns = ['id', 'subject', 'type', 'status', 'updatedAt'];
} }
@ -135,8 +138,8 @@ export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin imple
filters.push({ filters.push({
subprojectId: { subprojectId: {
operator: '*', operator: '*',
values: [] values: [],
} },
}); });
} }
@ -144,8 +147,7 @@ export class GlobalSearchWorkPackagesComponent extends UntilDestroyedMixin imple
'columns[]': columns, 'columns[]': columns,
filters: JSON.stringify(filters), filters: JSON.stringify(filters),
sortBy: JSON.stringify([['updatedAt', 'desc']]), sortBy: JSON.stringify([['updatedAt', 'desc']]),
showHierarchies: false showHierarchies: false,
}; };
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,32 +27,33 @@
//++ //++
import { import {
AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
HostListener, HostListener,
NgZone,
OnDestroy, OnDestroy,
ViewChild, ViewChild,
ViewEncapsulation, ViewEncapsulation,
NgZone, AfterViewInit
} from '@angular/core'; } from '@angular/core';
import { Observable, of } from "rxjs"; import { Observable, of } from 'rxjs';
import { map, tap } from "rxjs/operators"; import { map, tap } from 'rxjs/operators';
import { APIV3Service } from "../../apiv3/api-v3.service"; import { GlobalSearchService } from 'core-app/core/global_search/services/global-search.service';
import { GlobalSearchService } from "core-app/core/global_search/services/global-search.service"; import { isClickedWithModifier } from 'core-app/shared/helpers/link-handling/link-handling';
import { LinkHandling } from "core-app/shared/helpers/link-handling/link-handling"; import { Highlighting } from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions';
import { Highlighting } from "core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions"; import { DeviceService } from 'core-app/core/browser/device.service';
import { DeviceService } from "core-app/core/browser/device.service"; import { ContainHelpers } from 'core-app/shared/directives/focus/contain-helpers';
import { ContainHelpers } from "core-app/shared/directives/focus/contain-helpers"; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
import { HalResourceNotificationService } from "core-app/features/hal/services/hal-resource-notification.service"; import { I18nService } from 'core-app/core/i18n/i18n.service';
import { I18nService } from "core-app/core/i18n/i18n.service"; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { CurrentProjectService } from "core-app/core/current-project/current-project.service"; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; import { OpAutocompleterComponent } from 'core-app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component';
import { OpAutocompleterComponent } from "core-app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component"; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { WorkPackageResource } from "core-app/features/hal/resources/work-package-resource"; import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { HalResourceService } from "core-app/features/hal/services/hal-resource.service"; import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { HalResource } from "core-app/features/hal/resources/hal-resource"; import { APIV3Service } from '../../apiv3/api-v3.service';
export const globalSearchSelector = 'global-search-input'; export const globalSearchSelector = 'global-search-input';
@ -81,29 +82,31 @@ interface SearchOptionItem {
'./global-search.component.sass', './global-search.component.sass',
], ],
// Necessary because of ng-select // Necessary because of ng-select
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
}) })
export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy { export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
@ViewChild('btn', { static: true }) btn:ElementRef; @ViewChild('btn', { static: true }) btn:ElementRef;
@ViewChild(OpAutocompleterComponent, { static: true }) public ngSelectComponent:OpAutocompleterComponent; @ViewChild(OpAutocompleterComponent, { static: true }) public ngSelectComponent:OpAutocompleterComponent;
public expanded = false; public expanded = false;
public markable = false; public markable = false;
public isLoading = false; public isLoading = false;
getAutocompleterData = (query:string):Observable<any[]> => { getAutocompleterData = (query:string):Observable<any[]> => this.autocompleteWorkPackages(query);
return this.autocompleteWorkPackages(query);
};
public autocompleterOptions = { public autocompleterOptions = {
filters:[], filters: [],
resource:'work_packages', resource: 'work_packages',
searchKey:'subjectOrId', searchKey: 'subjectOrId',
getOptionsFn: this.getAutocompleterData getOptionsFn: this.getAutocompleterData,
}; };
/** Remember the current value */ /** Remember the current value */
public currentValue = ''; public currentValue = '';
public isFocusedDirectly = (this.globalSearchService.searchTerm.length > 0); public isFocusedDirectly = (this.globalSearchService.searchTerm.length > 0);
/** Remember the item that best matches the query. /** Remember the item that best matches the query.
@ -113,27 +116,26 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
private unregisterGlobalListener:Function|undefined; private unregisterGlobalListener:Function|undefined;
private isInitialized :boolean = false;
public text:{ [key:string]:string } = { public text:{ [key:string]:string } = {
all_projects: this.I18n.t('js.global_search.all_projects'), all_projects: this.I18n.t('js.global_search.all_projects'),
current_project: this.I18n.t('js.global_search.current_project'), current_project: this.I18n.t('js.global_search.current_project'),
current_project_and_all_descendants: this.I18n.t('js.global_search.current_project_and_all_descendants'), current_project_and_all_descendants: this.I18n.t('js.global_search.current_project_and_all_descendants'),
search: this.I18n.t('js.global_search.search'), search: this.I18n.t('js.global_search.search'),
search_dots: this.I18n.t('js.global_search.search') + ' ...', search_dots: `${this.I18n.t('js.global_search.search')} ...`,
close_search: this.I18n.t('js.global_search.close_search') close_search: this.I18n.t('js.global_search.close_search'),
}; };
constructor(readonly elementRef:ElementRef, constructor(readonly elementRef:ElementRef,
readonly I18n:I18nService, readonly I18n:I18nService,
readonly apiV3Service:APIV3Service, readonly apiV3Service:APIV3Service,
readonly PathHelperService:PathHelperService, readonly pathHelperService:PathHelperService,
readonly halResourceService:HalResourceService, readonly halResourceService:HalResourceService,
readonly globalSearchService:GlobalSearchService, readonly globalSearchService:GlobalSearchService,
readonly currentProjectService:CurrentProjectService, readonly currentProjectService:CurrentProjectService,
readonly deviceService:DeviceService, readonly deviceService:DeviceService,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
readonly halNotification:HalResourceNotificationService, readonly halNotification:HalResourceNotificationService,
readonly ngZone:NgZone) { readonly ngZone:NgZone) {
} }
ngAfterViewInit():void { ngAfterViewInit():void {
@ -175,7 +177,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
public redirectToWp(id:string, event:MouseEvent) { public redirectToWp(id:string, event:MouseEvent) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
if (LinkHandling.isClickedWithModifier(event)) { if (isClickedWithModifier(event)) {
return true; return true;
} }
@ -185,7 +187,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
} }
public wpPath(id:string) { public wpPath(id:string) {
return this.PathHelperService.workPackagePath(id); return this.pathHelperService.workPackagePath(id);
} }
public search($event:any) { public search($event:any) {
@ -195,7 +197,6 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
// close menu when input field is empty // close menu when input field is empty
public openCloseMenu(searchedTerm:string) { public openCloseMenu(searchedTerm:string) {
this.ngSelectComponent.ngSelectInstance.isOpen = (searchedTerm.trim().length > 0); this.ngSelectComponent.ngSelectInstance.isOpen = (searchedTerm.trim().length > 0);
} }
@ -209,7 +210,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
if (!this.deviceService.isMobile) { if (!this.deviceService.isMobile) {
this.expanded = (this.ngSelectComponent.ngSelectInstance.searchTerm !== null && this.ngSelectComponent.ngSelectInstance.searchTerm.length > 0); this.expanded = (this.ngSelectComponent.ngSelectInstance.searchTerm !== null && this.ngSelectComponent.ngSelectInstance.searchTerm.length > 0);
this.ngSelectComponent.ngSelectInstance.isOpen = false; this.ngSelectComponent.ngSelectInstance.isOpen = false;
this.selectedItem =null; this.selectedItem = null;
this.toggleTopMenuClass(); this.toggleTopMenuClass();
} }
} }
@ -270,31 +271,27 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
// Hide highlighting of ng-option // Hide highlighting of ng-option
this.markable = false; this.markable = false;
const hashFreeQuery = this.queryWithoutHash(query); const hashFreeQuery = this.queryWithoutHash(query);
this.isLoading = true;
this.isLoading = true
return this return this
.fetchSearchResults(hashFreeQuery, hashFreeQuery !== query) .fetchSearchResults(hashFreeQuery, hashFreeQuery !== query)
.get() .get()
.pipe( .pipe(
map((collection) => { map((collection) => this.searchResultsToOptions(collection.elements, hashFreeQuery)),
return this.searchResultsToOptions(collection.elements, hashFreeQuery);
}),
tap(() => { tap(() => {
this.isLoading = false; this.isLoading = false;
this.setMarkedOption();}) this.setMarkedOption();
}),
); );
} }
// Remove ID marker # when searching for #<number> // Remove ID marker # when searching for #<number>
private queryWithoutHash(query:string) { private queryWithoutHash(query:string) {
if (query.match(/^#(\d+)/)) { if (/^#(\d+)/.exec(query)) {
return query.substr(1); return query.substr(1);
} else {
return query;
} }
return query;
} }
private fetchSearchResults(query:string, idOnly:boolean) { private fetchSearchResults(query:string, idOnly:boolean) {
@ -306,7 +303,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
private searchResultsToOptions(results:WorkPackageResource[], query:string) { private searchResultsToOptions(results:WorkPackageResource[], query:string) {
const searchItems = results.map((wp) => { const searchItems = results.map((wp) => {
const item = { const item = {
id: wp.id!, id: wp.id!,
subject: wp.subject, subject: wp.subject,
status: wp.status.name, status: wp.status.name,
@ -314,7 +311,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
href: wp.href, href: wp.href,
project: wp.project.name, project: wp.project.name,
author: wp.author, author: wp.author,
type: wp.type type: wp.type,
} as SearchResultItem; } as SearchResultItem;
// If we have a direct hit, we choose it to be the selected element. // If we have a direct hit, we choose it to be the selected element.
if (query === wp.id!.toString()) { if (query === wp.id!.toString()) {
@ -347,9 +344,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
} }
searchOptions.push('all_projects'); searchOptions.push('all_projects');
return searchOptions.map((suggestion:string) => { return searchOptions.map((suggestion:string) => ({ projectScope: suggestion, text: this.text[suggestion] }));
return { projectScope: suggestion, text: this.text[suggestion] };
});
} }
/* /*
@ -407,6 +402,8 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
this.submitNonEmptySearch(); this.submitNonEmptySearch();
break; break;
} }
default: // Do nothing
break;
} }
} }
@ -415,9 +412,9 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
if (this.currentValue.length > 0) { if (this.currentValue.length > 0) {
this.ngSelectComponent.ngSelectInstance.close(); this.ngSelectComponent.ngSelectInstance.close();
// Work package results can update without page reload. // Work package results can update without page reload.
if (!forcePageLoad && if (!forcePageLoad
this.globalSearchService.isAfterSearch() && && this.globalSearchService.isAfterSearch()
this.globalSearchService.currentTab === 'work_packages') { && this.globalSearchService.currentTab === 'work_packages') {
window.history window.history
.replaceState({}, .replaceState({},
`${I18n.t('global_search.search')}: ${this.ngSelectComponent.ngSelectInstance.searchTerm}`, `${I18n.t('global_search.search')}: ${this.ngSelectComponent.ngSelectInstance.searchTerm}`,
@ -450,5 +447,3 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
jQuery('.op-app-header').toggleClass('op-app-header_search-open', this.expanded); jQuery('.op-app-header').toggleClass('op-app-header_search-open', this.expanded);
} }
} }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -27,15 +27,15 @@
//++ //++
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { OpenprojectWorkPackagesModule } from "core-app/features/work-packages/openproject-work-packages.module"; import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { GlobalSearchInputComponent } from "core-app/core/global_search/input/global-search-input.component"; import { GlobalSearchInputComponent } from 'core-app/core/global_search/input/global-search-input.component';
import { GlobalSearchWorkPackagesComponent } from "core-app/core/global_search/global-search-work-packages.component"; import { GlobalSearchWorkPackagesComponent } from 'core-app/core/global_search/global-search-work-packages.component';
import { GlobalSearchTabsComponent } from "core-app/core/global_search/tabs/global-search-tabs.component"; import { GlobalSearchTabsComponent } from 'core-app/core/global_search/tabs/global-search-tabs.component';
import { GlobalSearchTitleComponent } from "core-app/core/global_search/title/global-search-title.component"; import { GlobalSearchTitleComponent } from 'core-app/core/global_search/title/global-search-title.component';
import { GlobalSearchService } from "core-app/core/global_search/services/global-search.service"; import { GlobalSearchService } from 'core-app/core/global_search/services/global-search.service';
import { GlobalSearchWorkPackagesEntryComponent } from "core-app/core/global_search/global-search-work-packages-entry.component"; import { GlobalSearchWorkPackagesEntryComponent } from 'core-app/core/global_search/global-search-work-packages-entry.component';
import { OpenprojectAutocompleterModule } from "core-app/shared/components/autocompleter/openproject-autocompleter.module"; import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autocompleter/openproject-autocompleter.module';
import { OPSharedModule } from "core-app/shared/shared.module"; import { OPSharedModule } from 'core-app/shared/shared.module';
@NgModule({ @NgModule({
imports: [ imports: [
@ -52,7 +52,6 @@ import { OPSharedModule } from "core-app/shared/shared.module";
GlobalSearchWorkPackagesComponent, GlobalSearchWorkPackagesComponent,
GlobalSearchTabsComponent, GlobalSearchTabsComponent,
GlobalSearchTitleComponent, GlobalSearchTitleComponent,
] ],
}) })
export class OpenprojectGlobalSearchModule { } export class OpenprojectGlobalSearchModule { }

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,17 +26,17 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
/*jshint expr: true*/ /* jshint expr: true */
import { CurrentProjectService } from "core-app/core/current-project/current-project.service"; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { GlobalSearchService } from "core-app/core/global_search/services/global-search.service"; import { GlobalSearchService } from 'core-app/core/global_search/services/global-search.service';
import { I18nService } from "core-app/core/i18n/i18n.service"; import { I18nService } from 'core-app/core/i18n/i18n.service';
import { TestBed, waitForAsync } from "@angular/core/testing"; import { TestBed, waitForAsync } from '@angular/core/testing';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { States } from "core-app/core/states/states.service"; import { States } from 'core-app/core/states/states.service';
import { APIV3Service } from "core-app/core/apiv3/api-v3.service"; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
describe('Global search service', function() { describe('Global search service', () => {
let service:GlobalSearchService; let service:GlobalSearchService;
let CurrentProject:CurrentProjectService; let CurrentProject:CurrentProjectService;
let CurrentProjectSpy; let CurrentProjectSpy;
@ -51,13 +51,13 @@ describe('Global search service', function() {
APIV3Service, APIV3Service,
CurrentProjectService, CurrentProjectService,
GlobalSearchService, GlobalSearchService,
] ],
}) })
.compileComponents() .compileComponents()
.then(() => { .then(() => {
CurrentProject = TestBed.inject(CurrentProjectService); CurrentProject = TestBed.inject(CurrentProjectService);
service = TestBed.inject(GlobalSearchService); service = TestBed.inject(GlobalSearchService);
}); });
})); }));
describe('outside a project', () => { describe('outside a project', () => {

@ -1,4 +1,4 @@
//-- copyright // -- copyright
// OpenProject is an open source project management software. // OpenProject is an open source project management software.
// Copyright (C) 2012-2021 the OpenProject GmbH // Copyright (C) 2012-2021 the OpenProject GmbH
// //
@ -26,37 +26,42 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
import { Injectable , Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { I18nService } from "core-app/core/i18n/i18n.service"; import { I18nService } from 'core-app/core/i18n/i18n.service';
import { CurrentProjectService } from "core-app/core/current-project/current-project.service"; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { PathHelperService } from "core-app/core/path-helper/path-helper.service"; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
@Injectable() @Injectable()
export class GlobalSearchService { export class GlobalSearchService {
private _searchTerm = new BehaviorSubject<string>(''); private _searchTerm = new BehaviorSubject<string>('');
public searchTerm$ = this._searchTerm.asObservable(); public searchTerm$ = this._searchTerm.asObservable();
// Default selected tab is Work Packages // Default selected tab is Work Packages
private _currentTab = new BehaviorSubject<any>('work_packages'); private _currentTab = new BehaviorSubject<any>('work_packages');
public currentTab$ = this._currentTab.asObservable(); public currentTab$ = this._currentTab.asObservable();
// Default project scope is "this project and all subprojets" // Default project scope is "this project and all subprojets"
private _projectScope = new BehaviorSubject<any>(''); private _projectScope = new BehaviorSubject<any>('');
public projectScope$ = this._projectScope.asObservable(); public projectScope$ = this._projectScope.asObservable();
private _tabs = new BehaviorSubject<any>([]); private _tabs = new BehaviorSubject<any>([]);
public tabs$ = this._tabs.asObservable(); public tabs$ = this._tabs.asObservable();
// Sometimes we need to be able to hide the search results altogether, i.e. while expecting a full page reload. // Sometimes we need to be able to hide the search results altogether, i.e. while expecting a full page reload.
private _resultsHidden = new BehaviorSubject<any>(false); private _resultsHidden = new BehaviorSubject<any>(false);
public resultsHidden$ = this._resultsHidden.asObservable(); public resultsHidden$ = this._resultsHidden.asObservable();
constructor(protected I18n:I18nService, constructor(protected I18n:I18nService,
protected injector:Injector, protected injector:Injector,
protected PathHelper:PathHelperService, protected PathHelper:PathHelperService,
protected currentProjectService:CurrentProjectService) { protected currentProjectService:CurrentProjectService) {
this.initialize(); this.initialize();
} }
@ -81,10 +86,10 @@ export class GlobalSearchService {
} }
} }
private loadGonData():{available_search_types:string[], private loadGonData():{ available_search_types:string[],
search_term:string, search_term:string,
project_scope:string, project_scope:string,
current_tab:string}|null { current_tab:string }|null {
try { try {
return (window as any).gon.global_search; return (window as any).gon.global_search;
} catch (e) { } catch (e) {
@ -101,7 +106,7 @@ export class GlobalSearchService {
if (this.currentProjectService.path && this.projectScope !== 'all') { if (this.currentProjectService.path && this.projectScope !== 'all') {
searchPath = this.currentProjectService.path; searchPath = this.currentProjectService.path;
} }
searchPath = searchPath + `/search?${this.searchQueryParams()}`; searchPath += `/search?${this.searchQueryParams()}`;
return searchPath; return searchPath;
} }

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

Loading…
Cancel
Save