diff --git a/config/locales/crowdin/js-ru.yml b/config/locales/crowdin/js-ru.yml
index 3e2a0b3324a..5b084f18b51 100644
--- a/config/locales/crowdin/js-ru.yml
+++ b/config/locales/crowdin/js-ru.yml
@@ -289,7 +289,7 @@ ru:
bim:
learn_about_link: https://www.openproject.org/openproject-11-3-release
current_new_feature_html: >
- The release contains various new features and improvements:
- BIM: Improved performance of the IFC model viewer.
- BIM: The IFC viewer now supports double precision models, which is great for large construction sites such as bridges, roads etc.
- BIM: Toggle button for switching between perspective and orthogonal mode.
- BIM: A menu for editing, deleting or flipping section planes.
- New button in header navigation to create projects, users and work packages.
- A new invite modal for users, groups and placeholder users allows to easily add or invite new users and assign them to work packages.
- API v3 extensions for groups and more, i.e. create, read, update and delete groups and group members through the API.
- Multi-selection for project custom fields of type list.
- Creation of backup from the web interface.
+ Релиз содержит различные новые функции и улучшения:
- BIM: Улучшенная производительность просмотра моделей IFC.
- BIM: Средство просмотра IFC теперь поддерживает двойную точность моделей, что отлично подходит для больших участков строительства, таких как мосты, дороги и т.д.
- BIM: Переключить кнопку для переключения между перспективным и ортогональным режимом.
- BIM: Меню для редактирования, удаления или отражения плоскостей разделов.
- Новая кнопка в навигации по заголовкам для создания проектов, пользователей и пакетов работ.
- Новое приглашение для пользователей, группы и заполнители позволяют легко добавлять или приглашать новых пользователей и назначать их к рабочим пакетам.
- расширения API v3 для групп и более, то есть создание, чтение, обновление и удаление групп и членов групп через API.
- Мульти-выбор пользовательских полей проекта списка типов.
- Создание резервной копии из веб-интерфейса.
label_activate: "Активировать"
label_add_column_after: "Добавить столбец после"
label_add_column_before: "Добавить столбец перед"
diff --git a/config/puma.rb b/config/puma.rb
index 9ba804fbd1a..3c434920b5e 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -21,7 +21,7 @@ environment ENV.fetch("RAILS_ENV") { "development" }
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
-workers ENV.fetch("OPENPROJECT_WEB_WORKERS") { 1 }.to_i
+workers ENV.fetch("OPENPROJECT_WEB_WORKERS") { 0 }.to_i
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
diff --git a/frontend/src/app/core/apiv3/endpoints/users/apiv3-users-paths.ts b/frontend/src/app/core/apiv3/endpoints/users/apiv3-users-paths.ts
index 6d9aed4a38a..d5926b8a6b7 100644
--- a/frontend/src/app/core/apiv3/endpoints/users/apiv3-users-paths.ts
+++ b/frontend/src/app/core/apiv3/endpoints/users/apiv3-users-paths.ts
@@ -31,6 +31,7 @@ import { APIv3UserPaths } from "core-app/core/apiv3/endpoints/users/apiv3-user-p
import { Observable } from "rxjs";
import { UserResource } from "core-app/features/hal/resources/user-resource";
import { APIV3Service } from "core-app/core/apiv3/api-v3.service";
+import { APIv3FormResource } from "core-app/core/apiv3/forms/apiv3-form-resource";
export class Apiv3UsersPaths extends APIv3ResourceCollection {
constructor(protected apiRoot:APIV3Service,
@@ -43,6 +44,9 @@ export class Apiv3UsersPaths extends APIv3ResourceCollection actions.reduce(
(acc, action) => {
- return acc && !!capabilities.find(cap => cap.action.href === `/api/v3/actions/${action}`);
+ return acc && !!capabilities.find(cap => cap.action.href.endsWith(`/api/v3/actions/${action}`));
},
capabilities.length > 0,
)),
@@ -168,7 +168,7 @@ export class CurrentUserService {
const actionsToFilter = _.castArray(actions);
return this.capabilitiesForContext$(contextId).pipe(
map((capabilities) => capabilities.reduce(
- (acc, cap) => acc || !!actionsToFilter.find(action => cap.action.href === `/api/v3/actions/${action}`),
+ (acc, cap) => acc || !!actionsToFilter.find(action => cap.action.href.endsWith(`/api/v3/actions/${action}`)),
false,
)),
distinctUntilChanged(),
diff --git a/frontend/src/app/features/invite-user-modal/invite-user-modal.module.ts b/frontend/src/app/features/invite-user-modal/invite-user-modal.module.ts
index cbf09245a52..162d5d71366 100644
--- a/frontend/src/app/features/invite-user-modal/invite-user-modal.module.ts
+++ b/frontend/src/app/features/invite-user-modal/invite-user-modal.module.ts
@@ -25,7 +25,7 @@ export function initializeServices(injector:Injector) {
return function () {
const inviteUserAugmentService = injector.get(OpInviteUserModalAugmentService);
inviteUserAugmentService.setupListener();
- }
+ };
}
@NgModule({
diff --git a/frontend/src/app/features/invite-user-modal/principal/principal.component.html b/frontend/src/app/features/invite-user-modal/principal/principal.component.html
index af176122c74..4ea7f30d922 100644
--- a/frontend/src/app/features/invite-user-modal/principal/principal.component.html
+++ b/frontend/src/app/features/invite-user-modal/principal/principal.component.html
@@ -55,7 +55,7 @@
*ngIf="isNewPrincipal && type === PrincipalType.User && userDynamicFieldConfig.schema"
[dynamicFormGroup]="dynamicFieldsControl"
[settings]="userDynamicFieldConfig"
- [resourcePath]="pathHelper.usersPath()"
+ [formUrl]="apiV3Service.users.form.path"
[handleSubmit]="false"
>
diff --git a/frontend/src/app/features/invite-user-modal/principal/principal.component.ts b/frontend/src/app/features/invite-user-modal/principal/principal.component.ts
index 8c216e78096..433381a808d 100644
--- a/frontend/src/app/features/invite-user-modal/principal/principal.component.ts
+++ b/frontend/src/app/features/invite-user-modal/principal/principal.component.ts
@@ -1,27 +1,17 @@
-import {
- Component,
- OnInit,
- Input,
- Output,
- EventEmitter,
- ViewChild,
- ChangeDetectorRef,
-} from '@angular/core';
+import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild, } from '@angular/core';
import { HttpClient } from "@angular/common/http";
-import {
- FormGroup,
- FormControl,
- Validators,
-} from '@angular/forms';
-import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
+import { FormControl, FormGroup, Validators, } from '@angular/forms';
import { I18nService } from "core-app/core/i18n/i18n.service";
import { HalResource } from "core-app/features/hal/resources/hal-resource";
import { PrincipalData, PrincipalLike } from "core-app/shared/components/principal/principal-types";
import { ProjectResource } from "core-app/features/hal/resources/project-resource";
import { DynamicFormComponent } from "core-app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component"
import { PrincipalType } from '../invite-user.component';
+import { take } from 'rxjs/internal/operators/take';
+import { map } from 'rxjs/operators';
+import { APIV3Service } from "core-app/core/apiv3/api-v3.service";
-function extractCustomFieldsFromSchema(schema: IOPFormSettings['_embedded']['schema']) {
+function extractCustomFieldsFromSchema(schema:IOPFormSettings['_embedded']['schema']) {
return Object.keys(schema)
.reduce((fields, name) => {
if (name.startsWith('customField') && schema[name].required) {
@@ -49,7 +39,7 @@ export class PrincipalComponent implements OnInit {
@Output() save = new EventEmitter<{ principalData:PrincipalData, isAlreadyMember:boolean }>();
@Output() back = new EventEmitter();
- @ViewChild(DynamicFormComponent) dynamicForm: DynamicFormComponent;
+ @ViewChild(DynamicFormComponent) dynamicForm:DynamicFormComponent;
public PrincipalType = PrincipalType;
@@ -77,13 +67,13 @@ export class PrincipalComponent implements OnInit {
};
public principalForm = new FormGroup({
- principal: new FormControl(null, [ Validators.required ]),
+ principal: new FormControl(null, [Validators.required]),
userDynamicFields: new FormGroup({}),
});
- public userDynamicFieldConfig: {
- payload: IOPFormSettings['_embedded']['payload']|null,
- schema: IOPFormSettings['_embedded']['schema']|null,
+ public userDynamicFieldConfig:{
+ payload:IOPFormSettings['_embedded']['payload']|null,
+ schema:IOPFormSettings['_embedded']['schema']|null,
} = {
payload: null,
schema: null,
@@ -101,7 +91,7 @@ export class PrincipalComponent implements OnInit {
return this.principalForm.get('userDynamicFields');
}
- get customFields():{[key:string]:any} {
+ get customFields():{ [key:string]:any } {
return this.dynamicFieldsControl?.value;
}
@@ -128,17 +118,27 @@ export class PrincipalComponent implements OnInit {
constructor(
readonly I18n:I18nService,
readonly httpClient:HttpClient,
- readonly pathHelper:PathHelperService,
- readonly cdRef: ChangeDetectorRef,
- ) {}
+ readonly apiV3Service:APIV3Service,
+ readonly cdRef:ChangeDetectorRef,
+ ) {
+ }
ngOnInit() {
this.principalControl?.setValue(this.principalData.principal);
if (this.type === PrincipalType.User) {
const payload = this.isNewPrincipal ? this.principalData.customFields : {};
- this.httpClient
- .post('/api/v3/users/form', payload, { withCredentials: true, responseType: 'json' })
+ this
+ .apiV3Service
+ .users
+ .form
+ .post(payload)
+ .pipe(
+ take(1),
+ // The subsequent code expects to not work with a HalResource but rather with the raw
+ // api response.
+ map(formResource => formResource.$source)
+ )
.subscribe((formConfig) => {
this.userDynamicFieldConfig.schema = extractCustomFieldsFromSchema(formConfig._embedded?.schema);
this.userDynamicFieldConfig.payload = formConfig._embedded?.payload;
@@ -177,7 +177,7 @@ export class PrincipalComponent implements OnInit {
_links: Object.keys(links).reduce((cfs, name) => ({
...cfs,
[name]: Array.isArray(links[name])
- ? links[name].map((opt: any) => opt._links ? opt._links.self : opt)
+ ? links[name].map((opt:any) => opt._links ? opt._links.self : opt)
: (links[name]._links ? links[name]._links.self : links[name])
}), {}),
};
diff --git a/frontend/src/app/features/projects/components/copy-project/copy-project.component.ts b/frontend/src/app/features/projects/components/copy-project/copy-project.component.ts
index e386305c0fa..813fdf8fbc3 100644
--- a/frontend/src/app/features/projects/components/copy-project/copy-project.component.ts
+++ b/frontend/src/app/features/projects/components/copy-project/copy-project.component.ts
@@ -1,17 +1,18 @@
-import {Component, OnInit} from '@angular/core';
-import {StateService, UIRouterGlobals} from "@uirouter/core";
-import {UntilDestroyedMixin} from "core-app/shared/helpers/angular/until-destroyed.mixin";
-import {PathHelperService} from "core-app/core/path-helper/path-helper.service";
-import {HalSource} from "core-app/features/hal/resources/hal-resource";
+import { Component, OnInit } from '@angular/core';
+import { StateService } from "@uirouter/core";
+import { UntilDestroyedMixin } from "core-app/shared/helpers/angular/until-destroyed.mixin";
+import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
+import { HalSource } from "core-app/features/hal/resources/hal-resource";
import {
IDynamicFieldGroupConfig,
IOPFormlyFieldSettings,
IOPFormlyTemplateOptions,
} from "core-app/shared/components/dynamic-forms/typings";
-import {I18nService} from "core-app/core/i18n/i18n.service";
-import {APIV3Service} from "core-app/core/apiv3/api-v3.service";
-import {JobStatusModal} from "core-app/features/job-status/job-status-modal/job-status.modal";
-import {OpModalService} from "core-app/shared/components/modal/modal.service";
+import { I18nService } from "core-app/core/i18n/i18n.service";
+import { APIV3Service } from "core-app/core/apiv3/api-v3.service";
+import { JobStatusModal } from "core-app/features/job-status/job-status-modal/job-status.modal";
+import { OpModalService } from "core-app/shared/components/modal/modal.service";
+import { CurrentProjectService } from "core-app/core/current-project/current-project.service";
@Component({
selector: 'op-copy-project',
@@ -31,11 +32,11 @@ export class CopyProjectComponent extends UntilDestroyedMixin implements OnInit
text = {
advancedSettingsLabel: this.I18n.t("js.forms.advanced_settings"),
copySettingsLabel: this.I18n.t("js.project.copy.copy_options"),
- }
+ };
constructor(
private apiV3Service:APIV3Service,
- private uIRouterGlobals:UIRouterGlobals,
+ private currentProjectService:CurrentProjectService,
private pathHelperService:PathHelperService,
private modalService:OpModalService,
private $state:StateService,
@@ -45,7 +46,7 @@ export class CopyProjectComponent extends UntilDestroyedMixin implements OnInit
}
ngOnInit():void {
- this.formUrl = this.apiV3Service.projects.id(this.uIRouterGlobals.params.projectPath).copy.form.path;
+ this.formUrl = this.apiV3Service.projects.id(this.currentProjectService.id!).copy.form.path;
this.fieldGroups = [
{
name: this.text.advancedSettingsLabel,
diff --git a/frontend/src/app/features/projects/components/new-project/new-project.component.ts b/frontend/src/app/features/projects/components/new-project/new-project.component.ts
index 2f1750863c5..e8486e3b04d 100644
--- a/frontend/src/app/features/projects/components/new-project/new-project.component.ts
+++ b/frontend/src/app/features/projects/components/new-project/new-project.component.ts
@@ -80,7 +80,7 @@ export class NewProjectComponent extends UntilDestroyedMixin implements OnInit {
}
ngOnInit():void {
- this.resourcePath = this.pathHelperService.projectsPath();
+ this.resourcePath = this.apiV3Service.projects.path;
this.fieldGroups = [{
name: this.text.advancedSettingsLabel,
fieldsFilter: (field) => !['name', 'parent'].includes(field.templateOptions?.property!) &&
diff --git a/frontend/src/app/features/projects/components/projects/projects.component.html b/frontend/src/app/features/projects/components/projects/projects.component.html
index 298deb5b645..6943dfd1dff 100644
--- a/frontend/src/app/features/projects/components/projects/projects.component.html
+++ b/frontend/src/app/features/projects/components/projects/projects.component.html
@@ -1,6 +1,6 @@
\ No newline at end of file
diff --git a/frontend/src/app/features/projects/components/projects/projects.component.ts b/frontend/src/app/features/projects/components/projects/projects.component.ts
index 25b04efc430..dbf56e5d7e8 100644
--- a/frontend/src/app/features/projects/components/projects/projects.component.ts
+++ b/frontend/src/app/features/projects/components/projects/projects.component.ts
@@ -1,8 +1,9 @@
import { Component, OnInit } from '@angular/core';
-import { StateService, UIRouterGlobals } from "@uirouter/core";
+import { StateService } from "@uirouter/core";
import { UntilDestroyedMixin } from "core-app/shared/helpers/angular/until-destroyed.mixin";
import { PathHelperService } from "core-app/core/path-helper/path-helper.service";
import { IOPFormlyFieldSettings } from "core-app/shared/components/dynamic-forms/typings";
+import { CurrentProjectService } from "core-app/core/current-project/current-project.service";
@Component({
selector: 'app-projects',
@@ -10,23 +11,22 @@ import { IOPFormlyFieldSettings } from "core-app/shared/components/dynamic-forms
styleUrls: ['./projects.component.scss']
})
export class ProjectsComponent extends UntilDestroyedMixin implements OnInit {
- resourceId:string;
projectsPath:string;
+ formMethod = 'patch';
text:{ [key:string]:string };
dynamicFieldsSettingsPipe:(dynamicFieldsSettings:IOPFormlyFieldSettings[]) => IOPFormlyFieldSettings[];
hiddenFields = ['identifier', 'active'];
constructor(
- private _uIRouterGlobals:UIRouterGlobals,
private _pathHelperService:PathHelperService,
private _$state:StateService,
+ private _currentProjectService:CurrentProjectService,
) {
super();
}
ngOnInit():void {
- this.projectsPath = this._pathHelperService.projectsPath();
- this.resourceId = this._uIRouterGlobals.params.projectPath;
+ this.projectsPath = this._currentProjectService.apiv3Path!;
this.dynamicFieldsSettingsPipe = (dynamicFieldsSettings) => {
return dynamicFieldsSettings
.reduce((formattedDynamicFieldsSettings:IOPFormlyFieldSettings[], dynamicFormField) => {
@@ -42,13 +42,6 @@ export class ProjectsComponent extends UntilDestroyedMixin implements OnInit {
}
}
- onSubmitted(formResource:HalSource) {
- // TODO: Filter out if this.resourceId === 'new'?
- if (!this.resourceId) {
- this._$state.go('.', { ...this._$state.params, projectPath: formResource.identifier });
- }
- }
-
private isFieldHidden(name:string|undefined) {
return this.hiddenFields.includes(name || '');
}
diff --git a/frontend/src/app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component.ts b/frontend/src/app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component.ts
index 9c2962a4526..38cc27d35df 100644
--- a/frontend/src/app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component.ts
+++ b/frontend/src/app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component.ts
@@ -295,7 +295,7 @@ export class DynamicFormComponent extends UntilDestroyedMixin implements OnChang
}
if (resourcePath) {
- return `${this._pathHelperService.api.v3.apiV3Base}${resourcePath}`;
+ return resourcePath;
}
return;
@@ -349,7 +349,7 @@ export class DynamicFormComponent extends UntilDestroyedMixin implements OnChang
submit_message = `${title || ''} ${this.text.job_started}`;
} else {
- submit_message = this.resourceId ? this.text.successful_update : this.text.successful_create;
+ submit_message = this.formHttpMethod === 'patch' ? this.text.successful_update : this.text.successful_create;
}
this._notificationsService.addSuccess(submit_message);
diff --git a/lib/open_project/logging/sentry_logger.rb b/lib/open_project/logging/sentry_logger.rb
index 27af233a7c7..88d9511476d 100644
--- a/lib/open_project/logging/sentry_logger.rb
+++ b/lib/open_project/logging/sentry_logger.rb
@@ -6,7 +6,7 @@ module OpenProject
# Capture a message to sentry
def log(message, log_context = {})
Sentry.configure_scope do |sentry_scope|
- build_sentry_context(sentry_scope, log_context)
+ build_sentry_context(sentry_scope, log_context.to_h)
Sentry.capture_message(message, level: sentry_level(log_context[:level]))
end
end