mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge release/11.3 into dev
This commit is contained in:
@@ -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: <br> <ul class="%{list_styling_class}"> <li>BIM: Improved performance of the IFC model viewer.</li> <li>BIM: The IFC viewer now supports double precision models, which is great for large construction sites such as bridges, roads etc.</li> <li>BIM: Toggle button for switching between perspective and orthogonal mode.</li> <li>BIM: A menu for editing, deleting or flipping section planes.</li> <li>New button in header navigation to create projects, users and work packages.</li> <li>A new invite modal for users, groups and placeholder users allows to easily add or invite new users and assign them to work packages.</li> <li>API v3 extensions for groups and more, i.e. create, read, update and delete groups and group members through the API.</li> <li>Multi-selection for project custom fields of type list.</li> <li>Creation of backup from the web interface.</li> </ul>
|
||||
Релиз содержит различные новые функции и улучшения: <br> <ul class="%{list_styling_class}"> <li>BIM: Улучшенная производительность просмотра моделей IFC.</li> <li>BIM: Средство просмотра IFC теперь поддерживает двойную точность моделей, что отлично подходит для больших участков строительства, таких как мосты, дороги и т.д.</li> <li>BIM: Переключить кнопку для переключения между перспективным и ортогональным режимом.</li> <li>BIM: Меню для редактирования, удаления или отражения плоскостей разделов.</li> <li>Новая кнопка в навигации по заголовкам для создания проектов, пользователей и пакетов работ.</li> <li>Новое приглашение для пользователей, группы и заполнители позволяют легко добавлять или приглашать новых пользователей и назначать их к рабочим пакетам.</li> <li>расширения API v3 для групп и более, то есть создание, чтение, обновление и удаление групп и членов групп через API.</li> <li>Мульти-выбор пользовательских полей проекта списка типов.</li> <li>Создание резервной копии из веб-интерфейса.</li> </ul>
|
||||
label_activate: "Активировать"
|
||||
label_add_column_after: "Добавить столбец после"
|
||||
label_add_column_before: "Добавить столбец перед"
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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<UserResource, APIv3UserPaths> {
|
||||
constructor(protected apiRoot:APIV3Service,
|
||||
@@ -43,6 +44,9 @@ export class Apiv3UsersPaths extends APIv3ResourceCollection<UserResource, APIv3
|
||||
// /api/v3/users/me
|
||||
public readonly me = this.path + '/me';
|
||||
|
||||
// /api/v3/users/form
|
||||
public readonly form = this.subResource('form', APIv3FormResource);
|
||||
|
||||
/**
|
||||
* Create a new UserResource
|
||||
*
|
||||
|
||||
@@ -153,7 +153,7 @@ export class CurrentUserService {
|
||||
return this.capabilitiesForContext$(contextId).pipe(
|
||||
map((capabilities) => 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(),
|
||||
|
||||
@@ -25,7 +25,7 @@ export function initializeServices(injector:Injector) {
|
||||
return function () {
|
||||
const inviteUserAugmentService = injector.get(OpInviteUserModalAugmentService);
|
||||
inviteUserAugmentService.setupListener();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -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"
|
||||
></op-dynamic-form>
|
||||
</div>
|
||||
|
||||
@@ -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<IOPFormSettings>('/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])
|
||||
}), {}),
|
||||
};
|
||||
|
||||
+13
-12
@@ -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,
|
||||
|
||||
@@ -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!) &&
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<op-dynamic-form
|
||||
[resourceId]="resourceId"
|
||||
[resourcePath]="projectsPath"
|
||||
[formHttpMethod]="formMethod"
|
||||
[fieldsSettingsPipe]="dynamicFieldsSettingsPipe"
|
||||
helpTextAttributeScope="Project"
|
||||
></op-dynamic-form>
|
||||
@@ -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 || '');
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user