From fe7e59c706d09a19665082a3732660e3a3a0fc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 5 Aug 2020 08:38:36 +0200 Subject: [PATCH 01/29] Debounced filtering of potential board column candidates --- .../api/api-v3/api-v3-filter-builder.ts | 14 +++++- .../helpers/rxjs/debounced-input-switchmap.ts | 3 +- .../add-list-modal.component.ts | 31 ++++++------ .../board/add-list-modal/add-list-modal.html | 5 +- .../assignee/assignee-action.service.ts | 9 ++-- .../board-actions/board-action.service.ts | 42 ++++++++-------- .../cached-board-action.service.ts | 48 ++++++++++++++++++ .../status/status-action.service.ts | 10 ++-- .../subproject/subproject-action.service.ts | 18 ++----- .../subtasks/board-subtasks-action.service.ts | 49 +++++++++++++++++++ .../version/version-action.service.ts | 9 ++-- .../board-list-container.component.ts | 2 +- .../boards-root/boards-root.component.ts | 15 +++--- .../create-autocompleter.component.html | 1 + modules/boards/config/locales/js-en.yml | 1 + 15 files changed, 181 insertions(+), 76 deletions(-) create mode 100644 frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts create mode 100644 frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts diff --git a/frontend/src/app/components/api/api-v3/api-v3-filter-builder.ts b/frontend/src/app/components/api/api-v3/api-v3-filter-builder.ts index 64b38cf6a9..e9ff8ad34e 100644 --- a/frontend/src/app/components/api/api-v3/api-v3-filter-builder.ts +++ b/frontend/src/app/components/api/api-v3/api-v3-filter-builder.ts @@ -27,10 +27,12 @@ //++ export type FilterOperator = '='|'!*'|'!'|'~'|'o'|'>t-'|'<>d'|'**'|'ow' ; +export const FalseValue = ['f']; +export const TrueValue = ['t']; export interface ApiV3FilterValue { operator:FilterOperator; - values:any; + values:unknown[]; } export interface ApiV3Filter { @@ -43,7 +45,15 @@ export class ApiV3FilterBuilder { private filterMap:ApiV3FilterObject = {}; - public add(name:string, operator:FilterOperator, values:any):this { + public add(name:string, operator:FilterOperator, values:unknown[]|boolean):this { + if (values === true) { + values = TrueValue; + } + + if (values === false) { + values = FalseValue; + } + this.filterMap[name] = { operator: operator, values: values diff --git a/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts b/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts index 6b2294d0e1..a87559b585 100644 --- a/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts +++ b/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts @@ -3,7 +3,7 @@ import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import { catchError, debounceTime, - distinctUntilChanged, + distinctUntilChanged, filter, switchMap, takeUntil, tap @@ -47,6 +47,7 @@ export class DebouncedRequestSwitchmap { this.output$ = concat( of([]), this.input$.pipe( + filter(val => val !== undefined && val !== null), distinctUntilChanged(), debounceTime(debounceMs), tap((val:T) => { diff --git a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts index 3f7f52e901..3df9c41ec0 100644 --- a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts +++ b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts @@ -39,11 +39,21 @@ import {BoardActionService} from "core-app/modules/boards/board/board-actions/bo import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import {AngularTrackingHelpers} from "core-components/angular/tracking-functions"; import {CreateAutocompleterComponent} from "core-app/modules/common/autocomplete/create-autocompleter.component"; +import {of} from "rxjs"; +import {DebouncedRequestSwitchmap, errorNotificationHandler} from "core-app/helpers/rxjs/debounced-input-switchmap"; +import {ValueOption} from "core-app/modules/fields/edit/field-types/select-edit-field.component"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; @Component({ templateUrl: './add-list-modal.html' }) export class AddListModalComponent extends OpModalComponent implements OnInit { + /** Keep a switchmap for search term and loading state */ + public requests = new DebouncedRequestSwitchmap( + (searchTerm:string) => this.actionService.loadAvailable(this.board, this.active, searchTerm), + errorNotificationHandler(this.halNotification) + ); + public showClose:boolean; public confirmed = false; @@ -57,9 +67,6 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { /** Action service used by the board */ public actionService:BoardActionService; - /** Remaining available values */ - public availableValues:HalResource[] = []; - /** The selected attribute */ public selectedAttribute:HalResource|undefined; @@ -71,8 +78,6 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { /* Do not close on outside click (because the select option are appended to the body */ public closeOnOutsideClick = false; - public valuesAvailable:boolean = true; - public warningText:string|undefined; public text:any = { @@ -100,6 +105,7 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { @Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, readonly cdRef:ChangeDetectorRef, readonly boardActions:BoardActionsRegistryService, + readonly halNotification:HalResourceNotificationService, readonly state:StateService, readonly boardService:BoardService, readonly I18n:I18nService) { @@ -115,17 +121,9 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { this.actionService = this.boardActions.get(this.board.actionAttribute!); this.actionService - .getAvailableValues(this.board, this.active) - .then(available => { - this.availableValues = available; - if (this.availableValues.length === 0) { - this.actionService - .warningTextWhenNoOptionsAvailable() - .then((text) => { - this.warningText = text; - this.valuesAvailable = false; - }); - } + .warningTextWhenNoOptionsAvailable() + .then((text) => { + this.warningText = text; }); } @@ -147,7 +145,6 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { } onNewActionCreated(newValue:HalResource) { - this.actionService.cache.clear("New attribute added."); this.selectedAttribute = newValue; this.create(); } diff --git a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html index 2c38fd3786..d756a09c02 100644 --- a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html +++ b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html @@ -19,7 +19,8 @@
-

diff --git a/frontend/src/app/modules/boards/board/board-actions/assignee/assignee-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/assignee/assignee-action.service.ts index e93f215cc8..b24dc90249 100644 --- a/frontend/src/app/modules/boards/board/board-actions/assignee/assignee-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/assignee/assignee-action.service.ts @@ -1,14 +1,15 @@ import {Injectable} from "@angular/core"; import {BoardActionService} from "core-app/modules/boards/board/board-actions/board-action.service"; -import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import {UserResource} from 'core-app/modules/hal/resources/user-resource'; import {CollectionResource} from 'core-app/modules/hal/resources/collection-resource'; import {AssigneeBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/assignee/assignee-board-header.component"; import {ProjectResource} from "core-app/modules/hal/resources/project-resource"; -import {take} from "rxjs/operators"; +import {StatusResource} from "core-app/modules/hal/resources/status-resource"; +import {CachedBoardActionService} from "core-app/modules/boards/board/board-actions/cached-board-action.service"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; @Injectable() -export class BoardAssigneeActionService extends BoardActionService { +export class BoardAssigneeActionService extends CachedBoardActionService { filterName = 'assignee'; text = this.I18n.t('js.boards.board_type.action_by_attribute', @@ -49,7 +50,7 @@ export class BoardAssigneeActionService extends BoardActionService { }); } - protected loadAvailable():Promise { + protected loadUncached():Promise { return this .apiV3Service .projects diff --git a/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts index e9d61026ec..0c341eb694 100644 --- a/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts @@ -12,21 +12,18 @@ import {HalResourceService} from "core-app/modules/hal/services/hal-resource.ser import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; import {CurrentProjectService} from "core-components/projects/current-project.service"; import {Injectable, Injector} from "@angular/core"; -import {take} from "rxjs/operators"; -import {input} from "reactivestates"; +import {map} from "rxjs/operators"; import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; import {IFieldSchema} from "core-app/modules/fields/field.base"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {WorkPackageFilterValues} from "core-components/wp-edit-form/work-package-filter-values"; import {APIV3Service} from "core-app/modules/apiv3/api-v3.service"; import {SchemaCacheService} from "core-components/schemas/schema-cache.service"; +import {Observable} from "rxjs"; @Injectable() export abstract class BoardActionService { - // Cache the available values for the duration of the board - readonly cache = input(); - constructor(readonly injector:Injector, protected boardListsService:BoardListsService, protected I18n:I18nService, @@ -90,8 +87,7 @@ export abstract class BoardActionService { } return this - .withLoadedAvailable() - .then(collection => collection.find(resource => resource.href === href)); + .require(href); } /** @@ -126,12 +122,13 @@ export abstract class BoardActionService { * * @param board The board we're looking at * @param active The active set of values (hrefs or plain values) + * @param matching values matching the given name */ - getAvailableValues(board:Board, active:Set):Promise { + loadAvailable(board:Board, active:Set, matching:string):Observable { return this - .withLoadedAvailable() - .then(results => - results.filter(item => !active.has(item.id!)) + .loadValues(matching) + .pipe( + map(items => items.filter(item => !active.has(item.id!))) ); } @@ -218,17 +215,20 @@ export abstract class BoardActionService { filter.applyDefaultsFromFilters(); } - protected withLoadedAvailable():Promise { - this.cache.putFromPromiseIfPristine(() => this.loadAvailable()); - - return this.cache - .values$() - .pipe(take(1)) - .toPromise(); - } + /** + * Require the given resource to be loaded. + * + * @param href + * @protected + */ + protected abstract require(href:string):Promise; /** - * Load the available values for this action attribute + * Load values optionally matching the given name + * + * @param matching + * @protected */ - protected abstract loadAvailable():Promise; + protected abstract loadValues(matching?:string):Observable; } + diff --git a/frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts new file mode 100644 index 0000000000..7b104896fe --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts @@ -0,0 +1,48 @@ +import {Injectable} from "@angular/core"; +import {BoardActionService} from "core-app/modules/boards/board/board-actions/board-action.service"; +import {input} from "reactivestates"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; +import {Observable} from "rxjs"; +import {filter, map, take} from "rxjs/operators"; + +@Injectable() +export abstract class CachedBoardActionService extends BoardActionService { + protected cache = input([]); + + protected loadValues(matching?:string):Observable { + this + .cache + .putFromPromiseIfPristine(() => this.loadUncached()); + + return this + .cache + .values$() + .pipe( + map(results => { + if (matching) { + return results.filter(resource => resource.name.includes(matching)); + } else { + return results; + } + }), + take(1) + ); + } + + protected require(idOrHref:string):Promise { + this + .cache + .putFromPromiseIfPristine(() => this.loadUncached()); + + return this + .cache + .values$() + .toPromise() + .then(results => { + return results.find(resource => resource.id === idOrHref || resource.href === idOrHref)!; + }); + } + + protected abstract loadUncached():Promise; +} + diff --git a/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts index 4ec3392972..1bb00685e0 100644 --- a/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/status/status-action.service.ts @@ -2,9 +2,10 @@ import {Injectable} from "@angular/core"; import {Board} from "core-app/modules/boards/board/board"; import {StatusResource} from "core-app/modules/hal/resources/status-resource"; import {BoardActionService} from "core-app/modules/boards/board/board-actions/board-action.service"; +import {CachedBoardActionService} from "core-app/modules/boards/board/board-actions/cached-board-action.service"; @Injectable() -export class BoardStatusActionService extends BoardActionService { +export class BoardStatusActionService extends CachedBoardActionService { filterName = 'status'; text = this.I18n.t('js.boards.board_type.action_by_attribute', @@ -20,7 +21,9 @@ export class BoardStatusActionService extends BoardActionService { } public addInitialColumnsForAction(board:Board):Promise { - return this.withLoadedAvailable() + return this + .loadValues() + .toPromise() .then((results) => Promise.all( results.map((status:StatusResource) => { @@ -40,7 +43,7 @@ export class BoardStatusActionService extends BoardActionService { return Promise.resolve(this.I18n.t('js.boards.add_list_modal.warning.status')); } - protected loadAvailable():Promise { + protected loadUncached():Promise { return this .apiV3Service .statuses @@ -48,5 +51,4 @@ export class BoardStatusActionService extends BoardActionService { .toPromise() .then(collection => collection.elements); } - } diff --git a/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts index b914c56439..da6623fb48 100644 --- a/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts @@ -1,18 +1,17 @@ import {Injectable} from "@angular/core"; import {QueryResource} from "core-app/modules/hal/resources/query-resource"; -import {BoardActionService} from "core-app/modules/boards/board/board-actions/board-action.service"; import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import {buildApiV3Filter} from "core-components/api/api-v3/api-v3-filter-builder"; import {UserResource} from 'core-app/modules/hal/resources/user-resource'; import {CollectionResource} from 'core-app/modules/hal/resources/collection-resource'; import {input} from "reactivestates"; -import {take} from "rxjs/operators"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; import {SubprojectBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/subproject/subproject-board-header.component"; +import {CachedBoardActionService} from "core-app/modules/boards/board/board-actions/cached-board-action.service"; @Injectable() -export class BoardSubprojectActionService extends BoardActionService { +export class BoardSubprojectActionService extends CachedBoardActionService { filterName = 'onlySubproject'; text = this.I18n.t('js.boards.board_type.action_by_attribute', @@ -44,22 +43,15 @@ export class BoardSubprojectActionService extends BoardActionService { changeset.setValue('project', { href: href }); } - protected loadAvailable():Promise { + protected loadUncached():Promise { const currentProjectId = this.currentProject.id!; - this.subprojects.putFromPromiseIfPristine(() => - this + return this .apiV3Service .projects .filtered(buildApiV3Filter('ancestor', '=', [currentProjectId])) .get() .toPromise() - .then((collection:CollectionResource) => collection.elements) - ); - - return this.subprojects - .values$() - .pipe(take(1)) - .toPromise(); + .then((collection:CollectionResource) => collection.elements); } } diff --git a/frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts new file mode 100644 index 0000000000..caecd45539 --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts @@ -0,0 +1,49 @@ +import {Injectable} from "@angular/core"; +import {Board} from "core-app/modules/boards/board/board"; +import {StatusResource} from "core-app/modules/hal/resources/status-resource"; +import {BoardActionService} from "core-app/modules/boards/board/board-actions/board-action.service"; +import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; +import {Observable} from "rxjs"; +import {map} from "rxjs/operators"; +import {ApiV3FilterBuilder, buildApiV3Filter, FalseValue} from "core-components/api/api-v3/api-v3-filter-builder"; + +@Injectable() +export class BoardSubtasksActionService extends BoardActionService { + filterName = 'parent'; + + public get localizedName() { + return this.I18n.t('js.boards.board_type.action_type.subtasks'); + } + + public canMove(workPackage:WorkPackageResource):boolean { + return !!workPackage.changeParent; + } + + protected loadValues(matching?:string):Observable { + let filters = new ApiV3FilterBuilder(); + filters.add('is_milestone', '=', false); + + if (matching) { + filters.add('subjectOrId', '**', [matching]); + } + + return this + .apiV3Service + .work_packages + .filtered(filters) + .get() + .pipe( + map(collection => collection.elements) + ); + } + + protected require(id:string):Promise { + return this + .apiV3Service + .work_packages + .id(id) + .get() + .toPromise(); + } +} diff --git a/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts index da2c997e8b..c8a0f88841 100644 --- a/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/version/version-action.service.ts @@ -12,9 +12,10 @@ import {HalResourceNotificationService} from "core-app/modules/hal/services/hal- import {VersionBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/version/version-board-header.component"; import {FormResource} from "core-app/modules/hal/resources/form-resource"; import {InjectField} from "core-app/helpers/angular/inject-field.decorator"; +import {CachedBoardActionService} from "core-app/modules/boards/board/board-actions/cached-board-action.service"; @Injectable() -export class BoardVersionActionService extends BoardActionService { +export class BoardVersionActionService extends CachedBoardActionService { @InjectField() state:StateService; @InjectField() halNotification:HalResourceNotificationService; @@ -50,7 +51,9 @@ export class BoardVersionActionService extends BoardActionService { } public addInitialColumnsForAction(board:Board):Promise { - return this.withLoadedAvailable() + return this + .loadValues() + .toPromise() .then((results) => { return Promise.all( results.map((version:VersionResource) => { @@ -105,7 +108,7 @@ export class BoardVersionActionService extends BoardActionService { return value instanceof VersionResource && value.isOpen(); } - protected loadAvailable():Promise { + protected loadUncached():Promise { if (this.currentProject.id === null) { return Promise.resolve([]); } diff --git a/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts index cef15d4f24..a375f30289 100644 --- a/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts +++ b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts @@ -204,7 +204,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements const filter = _.find(options.filters, (filter) => !!filter[filterName]); if (filter) { - return filter[filterName].values[0]; + return filter[filterName].values[0] as any; } }) .filter(value => !!value); diff --git a/frontend/src/app/modules/boards/boards-root/boards-root.component.ts b/frontend/src/app/modules/boards/boards-root/boards-root.component.ts index 9ca28072bb..6fab1d3869 100644 --- a/frontend/src/app/modules/boards/boards-root/boards-root.component.ts +++ b/frontend/src/app/modules/boards/boards-root/boards-root.component.ts @@ -6,6 +6,7 @@ import {BoardVersionActionService} from "core-app/modules/boards/board/board-act import {QueryUpdatedService} from "core-app/modules/boards/board/query-updated/query-updated.service"; import {BoardAssigneeActionService} from "core-app/modules/boards/board/board-actions/assignee/assignee-action.service"; import {BoardSubprojectActionService} from "core-app/modules/boards/board/board-actions/subproject/subproject-action.service"; +import {BoardSubtasksActionService} from "core-app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service"; @Component({ selector: 'boards-entry', @@ -16,6 +17,7 @@ import {BoardSubprojectActionService} from "core-app/modules/boards/board/board- BoardVersionActionService, BoardAssigneeActionService, BoardSubprojectActionService, + BoardSubtasksActionService, QueryUpdatedService, ] }) @@ -25,14 +27,11 @@ export class BoardsRootComponent { // Register action services const registry = injector.get(BoardActionsRegistryService); - const statusAction = injector.get(BoardStatusActionService); - const versionAction = injector.get(BoardVersionActionService); - const assigneeAction = injector.get(BoardAssigneeActionService); - const subprojectAction = injector.get(BoardSubprojectActionService); - registry.add('status', statusAction); - registry.add('assignee', assigneeAction); - registry.add('version', versionAction); - registry.add('subproject', subprojectAction); + registry.add('status', injector.get(BoardStatusActionService)); + registry.add('assignee', injector.get(BoardAssigneeActionService)); + registry.add('version', injector.get(BoardVersionActionService)); + registry.add('subproject', injector.get(BoardSubprojectActionService)); + registry.add('subtasks', injector.get(BoardSubtasksActionService)); } } diff --git a/frontend/src/app/modules/common/autocomplete/create-autocompleter.component.html b/frontend/src/app/modules/common/autocomplete/create-autocompleter.component.html index fbeae05a7c..a3592ac61f 100644 --- a/frontend/src/app/modules/common/autocomplete/create-autocompleter.component.html +++ b/frontend/src/app/modules/common/autocomplete/create-autocompleter.component.html @@ -9,6 +9,7 @@ [disabled]="disabled" [typeahead]="typeahead" [clearOnBackspace]="false" + [clearSearchOnAdd]="false" [appendTo]="appendTo" [hideSelected]="hideSelected" [id]="id" diff --git a/modules/boards/config/locales/js-en.yml b/modules/boards/config/locales/js-en.yml index 6357391b8a..ed8f2b0a64 100644 --- a/modules/boards/config/locales/js-en.yml +++ b/modules/boards/config/locales/js-en.yml @@ -50,6 +50,7 @@ en: status: status version: version subproject: subproject + subtasks: children select_attribute: "Action attribute" add_list_modal: From fbff9d93b4e9028dae9121af38326c1632ece211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 5 Aug 2020 14:17:39 +0200 Subject: [PATCH 02/29] Add subtasks action with header --- config/locales/js-en.yml | 1 + .../subtasks/board-subtasks-action.service.ts | 6 ++ .../subtasks-board-header.component.ts | 57 +++++++++++++++++++ .../subtasks/subtasks-board-header.html | 14 +++++ .../subtasks/subtasks-board-header.sass | 7 +++ .../boards/openproject-boards.module.ts | 4 +- 6 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.component.ts create mode 100644 frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.html create mode 100644 frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.sass diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 79d1996497..1c01f1134a 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -447,6 +447,7 @@ en: label_value_derived_from_children: "(value derived from children)" label_warning: "Warning" label_work_package: "Work package" + label_work_package_parent: "Parent work package" label_work_package_plural: "Work packages" label_watch: "Watch" label_watch_work_package: "Watch work package" diff --git a/frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts index caecd45539..ed543fac42 100644 --- a/frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/subtasks/board-subtasks-action.service.ts @@ -7,6 +7,7 @@ import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import {Observable} from "rxjs"; import {map} from "rxjs/operators"; import {ApiV3FilterBuilder, buildApiV3Filter, FalseValue} from "core-components/api/api-v3/api-v3-filter-builder"; +import {SubtasksBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/subtasks/subtasks-board-header.component"; @Injectable() export class BoardSubtasksActionService extends BoardActionService { @@ -16,6 +17,10 @@ export class BoardSubtasksActionService extends BoardActionService { return this.I18n.t('js.boards.board_type.action_type.subtasks'); } + public headerComponent() { + return SubtasksBoardHeaderComponent; + } + public canMove(workPackage:WorkPackageResource):boolean { return !!workPackage.changeParent; } @@ -23,6 +28,7 @@ export class BoardSubtasksActionService extends BoardActionService { protected loadValues(matching?:string):Observable { let filters = new ApiV3FilterBuilder(); filters.add('is_milestone', '=', false); + filters.add('project', '=', [this.currentProject.id]); if (matching) { filters.add('subjectOrId', '**', [matching]); diff --git a/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.component.ts b/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.component.ts new file mode 100644 index 0000000000..b951ae48ba --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.component.ts @@ -0,0 +1,57 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2020 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See docs/COPYRIGHT.rdoc for more details. +//++ +import {Component, Input, OnInit} from "@angular/core"; +import {HalResource} from "core-app/modules/hal/resources/hal-resource"; +import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; +import {I18nService} from "core-app/modules/common/i18n/i18n.service"; +import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; +import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions"; + + +@Component({ + templateUrl: './subtasks-board-header.html', + styleUrls: ['./subtasks-board-header.sass'], + host: { 'class': 'title-container -small' } +}) +export class SubtasksBoardHeaderComponent implements OnInit { + @Input() public resource:WorkPackageResource; + + text = { + workPackage: this.I18n.t('js.label_work_package_parent') + }; + + typeHighlightingClass:string; + + constructor(readonly pathHelper:PathHelperService, + readonly I18n:I18nService) { + } + + ngOnInit() { + this.typeHighlightingClass = Highlighting.inlineClass('type', this.resource.type.id!); + } +} diff --git a/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.html b/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.html new file mode 100644 index 0000000000..47aa182440 --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.html @@ -0,0 +1,14 @@ +
+

+ +
+ + + + +

+
diff --git a/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.sass b/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.sass new file mode 100644 index 0000000000..3fb7e635b5 --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-actions/subtasks/subtasks-board-header.sass @@ -0,0 +1,7 @@ +// Override line-height for proper +// display of the h2 + small +.editable-toolbar-title--fixed + line-height: 1 !important + +.work-package-type + padding-right: 0.5rem \ No newline at end of file diff --git a/frontend/src/app/modules/boards/openproject-boards.module.ts b/frontend/src/app/modules/boards/openproject-boards.module.ts index 4bca52a85a..4f1a94f527 100644 --- a/frontend/src/app/modules/boards/openproject-boards.module.ts +++ b/frontend/src/app/modules/boards/openproject-boards.module.ts @@ -53,6 +53,7 @@ import {BoardsMenuButtonComponent} from "core-app/modules/boards/board/toolbar-m import {AssigneeBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/assignee/assignee-board-header.component"; import { TileViewComponent } from './tile-view/tile-view.component'; import {SubprojectBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/subproject/subproject-board-header.component"; +import {SubtasksBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/subtasks/subtasks-board-header.component"; @NgModule({ imports: [ @@ -88,8 +89,9 @@ import {SubprojectBoardHeaderComponent} from "core-app/modules/boards/board/boar BoardFilterComponent, VersionBoardHeaderComponent, AssigneeBoardHeaderComponent, + SubprojectBoardHeaderComponent, + SubtasksBoardHeaderComponent, TileViewComponent, - SubprojectBoardHeaderComponent ] }) export class OpenprojectBoardsModule { From fc4afc875cc47b984f1097a1a5107c11618ed1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 5 Aug 2020 14:18:03 +0200 Subject: [PATCH 03/29] Clarify that we're actually getting an ID, not href from the query filter --- .../board-actions/board-action.service.ts | 21 +++++++++---------- .../cached-board-action.service.ts | 4 ++-- .../subproject/subproject-action.service.ts | 2 +- .../board/board-list/board-list.component.ts | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts index 0c341eb694..6715406320 100644 --- a/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/board-action.service.ts @@ -62,13 +62,13 @@ export abstract class BoardActionService { /** * Returns the current filter value ID if any * @param query - * @returns /api/v3/status/:id if a status filter exists + * @returns The id of the resource */ - getActionValueHrefForColumn(query:QueryResource):string|undefined { + getActionValueId(query:QueryResource):string|undefined { const filter = _.find(query.filters, filter => filter.id === this.filterName); if (filter) { const value = filter.values[0] as string|HalResource; - return (value instanceof HalResource) ? value.href! : value; + return (value instanceof HalResource) ? value.id! : value; } return; @@ -77,17 +77,16 @@ export abstract class BoardActionService { /** * Returns the current filter value if any * @param query - * @returns /api/v3/status/:id if a status filter exists + * @returns The loaded action reosurce */ getLoadedActionValue(query:QueryResource):Promise { - const href = this.getActionValueHrefForColumn(query); + const id = this.getActionValueId(query); - if (!href) { + if (!id) { return Promise.resolve(undefined); } - return this - .require(href); + return this.require(id); } /** @@ -121,7 +120,7 @@ export abstract class BoardActionService { * Get available values from the active queries * * @param board The board we're looking at - * @param active The active set of values (hrefs or plain values) + * @param active The active set of values (resources or plain values) * @param matching values matching the given name */ loadAvailable(board:Board, active:Set, matching:string):Observable { @@ -218,10 +217,10 @@ export abstract class BoardActionService { /** * Require the given resource to be loaded. * - * @param href + * @param id * @protected */ - protected abstract require(href:string):Promise; + protected abstract require(id:string):Promise; /** * Load values optionally matching the given name diff --git a/frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts index 7b104896fe..48013dde6d 100644 --- a/frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/cached-board-action.service.ts @@ -29,7 +29,7 @@ export abstract class CachedBoardActionService extends BoardActionService { ); } - protected require(idOrHref:string):Promise { + protected require(id:string):Promise { this .cache .putFromPromiseIfPristine(() => this.loadUncached()); @@ -39,7 +39,7 @@ export abstract class CachedBoardActionService extends BoardActionService { .values$() .toPromise() .then(results => { - return results.find(resource => resource.id === idOrHref || resource.href === idOrHref)!; + return results.find(resource => resource.id === id)!; }); } diff --git a/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts b/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts index da6623fb48..a661a344f5 100644 --- a/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts +++ b/frontend/src/app/modules/boards/board/board-actions/subproject/subproject-action.service.ts @@ -39,7 +39,7 @@ export class BoardSubprojectActionService extends CachedBoardActionService { } assignToWorkPackage(changeset:WorkPackageChangeset, query:QueryResource) { - const href = this.getActionValueHrefForColumn(query); + const href = this.getActionValueId(query); changeset.setValue('project', { href: href }); } diff --git a/frontend/src/app/modules/boards/board/board-list/board-list.component.ts b/frontend/src/app/modules/boards/board/board-list/board-list.component.ts index e3e04cd954..0ba8d1733e 100644 --- a/frontend/src/app/modules/boards/board/board-list/board-list.component.ts +++ b/frontend/src/app/modules/boards/board/board-list/board-list.component.ts @@ -334,7 +334,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni } let actionService = this.actionService!; - const id = actionService.getActionValueHrefForColumn(query); + const id = actionService.getActionValueId(query); // Test if we loaded the resource already if (this.actionResource && id === this.actionResource.href) { From 9055654991c2fb0f0a30f5e3705547e9103ce884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 5 Aug 2020 14:40:28 +0200 Subject: [PATCH 04/29] Add spec for subtasks --- .../action_boards/subtasks_board_spec.rb | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 modules/boards/spec/features/action_boards/subtasks_board_spec.rb diff --git a/modules/boards/spec/features/action_boards/subtasks_board_spec.rb b/modules/boards/spec/features/action_boards/subtasks_board_spec.rb new file mode 100644 index 0000000000..56528c8e5a --- /dev/null +++ b/modules/boards/spec/features/action_boards/subtasks_board_spec.rb @@ -0,0 +1,153 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative './../support//board_index_page' +require_relative './../support/board_page' + +describe 'Subtasks action board', type: :feature, js: true do + let(:type) { FactoryBot.create(:type_standard) } + let(:project) { FactoryBot.create(:project, types: [type], enabled_module_names: %i[work_package_tracking board_view]) } + let(:role) { FactoryBot.create(:role, permissions: permissions) } + + let(:user) do + FactoryBot.create(:user, + member_in_project: project, + member_through_role: role) + end + + let(:board_index) { Pages::BoardIndex.new(project) } + + let!(:priority) { FactoryBot.create :default_priority } + let!(:open_status) { FactoryBot.create :default_status, name: 'Open' } + let!(:parent) { FactoryBot.create :work_package, project: project, subject: 'Parent WP', status: open_status } + let!(:child) { FactoryBot.create :work_package, project: project, subject: 'Child WP', parent: parent, status: open_status } + + before do + with_enterprise_token :board_view + login_as(user) + end + + context 'without the manage_subtasks permission' do + let(:permissions) { + %i[show_board_views manage_board_views add_work_packages + edit_work_packages view_work_packages manage_public_queries] + } + + it 'does not allow to move work packages' do + board_index.visit! + + # Create new board + board_page = board_index.create_board action: :Children, expect_empty: true + + # Expect we can add a work package column + board_page.add_list option: 'Parent WP' + board_page.expect_list 'Parent WP' + + # Expect one work package there + board_page.expect_card 'Parent WP', 'Child' + board_page.expect_movable 'Parent WP', 'Child', movable: false + end + end + + context 'with all permissions' do + let!(:other_wp) { FactoryBot.create :work_package, project: project, subject: 'Other WP', status: open_status } + + let(:permissions) { + %i[show_board_views manage_board_views add_work_packages + edit_work_packages view_work_packages manage_public_queries manage_subtasks] + } + + it 'allows management of subtasks work packages' do + board_index.visit! + + # Create new board + board_page = board_index.create_board action: :Children, expect_empty: true + + # Expect we can add a child 1 + board_page.add_list option: 'Parent WP' + board_page.expect_list 'Parent WP' + + # Expect one work package there + board_page.expect_card 'Parent WP', 'Child' + + # Expect move permission to be granted + board_page.expect_movable 'Parent WP', 'Child', movable: true + + board_page.board(reload: true) do |board| + expect(board.name).to eq 'Action board (Children)' + queries = board.contained_queries + expect(queries.count).to eq(1) + + query = queries.first + expect(query.name).to eq 'Parent WP' + + expect(query.filters.first.name).to eq :parent + expect(query.filters.first.values).to eq [parent.id.to_s] + end + + # Create new list + board_page.add_list option: 'Other WP' + board_page.expect_list 'Other WP' + + board_page.expect_cards_in_order 'Other WP' + + # Add item + board_page.add_card 'Parent WP', 'Second child' + sleep 2 + + # Expect added to query + queries = board_page.board(reload: true).contained_queries + expect(queries.count).to eq 2 + first = queries.find_by(name: 'Parent WP') + second = queries.find_by(name: 'Other WP') + expect(first.ordered_work_packages.count).to eq(1) + expect(second.ordered_work_packages).to be_empty + + # Expect work package to be saved in query first + wp = WorkPackage.where(id: first.ordered_work_packages.pluck(:work_package_id)).first + expect(wp.parent_id).to eq parent.id + + # Move item to Child 2 list + board_page.move_card(0, from: 'Parent WP', to: 'Other WP') + + board_page.expect_card('Parent WP', 'Second child', present: false) + board_page.expect_card('Other WP', 'Second child', present: true) + + # Expect work package to be saved in query second + sleep 2 + retry_block do + expect(first.reload.ordered_work_packages).to be_empty + expect(second.reload.ordered_work_packages.count).to eq(1) + end + + wp = WorkPackage.where(id: second.ordered_work_packages.pluck(:work_package_id)).first + expect(wp.parent_id).to eq other_wp.id + end + end +end From eaf9d5915178179ff7924f62dd9096c77074f5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 5 Aug 2020 15:22:07 +0200 Subject: [PATCH 05/29] Trigger empty search when opening the select --- .../boards/board/add-list-modal/add-list-modal.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts index 3df9c41ec0..6587c5c550 100644 --- a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts +++ b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts @@ -97,6 +97,7 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { public referenceOutputs = { onCreate: (value:HalResource) => this.onNewActionCreated(value), + onOpen: () => this.requests.input$.next(''), onChange: (value:HalResource) => this.onModelChange(value), onAfterViewInit: (component:CreateAutocompleterComponent) => component.focusInputField() }; From f7b24662af9d3643bf3620648e5f7d68ea4488f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 6 Aug 2020 10:04:17 +0200 Subject: [PATCH 06/29] Load an empty request in the beginning to show the warning text --- .../helpers/rxjs/debounced-input-switchmap.ts | 5 ++-- .../add-list-modal.component.ts | 25 +++++++++++++++++++ .../board/add-list-modal/add-list-modal.html | 16 ++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts b/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts index a87559b585..1fee6345fc 100644 --- a/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts +++ b/frontend/src/app/helpers/rxjs/debounced-input-switchmap.ts @@ -3,7 +3,7 @@ import {HalResource} from "core-app/modules/hal/resources/hal-resource"; import { catchError, debounceTime, - distinctUntilChanged, filter, + distinctUntilChanged, filter, share, shareReplay, switchMap, takeUntil, tap @@ -67,7 +67,8 @@ export class DebouncedRequestSwitchmap { this.lastResult = results; }) ) - ) + ), + shareReplay(1) ) ); } diff --git a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts index 6587c5c550..c2d036cf6c 100644 --- a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts +++ b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts @@ -102,6 +102,12 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { onAfterViewInit: (component:CreateAutocompleterComponent) => component.focusInputField() }; + /** The loaded available values */ + availableValues:any; + + /** Whether the no results warning is displayed */ + showWarning:boolean = false; + constructor(readonly elementRef:ElementRef, @Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, readonly cdRef:ChangeDetectorRef, @@ -126,6 +132,25 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { .then((text) => { this.warningText = text; }); + + this + .requests + .output$ + .pipe( + this.untilDestroyed() + ) + .subscribe((values:unknown[]) => { + this.availableValues = values; + + if (values.length === 0) { + this.showWarning = true; + } + + this.cdRef.detectChanges(); + }); + + // Request an empty value to load warning early on + this.requests.input$.next(''); } onModelChange(element:HalResource) { diff --git a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html index d756a09c02..cc091129e5 100644 --- a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html +++ b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.html @@ -14,12 +14,19 @@
+
+
+

+
+
+
-
-
-

-
-
-
<% end %> + +<% if OpenProject::Configuration.direct_uploads? %> + <% Hash(@form).each do |key, value| %> + + <% end %> +<% end %> +
- <%= f.file_field :ifc_attachment %> + <% if OpenProject::Configuration.direct_uploads? %> + + <% else %> + <%= f.file_field :ifc_attachment %> + <% end %>
+
<%= f.check_box 'is_default' %>
+ +<% if OpenProject::Configuration.direct_uploads? %> + +<%= nonced_javascript_tag do %> + jQuery(document).ready(function() { + jQuery("input[type=file]").change(function(e){ + var fileName = e.target.files[0].name; + + jQuery.post( + "<%= set_direct_upload_file_name_bcf_project_ifc_models_path %>", + { + title: fileName, + isDefault: jQuery("#bim_ifc_models_ifc_model_is_default").is(":checked") ? 1 : 0 + } + ); + + // rebuild form to post to S3 directly + if (jQuery("input[name=utf8]").length == 1) { + jQuery("input[name=utf8]").remove(); + jQuery("input[name=authenticity_token]").remove(); + jQuery("input[name=_method]").remove(); + + var url = jQuery("input[name=uri]").val(); + + jQuery("form").attr("action", url); + jQuery("form").attr("enctype", "multipart/form-data"); + + jQuery("input[name=uri]").remove(); + + jQuery("form").submit(function() { + jQuery("#bim_ifc_models_ifc_model_title").prop("disabled", "disabled"); + }); + } + }); + }); +<% end %> + +<% end %> diff --git a/modules/bim/config/routes.rb b/modules/bim/config/routes.rb index edfeb49978..1174d96f6c 100644 --- a/modules/bim/config/routes.rb +++ b/modules/bim/config/routes.rb @@ -46,6 +46,8 @@ OpenProject::Application.routes.draw do resources :ifc_models, controller: 'bim/ifc_models/ifc_models' do collection do get :defaults + get :direct_upload_finished + post :set_direct_upload_file_name end end end diff --git a/modules/bim/spec/features/bcf/direct_ifc_upload_spec.rb b/modules/bim/spec/features/bcf/direct_ifc_upload_spec.rb new file mode 100644 index 0000000000..3a3878a94f --- /dev/null +++ b/modules/bim/spec/features/bcf/direct_ifc_upload_spec.rb @@ -0,0 +1,57 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe 'direct IFC upload', type: :feature, js: true, with_direct_uploads: :redirect, with_config: { edition: 'bim' } do + let(:user) { FactoryBot.create :admin } + let(:project) { FactoryBot.create :project, enabled_module_names: %i[bim] } + let(:ifc_fixture) { Rails.root.join('modules/bim/spec/fixtures/files/minimal.ifc') } + + before do + login_as user + + allow_any_instance_of(Bim::IfcModels::BaseContract).to receive(:ifc_attachment_is_ifc).and_return true + end + + it 'should work' do + visit new_bcf_project_ifc_model_path(project_id: project.identifier) + + page.attach_file("file", ifc_fixture, visible: :all) + + click_on "Create" + + expect(page).to have_content("Upload succeeded") + + expect(Attachment.count).to eq 1 + expect(Attachment.first[:file]).to eq 'model.ifc' + + expect(Bim::IfcModels::IfcModel.count).to eq 1 + expect(Bim::IfcModels::IfcModel.first.title).to eq "minimal.ifc" + end +end diff --git a/modules/grids/app/controllers/api/v3/attachments/attachments_by_grid_api.rb b/modules/grids/app/controllers/api/v3/attachments/attachments_by_grid_api.rb index c9222df66f..880a7ee991 100644 --- a/modules/grids/app/controllers/api/v3/attachments/attachments_by_grid_api.rb +++ b/modules/grids/app/controllers/api/v3/attachments/attachments_by_grid_api.rb @@ -45,6 +45,10 @@ module API get &API::V3::Attachments::AttachmentsByContainerAPI.read post &API::V3::Attachments::AttachmentsByContainerAPI.create + + namespace :prepare do + post &API::V3::Attachments::AttachmentsByContainerAPI.prepare + end end end end diff --git a/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb index 70a2a15d5e..5e92b87925 100644 --- a/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb +++ b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb @@ -45,6 +45,10 @@ module API get &API::V3::Attachments::AttachmentsByContainerAPI.read post &API::V3::Attachments::AttachmentsByContainerAPI.create + + namespace :prepare do + post &API::V3::Attachments::AttachmentsByContainerAPI.prepare + end end end end diff --git a/spec/features/work_packages/attachments/attachment_upload_spec.rb b/spec/features/work_packages/attachments/attachment_upload_spec.rb index f7dc0be610..eccf380d96 100644 --- a/spec/features/work_packages/attachments/attachment_upload_spec.rb +++ b/spec/features/work_packages/attachments/attachment_upload_spec.rb @@ -77,57 +77,86 @@ describe 'Upload attachment to work package', js: true do end context 'on a new page' do - let!(:new_page) { Pages::FullWorkPackageCreate.new } - let!(:type) { FactoryBot.create(:type_task) } - let!(:status) { FactoryBot.create(:status, is_default: true) } - let!(:priority) { FactoryBot.create(:priority, is_default: true) } - let!(:project) do - FactoryBot.create(:project, types: [type]) - end + shared_examples 'it supports image uploads via drag & drop' do + let!(:new_page) { Pages::FullWorkPackageCreate.new } + let!(:type) { FactoryBot.create(:type_task) } + let!(:status) { FactoryBot.create(:status, is_default: true) } + let!(:priority) { FactoryBot.create(:priority, is_default: true) } + let!(:project) do + FactoryBot.create(:project, types: [type]) + end - before do - visit new_project_work_packages_path(project.identifier, type: type.id) - end + let(:post_conditions) { nil } - it 'can upload an image via drag & drop (Regression #28189)' do - subject = new_page.edit_field :subject - subject.set_value 'My subject' + before do + visit new_project_work_packages_path(project.identifier, type: type.id) + end - target = find('.ck-content') - attachments.drag_and_drop_file(target, image_fixture) + it 'can upload an image via drag & drop (Regression #28189)' do + subject = new_page.edit_field :subject + subject.set_value 'My subject' - sleep 2 - expect(page).not_to have_selector('notification-upload-progress') + target = find('.ck-content') + attachments.drag_and_drop_file(target, image_fixture) - editor.in_editor do |container, editable| - expect(editable).to have_selector('img[src*="/api/v3/attachments/"]', wait: 20) - end + sleep 2 + expect(page).not_to have_selector('notification-upload-progress') - sleep 2 + editor.in_editor do |container, editable| + expect(editable).to have_selector('img[src*="/api/v3/attachments/"]', wait: 20) + end - # Besides testing caption functionality this also slows down clicking on the submit button - # so that the image is properly embedded - caption = page.find('figure.image figcaption') - caption.click(x: 10, y: 10) - sleep 0.2 - caption.base.send_keys('Some image caption') + sleep 2 - sleep 2 + # Besides testing caption functionality this also slows down clicking on the submit button + # so that the image is properly embedded + caption = page.find('figure.image figcaption') + caption.click(x: 10, y: 10) + sleep 0.2 + caption.base.send_keys('Some image caption') - scroll_to_and_click find('#work-packages--edit-actions-save') + scroll_to_and_click find('#work-packages--edit-actions-save') - wp_page.expect_notification( - message: 'Successful creation.' - ) + wp_page.expect_notification( + message: 'Successful creation.' + ) - field = wp_page.edit_field :description + field = wp_page.edit_field :description - expect(field.display_element).to have_selector('img') - expect(field.display_element).to have_content('Some image caption') + expect(field.display_element).to have_selector('img') + expect(field.display_element).to have_content('Some image caption') + + wp = WorkPackage.last + expect(wp.subject).to eq('My subject') + expect(wp.attachments.count).to eq(1) - wp = WorkPackage.last - expect(wp.subject).to eq('My subject') - expect(wp.attachments.count).to eq(1) + post_conditions + end + end + + it_behaves_like 'it supports image uploads via drag & drop' + + # We do a complete integration test for direct uploads on this example. + # If this works all parts in the backend and frontend work properly together. + # Technically one could test this not only for new work packages, but also for existing + # ones, and for new and existing other attachable resources. But the code is the same + # everywhere so if this works it should work everywhere else too. + context 'with direct uploads', with_direct_uploads: true do + before do + allow_any_instance_of(Attachment).to receive(:diskfile).and_return Struct.new(:path).new(image_fixture.to_s) + end + + it_behaves_like 'it supports image uploads via drag & drop' do + let(:post_conditions) do + # check the attachment was created successfully + expect(Attachment.count).to eq 1 + a = Attachment.first + expect(a[:file]).to eq image_fixture.basename.to_s + + # check /api/v3/attachments/:id/uploaded was called + expect(::Attachments::FinishDirectUploadJob).to have_been_enqueued + end + end end end end diff --git a/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb b/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb index d855840e7d..6c1802b1bd 100644 --- a/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb +++ b/spec/lib/api/v3/attachments/attachment_metadata_representer_spec.rb @@ -31,14 +31,22 @@ require 'spec_helper' describe ::API::V3::Attachments::AttachmentMetadataRepresenter do include API::V3::Utilities::PathHelper - let(:metadata) { + let(:metadata) do data = Hashie::Mash.new data.file_name = original_file_name data.description = original_description + data.content_type = original_content_type + data.file_size = original_file_size + data.digest = original_digest data - } + end + let(:original_file_name) { 'a file name' } let(:original_description) { 'a description' } + let(:original_content_type) { 'text/plain' } + let(:original_file_size) { 42 } + let(:original_digest) { "0xFF" } + let(:representer) { ::API::V3::Attachments::AttachmentMetadataRepresenter.new(metadata) } describe 'generation' do @@ -49,6 +57,9 @@ describe ::API::V3::Attachments::AttachmentMetadataRepresenter do end it { is_expected.to be_json_eql(original_file_name.to_json).at_path('fileName') } + it { is_expected.to be_json_eql(original_content_type.to_json).at_path('contentType') } + it { is_expected.to be_json_eql(original_file_size.to_json).at_path('fileSize') } + it { is_expected.to be_json_eql(original_digest.to_json).at_path('digest') } it_behaves_like 'API V3 formattable', 'description' do let(:format) { 'plain' } @@ -60,7 +71,10 @@ describe ::API::V3::Attachments::AttachmentMetadataRepresenter do let(:parsed_hash) { { 'fileName' => 'the parsed name', - 'description' => { 'raw' => 'the parsed description' } + 'description' => { 'raw' => 'the parsed description' }, + 'contentType' => 'text/html', + 'fileSize' => 43, + 'digest' => '0x00' } } @@ -72,5 +86,8 @@ describe ::API::V3::Attachments::AttachmentMetadataRepresenter do it { expect(subject.file_name).to eql('the parsed name') } it { expect(subject.description).to eql('the parsed description') } + it { expect(subject.content_type).to eql('text/html') } + it { expect(subject.file_size).to eql(43) } + it { expect(subject.digest).to eql('0x00') } end end diff --git a/spec/lib/open_project/configuration_spec.rb b/spec/lib/open_project/configuration_spec.rb index 2e264dbf2d..f4bdafff54 100644 --- a/spec/lib/open_project/configuration_spec.rb +++ b/spec/lib/open_project/configuration_spec.rb @@ -480,4 +480,39 @@ describe OpenProject::Configuration do end end end + + context 'helpers' do + describe '#direct_uploads?' do + let(:value) { OpenProject::Configuration.direct_uploads? } + + it 'should be false by default' do + expect(value).to be false + end + + context 'with remote storage' do + def self.storage(provider) + { + attachments_storage: :fog, + fog: { + credentials: { + provider: provider + } + } + } + end + + context 'AWS', with_config: storage('AWS') do + it 'should be true' do + expect(value).to be true + end + end + + context 'Azure', with_config: storage('azure') do + it 'should be false' do + expect(value).to be false + end + end + end + end + end end diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index cc8f9a1c53..5e00a5ccf7 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -226,39 +226,18 @@ describe Attachment, type: :model do end end - describe "#external_url" do + # We just use with_direct_uploads here to make sure the + # FogAttachment class is defined and Fog is mocked. + describe "#external_url", with_direct_uploads: true do let(:author) { FactoryBot.create :user } let(:image_path) { Rails.root.join("spec/fixtures/files/image.png") } let(:text_path) { Rails.root.join("spec/fixtures/files/testfile.txt") } let(:binary_path) { Rails.root.join("spec/fixtures/files/textfile.txt.gz") } - let(:fog_attachment_class) do - class FogAttachment < Attachment - # Remounting the uploader overrides the original file setter taking care of setting, - # among other things, the content type. So we have to restore that original - # method this way. - # We do this in a new, separate class, as to not interfere with any other specs. - alias_method :set_file, :file= - mount_uploader :file, FogFileUploader - alias_method :file=, :set_file - end - - FogAttachment - end - - let(:image_attachment) { fog_attachment_class.new author: author, file: File.open(image_path) } - let(:text_attachment) { fog_attachment_class.new author: author, file: File.open(text_path) } - let(:binary_attachment) { fog_attachment_class.new author: author, file: File.open(binary_path) } - - before do - Fog.mock! - - connection = Fog::Storage.new provider: "AWS" - connection.directories.create key: "my-bucket" - - CarrierWave::Configuration.configure_fog! credentials: {}, directory: "my-bucket", public: false - end + let(:image_attachment) { FogAttachment.new author: author, file: File.open(image_path) } + let(:text_attachment) { FogAttachment.new author: author, file: File.open(text_path) } + let(:binary_attachment) { FogAttachment.new author: author, file: File.open(binary_path) } shared_examples "it has a temporary download link" do let(:url_options) { {} } diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index e2ce744046..cdc56ee527 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -47,6 +47,12 @@ require 'test_prof/recipes/rspec/before_all' # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # +# The files are sorted before requiring them to ensure the load order is the same +# everywhere. There are certain helpers that depend on a expected order. +# The CI may load the files in a different order than what you see locally which +# may lead to broken specs on the CI, if we don't sort here +# (example: with_config.rb has to precede with_direct_uploads.rb). +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } Dir[Rails.root.join('spec/features/support/**/*.rb')].each { |f| require f } Dir[Rails.root.join('spec/lib/api/v3/support/**/*.rb')].each { |f| require f } diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index 75e485db8a..cf4a6fbb82 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -29,6 +29,112 @@ require 'spec_helper' require 'rack/test' +shared_examples 'it supports direct uploads' do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + include FileHelpers + + let(:container_href) { raise "define me!" } + let(:request_path) { raise "define me!" } + + before do + allow(User).to receive(:current).and_return current_user + end + + describe 'POST /prepare', with_settings: { attachment_max_size: 512 } do + let(:request_parts) { { metadata: metadata, file: file } } + let(:metadata) { { fileName: 'cat.png', fileSize: file.size }.to_json } + let(:file) { mock_uploaded_file(name: 'original-filename.txt') } + + def request! + post request_path, request_parts + end + + subject(:response) { last_response } + + context 'with local storage' do + before do + request! + end + + it 'should respond with HTTP Not Found' do + expect(subject.status).to eq(404) + end + end + + context 'with remote AWS storage', with_direct_uploads: true do + before do + request! + end + + context 'with no filesize metadata' do + let(:metadata) { { fileName: 'cat.png' }.to_json } + + it 'should respond with 422 due to missing file size metadata' do + expect(subject.status).to eq(422) + expect(subject.body).to include 'fileSize' + end + end + + context 'with the correct parameters' do + let(:json) { JSON.parse subject.body } + + it 'should prepare a direct upload' do + expect(subject.status).to eq 201 + + expect(json["_type"]).to eq "AttachmentUpload" + expect(json["fileName"]).to eq "cat.png" + end + + describe 'response' do + describe '_links' do + describe 'container' do + let(:link) { json.dig "_links", "container" } + + before do + expect(link).to be_present + end + + it "it points to the expected container" do + expect(link["href"]).to eq container_href + end + end + + describe 'addAttachment' do + let(:link) { json.dig "_links", "addAttachment" } + + before do + expect(link).to be_present + end + + it 'should point to AWS' do + expect(link["href"]).to eq "https://#{MockCarrierwave.bucket}.s3.amazonaws.com/" + end + + it 'should have the method POST' do + expect(link["method"]).to eq "post" + end + + it 'should include form fields' do + fields = link["form_fields"] + + expect(fields).to be_present + expect(fields).to include( + "key", "acl", "policy", + "X-Amz-Signature", "X-Amz-Credential", "X-Amz-Algorithm", "X-Amz-Date", + "success_action_status" + ) + + expect(fields["key"]).to end_with "cat.png" + end + end + end + end + end + end + end +end + shared_examples 'an APIv3 attachment resource', type: :request, content_type: :json do |include_by_container = true| include Rack::Test::Methods include API::V3::Utilities::PathHelper @@ -366,6 +472,11 @@ shared_examples 'an APIv3 attachment resource', type: :request, content_type: :j end context 'by container', if: include_by_container do + it_behaves_like 'it supports direct uploads' do + let(:request_path) { "/api/v3/#{attachment_type}s/#{container.id}/attachments/prepare" } + let(:container_href) { "/api/v3/#{attachment_type}s/#{container.id}" } + end + subject(:response) { last_response } describe '#get' do diff --git a/spec/requests/api/v3/attachments_spec.rb b/spec/requests/api/v3/attachments_spec.rb new file mode 100644 index 0000000000..3940f03423 --- /dev/null +++ b/spec/requests/api/v3/attachments_spec.rb @@ -0,0 +1,103 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' +require_relative 'attachments/attachment_resource_shared_examples' + +describe API::V3::Attachments::AttachmentsAPI, type: :request do + include Rack::Test::Methods + include API::V3::Utilities::PathHelper + include FileHelpers + + let(:current_user) { FactoryBot.create(:user, member_in_project: project, member_through_role: role) } + + let(:project) { FactoryBot.create(:project, public: false) } + let(:role) { FactoryBot.create(:role, permissions: permissions) } + let(:permissions) { [:add_work_packages] } + + context( + 'with missing permissions', + with_config: { + attachments_storage: :fog, + fog: { credentials: { provider: 'AWS' } } + } + ) do + let(:permissions) { [] } + + let(:request_path) { api_v3_paths.prepare_new_attachment_upload } + let(:request_parts) { { metadata: metadata, file: file } } + let(:metadata) { { fileName: 'cat.png' }.to_json } + let(:file) { mock_uploaded_file(name: 'original-filename.txt') } + + before do + post request_path, request_parts + end + + it 'should forbid to prepare attachments' do + expect(last_response.status).to eq 403 + end + end + + it_behaves_like 'it supports direct uploads' do + let(:request_path) { api_v3_paths.prepare_new_attachment_upload } + let(:container_href) { nil } + + describe 'GET /uploaded' do + let(:digest) { "" } + let(:attachment) { FactoryBot.create :attachment, digest: digest, author: current_user, container: nil, container_type: nil, downloads: -1 } + + before do + get "/api/v3/attachments/#{attachment.id}/uploaded" + end + + context 'with no pending attachments' do + let(:digest) { "0xFF" } + + it 'should return 404' do + expect(last_response.status).to eq 404 + end + end + + context 'with a pending attachment' do + it 'should enqueue a FinishDirectUpload job' do + expect(::Attachments::FinishDirectUploadJob).to have_been_enqueued.at_least(1) + end + + it 'should respond with HTTP OK' do + expect(last_response.status).to eq 200 + end + + it 'should return the attachment representation' do + json = JSON.parse last_response.body + + expect(json["_type"]).to eq "Attachment" + end + end + end + end +end diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb index 72de19221d..bf7a280eea 100644 --- a/spec/support/carrierwave.rb +++ b/spec/support/carrierwave.rb @@ -26,17 +26,31 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -mock_credentials = { - provider: 'AWS', - aws_access_key_id: 'someaccesskeyid', - aws_secret_access_key: 'someprivateaccesskey', - region: 'us-east-1' -} -mock_bucket = 'test-bucket' +module MockCarrierwave + extend self -Fog.mock! -Fog.credentials = mock_credentials -CarrierWave::Configuration.configure_fog! directory: mock_bucket, credentials: mock_credentials + def apply + Fog.mock! + Fog.credentials = credentials -connection = Fog::Storage.new provider: mock_credentials[:provider] -connection.directories.create key: mock_bucket + CarrierWave::Configuration.configure_fog! directory: bucket, credentials: credentials + + connection = Fog::Storage.new provider: credentials[:provider] + connection.directories.create key: bucket + end + + def bucket + 'test-bucket' + end + + def credentials + { + provider: 'AWS', + aws_access_key_id: 'someaccesskeyid', + aws_secret_access_key: 'someprivateaccesskey', + region: 'us-east-1' + } + end +end + +MockCarrierwave.apply diff --git a/spec/support/shared/with_config.rb b/spec/support/shared/with_config.rb index fae1294c8e..f96a036abe 100644 --- a/spec/support/shared/with_config.rb +++ b/spec/support/shared/with_config.rb @@ -27,36 +27,64 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -def aggregate_mocked_configuration(example, config) - # We have to manually check parent groups for with_config:, - # since they are being ignored otherwise - example.example_group.module_parents.each do |parent| - if parent.respond_to?(:metadata) && parent.metadata[:with_config] - config.reverse_merge!(parent.metadata[:with_config]) +class WithConfig + attr_reader :context + + def initialize(context) + @context = context + end + + ## + # We need this so calls to rspec mocks (allow, expect etc.) will work here as expected. + def method_missing(method, *args, &block) + if context.respond_to?(method) + context.send method, *args, &block + else + super end end - config + ## + # Stubs the given configurations. + # + # @config [Hash] Hash containing the configurations with keys as seen in `configuration.rb`. + def before(example, config) + allow(OpenProject::Configuration).to receive(:[]).and_call_original + + aggregate_mocked_configuration(example, config) + .with_indifferent_access + .each(&method(:stub_key)) + end + + def stub_key(key, value) + allow(OpenProject::Configuration) + .to receive(:[]) + .with(key.to_s) + .and_return(value) + + allow(OpenProject::Configuration) + .to receive(:[]) + .with(key.to_sym) + .and_return(value) + end + + def aggregate_mocked_configuration(example, config) + # We have to manually check parent groups for with_config:, + # since they are being ignored otherwise + example.example_group.module_parents.each do |parent| + if parent.respond_to?(:metadata) && parent.metadata[:with_config] + config.reverse_merge!(parent.metadata[:with_config]) + end + end + + config + end end RSpec.configure do |config| config.before(:each) do |example| - config = example.metadata[:with_config] - if config.present? - config = aggregate_mocked_configuration(example, config).with_indifferent_access - - allow(OpenProject::Configuration).to receive(:[]).and_call_original - config.each do |k, v| - allow(OpenProject::Configuration) - .to receive(:[]) - .with(k.to_s) - .and_return(v) - - allow(OpenProject::Configuration) - .to receive(:[]) - .with(k.to_sym) - .and_return(v) - end - end + with_config = example.metadata[:with_config] + + WithConfig.new(self).before example, with_config if with_config.present? end end diff --git a/spec/support/shared/with_direct_uploads.rb b/spec/support/shared/with_direct_uploads.rb new file mode 100644 index 0000000000..c0ebd68325 --- /dev/null +++ b/spec/support/shared/with_direct_uploads.rb @@ -0,0 +1,199 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +class WithDirectUploads + attr_reader :context + + def initialize(context) + @context = context + end + + ## + # We need this so calls to rspec mocks (allow, expect etc.) will work here as expected. + def method_missing(method, *args, &block) + if context.respond_to?(method) + context.send method, *args, &block + else + super + end + end + + def before(example) + stub_config example + + mock_attachment + stub_frontend redirect: redirect?(example) if stub_frontend?(example) + + stub_uploader + end + + def stub_frontend?(example) + example.metadata[:js] + end + + def redirect?(example) + example.metadata[:with_direct_uploads] == :redirect + end + + def around(example) + example.metadata[:driver] = :headless_firefox_billy + + csp_config = SecureHeaders::Configuration.instance_variable_get("@default_config").csp + csp_config.connect_src = ["'self'", "my-bucket.s3.amazonaws.com"] + + begin + example.run + ensure + csp_config.connect_src = %w('self') + end + end + + def mock_attachment + allow(Attachment).to receive(:create) do |*args| + # We don't use create here because this would cause an infinite loop as FogAttachment's #create + # uses the base class's #create which is what we are mocking here. All this is necessary to begin + # with because the Attachment class is initialized with the LocalFileUploader before this test + # is ever run and we need remote attachments using the FogFileUploader in this scenario. + record = FogAttachment.new *args + record.save + record + end + + # This is so the uploaded callback works. Since we can't actually substitute the Attachment class + # used there we get a LocalFileUploader file for the attachment which is not readable when + # everything else is mocked to be remote. + allow_any_instance_of(FileUploader).to receive(:readable?).and_return true + end + + def stub_frontend(redirect: false) + proxy.stub("https://" + OpenProject::Configuration.remote_storage_host + ":443/", method: 'options').and_return( + headers: { + 'Access-Control-Allow-Methods' => 'POST', + 'Access-Control-Allow-Origin' => '*' + }, + code: 200 + ) + + if redirect + stub_with_redirect + else # use status response instead of redirect by default + stub_with_status + end + end + + def stub_with_redirect + proxy + .stub("https://" + OpenProject::Configuration.remote_storage_host + ":443/", method: 'post') + .and_return(Proc.new { |params, headers, body, url, method| + key = body.scan(/key"\s*([^\s]+)\s/m).flatten.first + redirect_url = body.scan(/success_action_redirect"\s*(http[^\s]+)\s/m).flatten.first + ok = body =~ /X-Amz-Signature/ # check that the expected post to AWS was made with the form fields + + { + code: ok ? 302 : 403, + headers: { + 'Location' => ok ? redirect_url + '?key=' + CGI.escape(key) : nil, + 'Access-Control-Allow-Methods' => 'POST', + 'Access-Control-Allow-Origin' => '*' + } + } + }) + end + + def stub_with_status + proxy + .stub("https://" + OpenProject::Configuration.remote_storage_host + ":443/", method: 'post') + .and_return(Proc.new { |params, headers, body, url, method| + { + code: (body =~ /X-Amz-Signature/) ? 201 : 403, # check that the expected post to AWS was made with the form fields + headers: { + 'Access-Control-Allow-Methods' => 'POST', + 'Access-Control-Allow-Origin' => '*' + } + } + }) + end + + def stub_uploader + creds = config[:fog][:credentials] + + allow_any_instance_of(FogFileUploader).to receive(:fog_credentials).and_return creds + + allow_any_instance_of(FogFileUploader).to receive(:aws_access_key_id).and_return creds[:aws_access_key_id] + allow_any_instance_of(FogFileUploader).to receive(:aws_secret_access_key).and_return creds[:aws_secret_access_key] + allow_any_instance_of(FogFileUploader).to receive(:provider).and_return creds[:provider] + allow_any_instance_of(FogFileUploader).to receive(:region).and_return creds[:region] + allow_any_instance_of(FogFileUploader).to receive(:directory).and_return config[:fog][:directory] + + allow(OpenProject::Configuration).to receive(:direct_uploads?).and_return(true) + end + + def stub_config(example) + WithConfig.new(context).before example, config + end + + def config + { + attachments_storage: :fog, + fog: { + directory: MockCarrierwave.bucket, + credentials: MockCarrierwave.credentials + } + } + end +end + +RSpec.configure do |config| + config.before(:each) do |example| + next unless example.metadata[:with_direct_uploads] + + WithDirectUploads.new(self).before example + + class FogAttachment < Attachment + # Remounting the uploader overrides the original file setter taking care of setting, + # among other things, the content type. So we have to restore that original + # method this way. + # We do this in a new, separate class, as to not interfere with any other specs. + alias_method :set_file, :file= + mount_uploader :file, FogFileUploader + alias_method :file=, :set_file + end + end + + config.around(:each) do |example| + enabled = example.metadata[:with_direct_uploads] + + if enabled + WithDirectUploads.new(self).around example + else + example.run + end + end +end diff --git a/spec/views/layouts/base.html.erb_spec.rb b/spec/views/layouts/base.html.erb_spec.rb index 7729e17fa6..5d38cbc5c2 100644 --- a/spec/views/layouts/base.html.erb_spec.rb +++ b/spec/views/layouts/base.html.erb_spec.rb @@ -267,4 +267,17 @@ describe 'layouts/base', type: :view do end end end + + describe 'openproject_initializer meta tag' do + let(:current_user) { anonymous } + let(:base) { 'meta[name=openproject_initializer]' } + + before do + render + end + + it 'has the meta tag' do + expect(rendered).to have_selector(base, visible: false) + end + end end diff --git a/spec/workers/attachments/cleanup_uncontainered_job_integration_spec.rb b/spec/workers/attachments/cleanup_uncontainered_job_integration_spec.rb index b8b242294c..4242ee269e 100644 --- a/spec/workers/attachments/cleanup_uncontainered_job_integration_spec.rb +++ b/spec/workers/attachments/cleanup_uncontainered_job_integration_spec.rb @@ -32,6 +32,7 @@ require 'spec_helper' describe Attachments::CleanupUncontaineredJob, type: :job do let(:grace_period) { 120 } + let!(:containered_attachment) { FactoryBot.create(:attachment) } let!(:old_uncontainered_attachment) do FactoryBot.create(:attachment, container: nil, created_at: Time.now - grace_period.minutes) @@ -39,6 +40,17 @@ describe Attachments::CleanupUncontaineredJob, type: :job do let!(:new_uncontainered_attachment) do FactoryBot.create(:attachment, container: nil, created_at: Time.now - (grace_period - 1).minutes) end + + let!(:finished_upload) do + FactoryBot.create(:attachment, created_at: Time.now - grace_period.minutes, digest: "0x42") + end + let!(:old_pending_upload) do + FactoryBot.create(:attachment, created_at: Time.now - grace_period.minutes, digest: "", downloads: -1) + end + let!(:new_pending_upload) do + FactoryBot.create(:attachment, created_at: Time.now - (grace_period - 1).minutes, digest: "", downloads: -1) + end + let(:job) { described_class.new } before do @@ -47,10 +59,10 @@ describe Attachments::CleanupUncontaineredJob, type: :job do .and_return(grace_period) end - it 'removes all uncontainered attachments that are older than the grace period' do + it 'removes all uncontainered attachments and pending uploads that are older than the grace period' do job.perform expect(Attachment.all) - .to match_array([containered_attachment, new_uncontainered_attachment]) + .to match_array([containered_attachment, new_uncontainered_attachment, finished_upload, new_pending_upload]) end end From 5f8a1e4b4b4511883f1680999407880785dbdda3 Mon Sep 17 00:00:00 2001 From: Travis CI User Date: Sat, 8 Aug 2020 08:17:37 +0000 Subject: [PATCH 16/29] update locales from crowdin [ci skip] --- config/locales/crowdin/ar.yml | 3 +++ config/locales/crowdin/bg.yml | 3 +++ config/locales/crowdin/ca.yml | 3 +++ config/locales/crowdin/cs.yml | 3 +++ config/locales/crowdin/da.yml | 3 +++ config/locales/crowdin/de.yml | 5 ++++- config/locales/crowdin/el.yml | 3 +++ config/locales/crowdin/es.yml | 3 +++ config/locales/crowdin/fi.yml | 3 +++ config/locales/crowdin/fil.yml | 3 +++ config/locales/crowdin/fr.yml | 3 +++ config/locales/crowdin/hr.yml | 3 +++ config/locales/crowdin/hu.yml | 3 +++ config/locales/crowdin/id.yml | 3 +++ config/locales/crowdin/it.yml | 3 +++ config/locales/crowdin/ja.yml | 3 +++ config/locales/crowdin/ko.yml | 3 +++ config/locales/crowdin/lt.yml | 3 +++ config/locales/crowdin/nl.yml | 3 +++ config/locales/crowdin/no.yml | 3 +++ config/locales/crowdin/pl.yml | 3 +++ config/locales/crowdin/pt.yml | 3 +++ config/locales/crowdin/ro.yml | 3 +++ config/locales/crowdin/ru.yml | 3 +++ config/locales/crowdin/sk.yml | 3 +++ config/locales/crowdin/sl.yml | 3 +++ config/locales/crowdin/sv.yml | 3 +++ config/locales/crowdin/tr.yml | 3 +++ config/locales/crowdin/uk.yml | 3 +++ config/locales/crowdin/vi.yml | 3 +++ config/locales/crowdin/zh-CN.yml | 3 +++ config/locales/crowdin/zh-TW.yml | 3 +++ modules/boards/config/locales/crowdin/js-lt.yml | 6 +++--- modules/boards/config/locales/crowdin/js-ru.yml | 6 +++--- modules/boards/config/locales/crowdin/js-tr.yml | 6 +++--- 35 files changed, 106 insertions(+), 10 deletions(-) diff --git a/config/locales/crowdin/ar.yml b/config/locales/crowdin/ar.yml index ceffc74618..749f5e3017 100644 --- a/config/locales/crowdin/ar.yml +++ b/config/locales/crowdin/ar.yml @@ -756,6 +756,9 @@ ar: date: "التاريخ" default_columns: "الأعمدة الافتراضية" description: "الوصف" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "عرض المبالغ" due_date: "Finish date" estimated_hours: "الوقت المُقّدَّر" diff --git a/config/locales/crowdin/bg.yml b/config/locales/crowdin/bg.yml index 7aef3e31fe..6c47b9e494 100644 --- a/config/locales/crowdin/bg.yml +++ b/config/locales/crowdin/bg.yml @@ -740,6 +740,9 @@ bg: date: "Дата" default_columns: "Колони по подразбиране" description: "Описание" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Показване на суми" due_date: "Finish date" estimated_hours: "Очаквано време" diff --git a/config/locales/crowdin/ca.yml b/config/locales/crowdin/ca.yml index 766793fd27..cee8b680ea 100644 --- a/config/locales/crowdin/ca.yml +++ b/config/locales/crowdin/ca.yml @@ -740,6 +740,9 @@ ca: date: "Data" default_columns: "Columnes predeterminades" description: "Descripció" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Mostra les sumes" due_date: "Finish date" estimated_hours: "Temps estimat" diff --git a/config/locales/crowdin/cs.yml b/config/locales/crowdin/cs.yml index d27295c88a..831a3c3806 100644 --- a/config/locales/crowdin/cs.yml +++ b/config/locales/crowdin/cs.yml @@ -748,6 +748,9 @@ cs: date: "Datum" default_columns: "Výchozí sloupce" description: "Popis" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Zobrazit součty" due_date: "Datum dokončení" estimated_hours: "Odhadovaný čas" diff --git a/config/locales/crowdin/da.yml b/config/locales/crowdin/da.yml index 1d7ff936ef..820d5e2c27 100644 --- a/config/locales/crowdin/da.yml +++ b/config/locales/crowdin/da.yml @@ -740,6 +740,9 @@ da: date: "Dato" default_columns: "Forudvalgte kolonner" description: "Beskrivelse" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Vis totaler" due_date: "Finish date" estimated_hours: "Anslået tid" diff --git a/config/locales/crowdin/de.yml b/config/locales/crowdin/de.yml index 6bd3e67ea6..1d7a9b98f9 100644 --- a/config/locales/crowdin/de.yml +++ b/config/locales/crowdin/de.yml @@ -115,7 +115,7 @@ de: filter_string: | Fügen Sie einen optionalen RFC4515 Filter hinzu, um die zu findenden Benutzer im LDAP weiter einschränken zu können. Dieser Fillter wird für die Authentifizierung und Gruppensynchronisierung verwendet. filter_string_concat: | - OpenProject filtert immer nach dem Login-Attribut des Benutzers filtern, um einen LDAP-Eintrag zu identifizieren. Wenn Sie hier einen Filter angeben, wird + OpenProject filtert immer nach dem Login-Attribut des Benutzers, um einen LDAP-Eintrag zu identifizieren. Wenn Sie hier einen Filter angeben, wird mit einem 'UND' verbunden. Standardmäßig wird ein Catch-All-Filter (objectClass=*) verwendet. onthefly_register: | Wenn Sie dieses Häkchen setzen, erstellt OpenProject automatisch neue Benutzer aus ihren zugehörigen LDAP-Einträgen, wenn sie sich zuerst mit OpenProject anmelden. @@ -735,6 +735,9 @@ de: date: "Datum" default_columns: "Standard-Spalten" description: "Beschreibung" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Summen anzeigen" due_date: "Endtermin" estimated_hours: "Geschätzter Aufwand" diff --git a/config/locales/crowdin/el.yml b/config/locales/crowdin/el.yml index 8f1540435c..28e367a716 100644 --- a/config/locales/crowdin/el.yml +++ b/config/locales/crowdin/el.yml @@ -737,6 +737,9 @@ el: date: "Ημερομηνία" default_columns: "Προεπιλεγμένες στήλες" description: "Περιγραφή" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Εμφάνιση Αθροισμάτων" due_date: "Ημερομηνία λήξης" estimated_hours: "Εκτιμώμενος χρόνος" diff --git a/config/locales/crowdin/es.yml b/config/locales/crowdin/es.yml index 0bd8757020..7e82bd1610 100644 --- a/config/locales/crowdin/es.yml +++ b/config/locales/crowdin/es.yml @@ -737,6 +737,9 @@ es: date: "Fecha" default_columns: "Columnas predeterminadas" description: "Descripción" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Mostrar sumas" due_date: "Fecha de finalización" estimated_hours: "Tiempo estimado" diff --git a/config/locales/crowdin/fi.yml b/config/locales/crowdin/fi.yml index f62c6b6510..b342b2ce10 100644 --- a/config/locales/crowdin/fi.yml +++ b/config/locales/crowdin/fi.yml @@ -740,6 +740,9 @@ fi: date: "Päivämäärä" default_columns: "Oletussarakkeet" description: "Kuvaus" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Näytä summat" due_date: "Päättymispäivä" estimated_hours: "Työmääräarvio" diff --git a/config/locales/crowdin/fil.yml b/config/locales/crowdin/fil.yml index 6fa42e3a1b..481868e22d 100644 --- a/config/locales/crowdin/fil.yml +++ b/config/locales/crowdin/fil.yml @@ -740,6 +740,9 @@ fil: date: "Petsa" default_columns: "I-default ang mga hanay" description: "Deskripsyon" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Ipakita ang mga sum" due_date: "Finish date" estimated_hours: "Tinantyang oras" diff --git a/config/locales/crowdin/fr.yml b/config/locales/crowdin/fr.yml index df1d7cadef..e747e39553 100644 --- a/config/locales/crowdin/fr.yml +++ b/config/locales/crowdin/fr.yml @@ -739,6 +739,9 @@ fr: date: "date" default_columns: "Colonnes par défaut" description: "Description" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Afficher les sommes" due_date: "Date de fin" estimated_hours: "Durée estimée" diff --git a/config/locales/crowdin/hr.yml b/config/locales/crowdin/hr.yml index ae2875301a..93cc31f7cb 100644 --- a/config/locales/crowdin/hr.yml +++ b/config/locales/crowdin/hr.yml @@ -744,6 +744,9 @@ hr: date: "Datum" default_columns: "Zadani stupci" description: "Opis" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Prikaži iznose" due_date: "Finish date" estimated_hours: "Predviđeno vrijeme" diff --git a/config/locales/crowdin/hu.yml b/config/locales/crowdin/hu.yml index 4593192d48..4f96408b2f 100644 --- a/config/locales/crowdin/hu.yml +++ b/config/locales/crowdin/hu.yml @@ -737,6 +737,9 @@ hu: date: "dátum" default_columns: "Alapértelmezett oszlopok" description: "Leírás" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Megjelenitendő összegek" due_date: "Befejezési dátum" estimated_hours: "Becsült idő (óra)" diff --git a/config/locales/crowdin/id.yml b/config/locales/crowdin/id.yml index 075805b943..ed18b8b4b0 100644 --- a/config/locales/crowdin/id.yml +++ b/config/locales/crowdin/id.yml @@ -735,6 +735,9 @@ id: date: "Tanggal" default_columns: "Kolom default" description: "Deskripsi" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Tampilkan jumlah" due_date: "Finish date" estimated_hours: "Estimasi Waktu" diff --git a/config/locales/crowdin/it.yml b/config/locales/crowdin/it.yml index 84f83fdafb..c51cfb08b5 100644 --- a/config/locales/crowdin/it.yml +++ b/config/locales/crowdin/it.yml @@ -736,6 +736,9 @@ it: date: "Data" default_columns: "Colonne predefinite" description: "Descrizione" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Visualizza somme" due_date: "Data di fine" estimated_hours: "Tempo stimato" diff --git a/config/locales/crowdin/ja.yml b/config/locales/crowdin/ja.yml index 539f19b9b7..be2f987bfb 100644 --- a/config/locales/crowdin/ja.yml +++ b/config/locales/crowdin/ja.yml @@ -732,6 +732,9 @@ ja: date: "日付" default_columns: "既定の列" description: "説明" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "合計を表示" due_date: "終了日" estimated_hours: "予定工数" diff --git a/config/locales/crowdin/ko.yml b/config/locales/crowdin/ko.yml index 6ba1049126..237b3bdf12 100644 --- a/config/locales/crowdin/ko.yml +++ b/config/locales/crowdin/ko.yml @@ -735,6 +735,9 @@ ko: date: "날짜" default_columns: "기본 칼럼" description: "설명" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "합계 표시" due_date: "완료 날짜" estimated_hours: "예상된 시간" diff --git a/config/locales/crowdin/lt.yml b/config/locales/crowdin/lt.yml index 1b041c2a4f..07bc117561 100644 --- a/config/locales/crowdin/lt.yml +++ b/config/locales/crowdin/lt.yml @@ -743,6 +743,9 @@ lt: date: "Data" default_columns: "Numatytieji stulpeliai" description: "Aprašymas" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Rodyti suvestines" due_date: "Pabaigos data" estimated_hours: "Numatyta trukmė" diff --git a/config/locales/crowdin/nl.yml b/config/locales/crowdin/nl.yml index aa017dc49b..7c37fde939 100644 --- a/config/locales/crowdin/nl.yml +++ b/config/locales/crowdin/nl.yml @@ -740,6 +740,9 @@ nl: date: "Datum" default_columns: "Standaardkolommen" description: "Omschrijving" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Bedragen weergeven" due_date: "Einddatum" estimated_hours: "Geschatte tijd" diff --git a/config/locales/crowdin/no.yml b/config/locales/crowdin/no.yml index 62def70146..daa13b358b 100644 --- a/config/locales/crowdin/no.yml +++ b/config/locales/crowdin/no.yml @@ -740,6 +740,9 @@ date: "Dato" default_columns: "Standardkolonner" description: "Beskrivelse" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Vis summer" due_date: "Sluttdato" estimated_hours: "Tidsestimat" diff --git a/config/locales/crowdin/pl.yml b/config/locales/crowdin/pl.yml index d9ec1621b5..d05ea49ef4 100644 --- a/config/locales/crowdin/pl.yml +++ b/config/locales/crowdin/pl.yml @@ -744,6 +744,9 @@ pl: date: "Data" default_columns: "Domyślne kolumny" description: "Opis" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Wyświetl sumy" due_date: "Data zakończenia" estimated_hours: "Szacowany czas" diff --git a/config/locales/crowdin/pt.yml b/config/locales/crowdin/pt.yml index 352996a47e..25c2207733 100644 --- a/config/locales/crowdin/pt.yml +++ b/config/locales/crowdin/pt.yml @@ -738,6 +738,9 @@ pt: date: "Data" default_columns: "Colunas padrão" description: "Descrição" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Mostrar somas" due_date: "Data de conclusão" estimated_hours: "Tempo estimado" diff --git a/config/locales/crowdin/ro.yml b/config/locales/crowdin/ro.yml index 7322769df2..cc8a33dcaf 100644 --- a/config/locales/crowdin/ro.yml +++ b/config/locales/crowdin/ro.yml @@ -744,6 +744,9 @@ ro: date: "Dată" default_columns: "Coloane implicite" description: "Descriere" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Afişare totaluri" due_date: "Finish date" estimated_hours: "Durata estimată" diff --git a/config/locales/crowdin/ru.yml b/config/locales/crowdin/ru.yml index b468e95228..e88450778d 100644 --- a/config/locales/crowdin/ru.yml +++ b/config/locales/crowdin/ru.yml @@ -747,6 +747,9 @@ ru: date: "Дата" default_columns: "Столбцы по умолчанию" description: "Описание" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Отображение суммы" due_date: "Дата окончания" estimated_hours: "Предполагаемое время" diff --git a/config/locales/crowdin/sk.yml b/config/locales/crowdin/sk.yml index 07fdc886e9..7950432d4b 100644 --- a/config/locales/crowdin/sk.yml +++ b/config/locales/crowdin/sk.yml @@ -748,6 +748,9 @@ sk: date: "Dátum" default_columns: "Predvolené stĺpce" description: "Popis" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Zobraziť súčty" due_date: "Dátum dokončenia" estimated_hours: "Predpokladaný čas" diff --git a/config/locales/crowdin/sl.yml b/config/locales/crowdin/sl.yml index 0847eae63b..8e706451da 100644 --- a/config/locales/crowdin/sl.yml +++ b/config/locales/crowdin/sl.yml @@ -746,6 +746,9 @@ sl: date: "Datum" default_columns: "Privzeti stolpci" description: "Opis" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Prikaži vsote" due_date: "Končni datum" estimated_hours: "Predvideni čas" diff --git a/config/locales/crowdin/sv.yml b/config/locales/crowdin/sv.yml index 1f9ae804ab..fd20ec633c 100644 --- a/config/locales/crowdin/sv.yml +++ b/config/locales/crowdin/sv.yml @@ -739,6 +739,9 @@ sv: date: "Datum" default_columns: "Standardkolumnerna" description: "Beskrivning" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Visa summor" due_date: "Slutdatum" estimated_hours: "Beräknad tid" diff --git a/config/locales/crowdin/tr.yml b/config/locales/crowdin/tr.yml index f9ffaf5ff0..6dfbc29989 100644 --- a/config/locales/crowdin/tr.yml +++ b/config/locales/crowdin/tr.yml @@ -740,6 +740,9 @@ tr: date: "Tarih" default_columns: "Varsayılan sütunlar" description: "Açıklama" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Toplamları görüntüle" due_date: "Bitiş tarihi" estimated_hours: "Tahmini süre" diff --git a/config/locales/crowdin/uk.yml b/config/locales/crowdin/uk.yml index 42aedad680..9143bb9e04 100644 --- a/config/locales/crowdin/uk.yml +++ b/config/locales/crowdin/uk.yml @@ -748,6 +748,9 @@ uk: date: "Дата" default_columns: "Типові колонки" description: "Опис" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Відображати суми" due_date: "Дата закінчення" estimated_hours: "Час (приблизно)" diff --git a/config/locales/crowdin/vi.yml b/config/locales/crowdin/vi.yml index 6940f4b221..fb347c7ad4 100644 --- a/config/locales/crowdin/vi.yml +++ b/config/locales/crowdin/vi.yml @@ -738,6 +738,9 @@ vi: date: "Ngày" default_columns: "Cột mặc định" description: "Mô tả" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "Hiển thị tổng" due_date: "Finish date" estimated_hours: "Thời gian dự kiến" diff --git a/config/locales/crowdin/zh-CN.yml b/config/locales/crowdin/zh-CN.yml index 8d81e13e35..c09004d505 100644 --- a/config/locales/crowdin/zh-CN.yml +++ b/config/locales/crowdin/zh-CN.yml @@ -731,6 +731,9 @@ zh-CN: date: "日期" default_columns: "默认的列" description: "描述" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "显示汇总" due_date: "完成日期" estimated_hours: "估计的时间" diff --git a/config/locales/crowdin/zh-TW.yml b/config/locales/crowdin/zh-TW.yml index 96555d4a3b..f3c277e81e 100644 --- a/config/locales/crowdin/zh-TW.yml +++ b/config/locales/crowdin/zh-TW.yml @@ -736,6 +736,9 @@ zh-TW: date: "日期" default_columns: "預設欄" description: "說明" + derived_due_date: "Derived finish date" + derived_estimated_time: "Derived estimated time" + derived_start_date: "Derived start date" display_sums: "顯示加總" due_date: "完成日期" estimated_hours: "預估時間" diff --git a/modules/boards/config/locales/crowdin/js-lt.yml b/modules/boards/config/locales/crowdin/js-lt.yml index cb302a7ef1..7e683a787e 100644 --- a/modules/boards/config/locales/crowdin/js-lt.yml +++ b/modules/boards/config/locales/crowdin/js-lt.yml @@ -33,18 +33,18 @@ lt: board_type: text: 'Lentos tipas' free: 'Paprasta lenta' - select_board_type: 'Please choose the type of board you need.' + select_board_type: 'Prašome pasirinkti reikalingą lentos tipą.' free_text: > Sukurkite lentą, kurioje galėsite laisvai kurti sąrašus, o juose rikiuoti darbų paketus. Darbo paketo perkėlimas tarp sąrašų visiškai nekeis darbo paketo. action: 'Veiksmų lenta' action_by_attribute: 'Veiksmų lenta (%{attribute})' action_text: > - Create a board with filtered lists on %{attribute} attribute. Moving work packages to other lists will update their attribute. + Sukurkite lentą su filtruotomis atributo %{attribute} reikšmėmis. Perkeliant darbo paketą tarp sąrašų bus keičiama atributo reikšmė. action_type: assignee: paskirtas status: būsena version: versija - subproject: subproject + subproject: sub-projektas select_attribute: "Veiksmo atributas" add_list_modal: warning: diff --git a/modules/boards/config/locales/crowdin/js-ru.yml b/modules/boards/config/locales/crowdin/js-ru.yml index 4969592bc2..d5e8f0bd82 100644 --- a/modules/boards/config/locales/crowdin/js-ru.yml +++ b/modules/boards/config/locales/crowdin/js-ru.yml @@ -33,18 +33,18 @@ ru: board_type: text: 'Тип доски' free: 'Базовая доска' - select_board_type: 'Please choose the type of board you need.' + select_board_type: 'Пожалуйста, выберите требуемый вам тип доски.' free_text: > Создайте доску, в которой вы можете свободно создавать списки и заказать пакеты работ внутри. Перемещение пакетов работ между списками не изменяет сам пакет работ. action: 'Доска действий' action_by_attribute: 'Доска действий (%{attribute})' action_text: > - Create a board with filtered lists on %{attribute} attribute. Moving work packages to other lists will update their attribute. + Создать доску с отфильтрованными по атрибуту %{attribute} списками. Перемещение рабочих пакетов в другие списки обновит их атрибут. action_type: assignee: правопреемник status: статус version: версия - subproject: subproject + subproject: подпроект select_attribute: "Атрибут действия" add_list_modal: warning: diff --git a/modules/boards/config/locales/crowdin/js-tr.yml b/modules/boards/config/locales/crowdin/js-tr.yml index 4f0dbd71b2..6abecc9f78 100644 --- a/modules/boards/config/locales/crowdin/js-tr.yml +++ b/modules/boards/config/locales/crowdin/js-tr.yml @@ -33,18 +33,18 @@ tr: board_type: text: 'Yazı tahtası tipi' free: 'Temel kurulu' - select_board_type: 'Please choose the type of board you need.' + select_board_type: 'Lütfen ihtiyacınız olan kart tipini seçin.' free_text: > Serbestçe listeler oluşturabileceğiniz ve çalışma paketlerinizi içinde sipariş edebileceğiniz bir tahta oluşturun. İş paketlerini listeler arasında taşımak, iş paketini değiştirmez. action: 'Eylem kurulu' action_by_attribute: 'Eylem kurulu (%{attribute})' action_text: > - Create a board with filtered lists on %{attribute} attribute. Moving work packages to other lists will update their attribute. + %{attribute} özniteliğinde filtrelenmiş listeleri olan bir pano oluşturun. Çalışma paketlerini diğer listelere taşımak özniteliklerini güncelleştirir. action_type: assignee: vekil status: durum version: Sürüm - subproject: subproject + subproject: Alt proje select_attribute: "Eylem özelliği" add_list_modal: warning: From 25469a0698516814dc45ee51fb2417e94319da83 Mon Sep 17 00:00:00 2001 From: Travis CI User Date: Sun, 9 Aug 2020 08:19:18 +0000 Subject: [PATCH 17/29] update locales from crowdin [ci skip] --- config/locales/crowdin/ar.yml | 56 ++++++------- .../avatars/config/locales/crowdin/js-ar.yml | 4 +- .../backlogs/config/locales/crowdin/ar.yml | 8 +- modules/bim/config/locales/crowdin/ar.yml | 80 +++++++++---------- modules/boards/config/locales/crowdin/ar.yml | 4 +- .../boards/config/locales/crowdin/js-ar.yml | 42 +++++----- 6 files changed, 97 insertions(+), 97 deletions(-) diff --git a/config/locales/crowdin/ar.yml b/config/locales/crowdin/ar.yml index 749f5e3017..1c0cd89037 100644 --- a/config/locales/crowdin/ar.yml +++ b/config/locales/crowdin/ar.yml @@ -28,38 +28,38 @@ ar: plugins: no_results_title_text: لا يوجد حالياً أية إضافات متاحة. custom_styles: - color_theme: "Color theme" - color_theme_custom: "(Custom)" + color_theme: "لون السمة" + color_theme_custom: "(تخصيص)" colors: - alternative-color: "Alternative" - content-link-color: "Link font" - primary-color: "Primary" - primary-color-dark: "Primary (dark)" - header-bg-color: "Header background" - header-item-bg-hover-color: "Header background on hover" - header-item-font-color: "Header font" - header-item-font-hover-color: "Header font on hover" - header-border-bottom-color: "Header border" - main-menu-bg-color: "Main menu background" - main-menu-bg-selected-background: "Main menu when selected" - main-menu-bg-hover-background: "Main menu on hover" - main-menu-font-color: "Main menu font" - main-menu-selected-font-color: "Main menu font when selected" - main-menu-hover-font-color: "Main menu font on hover" - main-menu-border-color: "Main menu border" + alternative-color: "البديل" + content-link-color: "خط الارتبط" + primary-color: "الأساسي" + primary-color-dark: "الأساسي (داكن)" + header-bg-color: "خلفية الترويسة" + header-item-bg-hover-color: "خلفية الترويسة على الحافة" + header-item-font-color: "خط الترويسة" + header-item-font-hover-color: "خط الترويسة عند الحافة" + header-border-bottom-color: "حدود الترويسة" + main-menu-bg-color: "خلفية القائمة الرئيسية" + main-menu-bg-selected-background: "القائمة الرئيسية عند تحديد" + main-menu-bg-hover-background: "القائمة الرئيسية على الحافة" + main-menu-font-color: "خط القائمة الرئيسية" + main-menu-selected-font-color: "خط القائمة الرئيسية عند تحديد" + main-menu-hover-font-color: "خط القائمة الرئيسية عند الحوالة" + main-menu-border-color: "حدود القائمة الرئيسية" custom_colors: "تخصيص الألوان" customize: "عدل مشروعك الخاص بالشعار الذي تريده.ملاحظه:هذا الشعار سوف يكون مرئي لجميع المستخدمين" - enterprise_notice: "As a special 'Thank you!' for their financial contribution to develop OpenProject, this tiny feature is only available for Enterprise Edition support subscribers." - manage_colors: "Edit color select options" + enterprise_notice: "كخاصية \"شكرًا!\" على مساهمتهم المالية لتطوير OpenProject، هذه الميزة الصغيرة متاحة فقط للمشتركين في إصدار المؤسسة." + manage_colors: "تعديل خيارات تحديد اللون" instructions: - alternative-color: "Strong accent color, typically used for the most important button on a screen." - content-link-color: "Font color of most of the links." - primary-color: "Main color." - primary-color-dark: "Typically a darker version of the main color used for hover effects." - header-item-bg-hover-color: "Background color of clickable header items when hovered with the mouse." - header-item-font-color: "Font color of clickable header items." - header-item-font-hover-color: "Font color of clickable header items when hovered with the mouse." - header-border-bottom-color: "Thin line under the header. Leave this field empty if you don't want any line." + alternative-color: "لون اللكنة القوية، يستخدم عادة لأهم زر على الشاشة." + content-link-color: "لون الخط لمعظم الروابط." + primary-color: "اللون الرئيسي." + primary-color-dark: "عادةً ما تكون نسخة داكنة من اللون الرئيسي المستخدم لتأثيرات الحرارة." + header-item-bg-hover-color: "لون الخلفية لعناصر الترويسة القابلة للنقر عند ربطها بالفأرة." + header-item-font-color: "لون الخط لعناصر الترويسة النقر عليها." + header-item-font-hover-color: "لون الخط لعناصر الترويسة القابلة للنقر عند ربطها بالفأرة." + header-border-bottom-color: "خط تحت الرأس. اترك هذا الحقل فارغاً إذا كنت لا تريد أي سطر." main-menu-bg-color: "Left side menu's background color." theme_warning: Changing the theme will overwrite you custom style. The design will then be lost. Are you sure you want to continue? enterprise: diff --git a/modules/avatars/config/locales/crowdin/js-ar.yml b/modules/avatars/config/locales/crowdin/js-ar.yml index 2f7e9fc66f..a562d9d7a6 100644 --- a/modules/avatars/config/locales/crowdin/js-ar.yml +++ b/modules/avatars/config/locales/crowdin/js-ar.yml @@ -5,11 +5,11 @@ ar: button_update: 'التحديث' avatars: label_choose_avatar: "Choose Avatar from file" - uploading_avatar: "Uploading your avatar." + uploading_avatar: "تحميل الصورة الرمزية" text_upload_instructions: | Upload your own custom avatar of 128 by 128 pixels. Larger files will be resized and cropped to match. A preview of your avatar will be shown before uploading, once you selected an image. - error_image_too_large: "Image is too large." + error_image_too_large: "الصورة كبيرة جداً." wrong_file_format: "Allowed formats are jpg, png, gif" empty_file_error: "Please upload a valid image (jpg, png, gif)" diff --git a/modules/backlogs/config/locales/crowdin/ar.yml b/modules/backlogs/config/locales/crowdin/ar.yml index 1a6824fcd5..711983ba2e 100644 --- a/modules/backlogs/config/locales/crowdin/ar.yml +++ b/modules/backlogs/config/locales/crowdin/ar.yml @@ -45,13 +45,13 @@ ar: work_package: attributes: version_id: - task_version_must_be_the_same_as_story_version: "must be the same as the parent story's version." + task_version_must_be_the_same_as_story_version: "يجب أن يكون نفس نسخة القصة الأصلية." parent_id: parent_child_relationship_across_projects: "هو غير صالح لأن مجموعة العمل '%{work_package_name}' هي مهمة عمل متراكم غير منجز ولذلك لا يمكن أن يكون أحد الأصول خارج المشروع الحالي." backlogs: add_new_story: "قصة جديدة" any: "أي" - backlog_settings: "Backlogs settings" + backlog_settings: "إعدادات السجلات المتراكمة" burndown_graph: "الرسم البياني لتقدم العمل" card_paper_size: "حجم الورق لطباعة البطاقة" chart_options: "خيارات الرسم البياني" @@ -102,8 +102,8 @@ ar: backlogs_velocity_missing: "لا يمكن احتساب السرعة في هذا المشروع" backlogs_velocity_varies: "السرعة تتفاوت بشكل ملحوظ على السباقات" backlogs_wiki_template: "نموذج لصفحة ويكي wiki الخاصة بالسباق" - backlogs_empty_title: "No versions are defined to be used in backlogs" - backlogs_empty_action_text: "To get started with backlogs, create a new version and assign it to a backlogs column." + backlogs_empty_title: "لا توجد إصدارات محددة لاستخدامها في قائمة الأعمال" + backlogs_empty_action_text: "للبدء مع قائمة الأعمال ، قم بإنشاء إصدار جديد و تعيينه إلى عمود قائمة الأعمال." button_edit_wiki: "عدّل صفحة ويكي wiki" error_intro_plural: "تم مصادفة الأخطاء التالية:" error_intro_singular: "تمت مصادفة الخطأ التالي:" diff --git a/modules/bim/config/locales/crowdin/ar.yml b/modules/bim/config/locales/crowdin/ar.yml index a9f1ac21ac..c4f6eb6107 100644 --- a/modules/bim/config/locales/crowdin/ar.yml +++ b/modules/bim/config/locales/crowdin/ar.yml @@ -4,17 +4,17 @@ ar: label_bim: 'BIM' bcf: label_bcf: 'BCF' - label_imported_failed: 'Failed imports of BCF topics' - label_imported_successfully: 'Successfully imported BCF topics' - issues: "Issues" - recommended: 'recommended' - not_recommended: 'not recommended' - no_viewpoints: 'No viewpoints' + label_imported_failed: 'فشل استيراد مواضيع BCF' + label_imported_successfully: 'تم استيراد موضوعات BCF بنجاح' + issues: "مشاكل" + recommended: 'موصى بها' + not_recommended: 'غير موصى بها' + no_viewpoints: 'لا توجد وجهات نظر' new_badge: "جديد" exceptions: - file_invalid: "BCF file invalid" + file_invalid: "ملف BCF غير صالح" x_bcf_issues: - zero: 'No BCF issues' + zero: 'لا توجد مشاكل BCF' zero: '%{count} BCF issues' one: 'One BCF issue' two: '%{count} BCF issues' @@ -30,37 +30,37 @@ ar: import_failed_unsupported_bcf_version: 'Failed to read the BCF file: The BCF version is not supported. Please ensure the version is at least %{minimal_version} or higher.' import_successful: 'Imported %{count} BCF issues' import_canceled: 'BCF-XML import canceled.' - type_not_active: "The issue type is not activated for this project." + type_not_active: "لم يتم تفعيل نوع المشكلة لهذا المشروع." import: - num_issues_found: '%{x_bcf_issues} are contained in the BCF-XML file, their details are listed below.' - button_prepare: 'Prepare import' - button_perform_import: 'Confirm import' + num_issues_found: '%{x_bcf_issues} موجودة في ملف BCF-XML ، وترد تفاصيلها أدناه.' + button_prepare: 'إعداد الاستيراد' + button_perform_import: 'تأكيد الاستيراد' button_proceed: 'Proceed with import' - button_back_to_list: 'Back to list' - no_permission_to_add_members: 'You do not have sufficient permissions to add them as members to the project.' - contact_project_admin: 'Contact your project admin to add them as members and start this import again.' - continue_anyways: 'Do you want to proceed and finish the import anyways?' - description: "Provide a BCF-XML v2.1 file to import into this project. You can examine its contents before performing the import." - invalid_types_found: 'Invalid topic type names found' - invalid_statuses_found: 'Invalid status names found' - invalid_priorities_found: 'Invalid priority names found' - invalid_emails_found: 'Invalid email addresses found' - unknown_emails_found: 'Unknown email addresses found' - unknown_property: 'Unknown property' - non_members_found: 'Non project members found' - import_types_as: 'Set all these types to' - import_statuses_as: 'Set all these statuses to' - import_priorities_as: 'Set all these priorities to' - invite_as_members_with_role: 'Invite them as members to the project "%{project}" with role' - add_as_members_with_role: 'Add them as members to the project "%{project}" with role' - no_type_provided: 'No type provided' - no_status_provided: 'No status provided' - no_priority_provided: 'No priority provided' - perform_description: "Do you want to import or update the issues listed above?" - replace_with_system_user: 'Replace them with "System" user' - import_as_system_user: 'Import them as "System" user.' + button_back_to_list: 'رجوع إلى القائمة' + no_permission_to_add_members: 'ليس لديك الصلاحيات الكافية لإضافتها كأعضاء في المشروع.' + contact_project_admin: 'اتصل بمشرف المشروع الخاص بك لإضافته كأعضاء وبدء هذا الاستيراد مرة أخرى.' + continue_anyways: 'هل تريد المضي قدما وإنهاء الاستيراد على أي حال؟' + description: "توفير ملف BCF-XML v2.1 للاستيراد إلى هذا المشروع. يمكنك فحص محتوياته قبل إجراء الاستيراد." + invalid_types_found: 'تم العثور على أسماء غير صالحة لنوع الموضوع' + invalid_statuses_found: 'تم العثور على أسماء غير صالحة لنوع الموضوع' + invalid_priorities_found: 'تم العثور على أسماء غير صالحة لنوع الموضوع' + invalid_emails_found: 'عنوان البريد الإلكتروني غير صالح' + unknown_emails_found: 'عنوان البريد الإلكتروني غير صالح' + unknown_property: 'خاصية غير معروفة' + non_members_found: 'لم يتم العثور على أعضاء المشروع' + import_types_as: 'تعيين جميع هذه الأنواع إلى' + import_statuses_as: 'تعيين جميع هذه الحالات إلى' + import_priorities_as: 'تعيين جميع هذه الأولويات إلى' + invite_as_members_with_role: 'قم بدعوتهم كأعضاء في المشروع "%{project}" مع دور' + add_as_members_with_role: 'قم بدعوتهم كأعضاء في المشروع "%{project}" مع دور' + no_type_provided: 'لا يوجد نوع' + no_status_provided: 'لا توجد حالة' + no_priority_provided: 'لا توجد أولوية' + perform_description: "هل تريد استيراد أو تحديث المشكلات المدرجة أعلاه؟" + replace_with_system_user: 'استبدالها بمستخدم "النظام"' + import_as_system_user: 'استيرادها كمستخدم "النظام".' what_to_do: "ماذا تريد أن تفعل؟" - work_package_has_newer_changes: "Outdated! This topic was not updated as the latest changes on the server were newer than the \"ModifiedDate\" of the imported topic. However, comments to the topic were imported." + work_package_has_newer_changes: "انتهت صلاحيتها! لم يتم تحديث هذا الموضوع لأن أحدث التغييرات على الخادم كانت أحدث من \"تاريخ التعديل\" للموضوع المستورد. غير أن التعليقات على الموضوع قد استُوردت." bcf_file_not_found: "Failed to locate BCF file. Please start the upload process again." export: format: @@ -69,13 +69,13 @@ ar: bcf_thumbnail: "BCF snapshot" project_module_bcf: "BCF" project_module_bim: "BCF" - permission_view_linked_issues: "View BCF issues" - permission_manage_bcf: "Import and manage BCF issues" + permission_view_linked_issues: "لا توجد مشاكل BCF" + permission_manage_bcf: "استيراد وإدارة مشكلات BCF" permission_delete_bcf: "Delete BCF issues" oauth: scopes: - bcf_v2_1: "Full access to the BCF v2.1 API" - bcf_v2_1_text: "Application will receive full read & write access to the OpenProject BCF API v2.1 to perform actions on your behalf." + bcf_v2_1: "الوصول الكامل إلى BCF v2.1 API" + bcf_v2_1_text: "سيحصل التطبيق على الوصول الكامل للقراءة والكتابة إلى OpenProject BCF API v2.1 لتنفيذ الإجراءات نيابة عنك." activerecord: models: bim/ifc_models/ifc_model: "IFC model" diff --git a/modules/boards/config/locales/crowdin/ar.yml b/modules/boards/config/locales/crowdin/ar.yml index 748d4bbf6c..63c0ff49d7 100644 --- a/modules/boards/config/locales/crowdin/ar.yml +++ b/modules/boards/config/locales/crowdin/ar.yml @@ -1,7 +1,7 @@ #English strings go here ar: - permission_show_board_views: "View boards" - permission_manage_board_views: "Manage boards" + permission_show_board_views: "شاهد لوحات المهمات" + permission_manage_board_views: "إدارة اللوحات" project_module_board_view: "Boards" boards: label_boards: "Boards" diff --git a/modules/boards/config/locales/crowdin/js-ar.yml b/modules/boards/config/locales/crowdin/js-ar.yml index 09e34e89e1..944404494a 100644 --- a/modules/boards/config/locales/crowdin/js-ar.yml +++ b/modules/boards/config/locales/crowdin/js-ar.yml @@ -2,36 +2,36 @@ ar: js: boards: - label_unnamed_board: 'Unnamed board' - label_unnamed_list: 'Unnamed list' - label_board_type: 'Board type' + label_unnamed_board: 'لوحة غير مسماة' + label_unnamed_list: 'قائمة غير مسماة' + label_board_type: 'نوع اللوحة' upsale: - teaser_text: 'Improve your agile project management with this flexible Boards view. Create as many boards as you like for anything you would like to keep track of.' - upgrade_to_ee_text: 'Boards is an Enterprise feature. Please upgrade to a paid plan.' - upgrade: 'Upgrade now' - personal_demo: 'Get a personal demo' + teaser_text: 'قم بتحسين إدارة مشروعك الرائع مع عرض المجالس المرنة هذا. قم بإنشاء أكبر عدد من اللوحات التي تريدها لأي شيء ترغب في متابعته.' + upgrade_to_ee_text: 'الجلسات هي ميزة المؤسسة. الرجاء الترقية إلى خطة مدفوعة.' + upgrade: 'الترقية الآن' + personal_demo: 'الحصول على عرض تجريبي' lists: - delete: 'Delete list' + delete: 'حذف القائمة' version: - is_locked: 'Version is locked. No items can be added to this version.' - is_closed: 'Version is closed. No items can be added to this version.' - close_version: 'Close version' - open_version: 'Open version' + is_locked: 'الإصدار مقفل. لا يمكن إضافة أي عناصر إلى هذا الإصدار.' + is_closed: 'الإصدار مقفل. لا يمكن إضافة أي عناصر إلى هذا الإصدار.' + close_version: 'النسخة الأساسية' + open_version: 'إصدار جديد' lock_version: 'Lock version' unlock_version: 'Unlock version' edit_version: 'Edit version' - show_version: 'Show version' + show_version: 'إظهار الإصدار' locked: 'مقفل' closed: 'مغلق' - new_board: 'New board' - add_list: 'Add list' - add_card: 'Add card' - error_attribute_not_writable: "Cannot move the work package, %{attribute} is not writable." - error_loading_the_list: "Error loading the list: %{error_message}" - error_permission_missing: "The permission to create public queries is missing" - click_to_remove_list: "Click to remove this list" + new_board: 'لوحة جديدة' + add_list: 'إنشاء قائمة' + add_card: 'إضافة بطاقة' + error_attribute_not_writable: "لا يمكن نقل حزمة العمل، %{attribute} غير قابل للكتابة." + error_loading_the_list: "خطأ في تحميل القائمة: %{error_message}" + error_permission_missing: "إذن إنشاء استفسارات عامة مفقود" + click_to_remove_list: "انقر لإزالة هذه القائمة" board_type: - text: 'Board type' + text: 'نوع اللوحة' free: 'Basic board' select_board_type: 'Please choose the type of board you need.' free_text: > From f434701c305ebc827d610b4cdcf76da4ee7e8076 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 10 Aug 2020 08:02:11 +0200 Subject: [PATCH 18/29] bump kramdown to fix CVE-2020-14001 --- Gemfile.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2565d4cd56..a7219c9b47 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -570,7 +570,8 @@ GEM multi_json (~> 1.0) rspec (>= 2.0, < 4.0) kgio (2.11.3) - kramdown (2.1.0) + kramdown (2.3.0) + rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) ladle (1.0.1) From b483e6cd3592ded02f7cbff5ea79666f9ab2247f Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Mon, 10 Aug 2020 08:47:53 +0100 Subject: [PATCH 19/29] consider validation errors during pending attachment creation --- app/models/attachment.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index ba9cc36cf5..39caceb330 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -265,11 +265,13 @@ class Attachment < ApplicationRecord downloads: -1 ) - # We need to use update_column because `file` is an uploader which expects a File (not a string) + # We need to do it like this because `file` is an uploader which expects a File (not a string) # to upload usually. But in this case the data has already been uploaded and we just point to it. - a.update_column :file, file_name + a[:file] = file_name - a.reload + a.reload unless a.new_record? + + a end def pending_direct_upload? From b85380f0684f58d2e4707daa4bcb0cab0a182eeb Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Mon, 10 Aug 2020 10:34:34 +0100 Subject: [PATCH 20/29] handle pending attachment validation errors --- app/models/attachment.rb | 6 +++--- lib/api/v3/attachments/attachments_by_container_api.rb | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 39caceb330..4484601c04 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -256,10 +256,10 @@ class Attachment < ApplicationRecord end def self.create_pending_direct_upload(file_name:, author:, container: nil, content_type: nil, file_size: 0) - a = create( + a = new( container: container, author: author, - content_type: content_type || "application/octet-stream", + content_type: content_type.presence || "application/octet-stream", filesize: file_size, digest: "", downloads: -1 @@ -269,7 +269,7 @@ class Attachment < ApplicationRecord # to upload usually. But in this case the data has already been uploaded and we just point to it. a[:file] = file_name - a.reload unless a.new_record? + a.save! a end diff --git a/lib/api/v3/attachments/attachments_by_container_api.rb b/lib/api/v3/attachments/attachments_by_container_api.rb index 10ee4be59a..046e2d8123 100644 --- a/lib/api/v3/attachments/attachments_by_container_api.rb +++ b/lib/api/v3/attachments/attachments_by_container_api.rb @@ -77,7 +77,9 @@ module API ) end - create_attachment metadata + with_handled_create_errors do + create_attachment metadata + end end def create_attachment(metadata) From 0792ed64433ebbc9d6b6a916207136f856299401 Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Mon, 10 Aug 2020 12:39:20 +0100 Subject: [PATCH 21/29] fixed fog file uploader path in pending attachments --- app/models/attachment.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 4484601c04..71d59f7693 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -270,6 +270,7 @@ class Attachment < ApplicationRecord a[:file] = file_name a.save! + a.reload # necessary so that the fog file uploader path is correct a end From ea79fb9a22ce9948a97acb29c90f689a3bfe663d Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Mon, 10 Aug 2020 14:13:25 +0100 Subject: [PATCH 22/29] use correct max file size in fog hash --- app/uploaders/direct_fog_uploader.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/uploaders/direct_fog_uploader.rb b/app/uploaders/direct_fog_uploader.rb index f4ddd3ac03..037a3cea8b 100644 --- a/app/uploaders/direct_fog_uploader.rb +++ b/app/uploaders/direct_fog_uploader.rb @@ -18,7 +18,19 @@ class DirectFogUploader < FogFileUploader uploader end - def self.direct_fog_hash(attachment:, success_action_redirect: nil, success_action_status: "201") + ## + # Generates the direct upload form for the given attachment. + # + # @param attachment [Attachment] The attachment for which a file is to be uploaded. + # @param success_action_redirect [String] URL to redirect to if successful (none by default, using status). + # @param success_action_status [String] The HTTP status to return on success (201 by default). + # @param max_file_size [Integer] The maximum file size to be allowed in bytes. + def self.direct_fog_hash( + attachment:, + success_action_redirect: nil, + success_action_status: "201", + max_file_size: Setting.attachment_max_size * 1024 + ) uploader = for_attachment attachment if success_action_redirect.present? @@ -29,7 +41,7 @@ class DirectFogUploader < FogFileUploader uploader.use_action_status = true end - hash = uploader.direct_fog_hash(enforce_utf8: false) + hash = uploader.direct_fog_hash(enforce_utf8: false, max_file_size: max_file_size) if success_action_redirect.present? hash.merge(success_action_redirect: success_action_redirect) From cc5bebdc81cbd8b07a4ed914f7999316534a13d8 Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Tue, 11 Aug 2020 09:33:00 +0100 Subject: [PATCH 23/29] removed silly 'hook registered' warning --- modules/webhooks/lib/open_project/webhooks.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/webhooks/lib/open_project/webhooks.rb b/modules/webhooks/lib/open_project/webhooks.rb index 2f4e59cb46..7863c106e8 100644 --- a/modules/webhooks/lib/open_project/webhooks.rb +++ b/modules/webhooks/lib/open_project/webhooks.rb @@ -51,7 +51,7 @@ module OpenProject def self.register_hook(name, &callback) raise "A hook named '#{name}' is already registered!" if find(name) - Rails.logger.warn "hook registered" + Rails.logger.debug "incoming webhook registered: #{name}" hook = Hook.new(name, &callback) @@registered_hooks << hook hook From 5e795a844a1846e442513357d78639353f4dd373 Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Tue, 11 Aug 2020 09:40:15 +0100 Subject: [PATCH 24/29] remove unnecessary redefinition of order_by_name scope --- app/models/version.rb | 2 +- modules/backlogs/app/models/sprint.rb | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/models/version.rb b/app/models/version.rb index 30091938fc..810cab1832 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -58,7 +58,7 @@ class Version < ApplicationRecord scope :systemwide, -> { where(sharing: 'system') } - scope :order_by_name, -> { order(Arel.sql("LOWER(#{Version.table_name}.name)")) } + scope :order_by_name, -> { order(Arel.sql("LOWER(#{Version.table_name}.name) ASC")) } def self.with_status_open where(status: 'open') diff --git a/modules/backlogs/app/models/sprint.rb b/modules/backlogs/app/models/sprint.rb index ca7056b0da..a8202670f7 100644 --- a/modules/backlogs/app/models/sprint.rb +++ b/modules/backlogs/app/models/sprint.rb @@ -38,9 +38,6 @@ class Sprint < Version scope :order_by_date, -> { reorder(Arel.sql("start_date ASC NULLS LAST, effective_date ASC NULLS LAST")) } - scope :order_by_name, -> { - order Arel.sql("#{Version.table_name}.name ASC") - } scope :apply_to, lambda { |project| where("#{Version.table_name}.project_id = #{project.id}" + From 0c582c4f35a59cc08fc21e81d2fc1a57640de92d Mon Sep 17 00:00:00 2001 From: Markus Kahl Date: Tue, 11 Aug 2020 12:44:09 +0100 Subject: [PATCH 25/29] use create to be sure we have an id later --- app/models/attachment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 71d59f7693..7be64b9dcd 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -256,7 +256,7 @@ class Attachment < ApplicationRecord end def self.create_pending_direct_upload(file_name:, author:, container: nil, content_type: nil, file_size: 0) - a = new( + a = create( container: container, author: author, content_type: content_type.presence || "application/octet-stream", From 4e3467c8d545b421a95af06284b2bfef06b1c224 Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Wed, 12 Aug 2020 10:16:45 +0200 Subject: [PATCH 26/29] Fixed: Removed useless error catch --- .../api/op-file-upload/op-direct-file-upload.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/app/components/api/op-file-upload/op-direct-file-upload.service.ts b/frontend/src/app/components/api/op-file-upload/op-direct-file-upload.service.ts index 700460c65d..cf2c68d542 100644 --- a/frontend/src/app/components/api/op-file-upload/op-direct-file-upload.service.ts +++ b/frontend/src/app/components/api/op-file-upload/op-direct-file-upload.service.ts @@ -141,11 +141,6 @@ export class OpenProjectDirectFileUploadService extends OpenProjectFileUploadSer }); return { url: res._links.addAttachment.href, form: form, response: res }; - }) - .catch((err) => { - console.log(err); - - return new FormData(); }); return result; From 50fa4503bb1590dbedb99b56ea58642e34776662 Mon Sep 17 00:00:00 2001 From: Travis CI User Date: Wed, 12 Aug 2020 08:18:01 +0000 Subject: [PATCH 27/29] update locales from crowdin [ci skip] --- config/locales/crowdin/lt.yml | 6 +++--- config/locales/crowdin/tr.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/locales/crowdin/lt.yml b/config/locales/crowdin/lt.yml index 07bc117561..c2a4b5717a 100644 --- a/config/locales/crowdin/lt.yml +++ b/config/locales/crowdin/lt.yml @@ -743,9 +743,9 @@ lt: date: "Data" default_columns: "Numatytieji stulpeliai" description: "Aprašymas" - derived_due_date: "Derived finish date" - derived_estimated_time: "Derived estimated time" - derived_start_date: "Derived start date" + derived_due_date: "Išvestinė pabaigos data" + derived_estimated_time: "Išvestinis numatytas laikas" + derived_start_date: "Išvestinė pradžios data" display_sums: "Rodyti suvestines" due_date: "Pabaigos data" estimated_hours: "Numatyta trukmė" diff --git a/config/locales/crowdin/tr.yml b/config/locales/crowdin/tr.yml index 6dfbc29989..a6c2f13c95 100644 --- a/config/locales/crowdin/tr.yml +++ b/config/locales/crowdin/tr.yml @@ -740,9 +740,9 @@ tr: date: "Tarih" default_columns: "Varsayılan sütunlar" description: "Açıklama" - derived_due_date: "Derived finish date" - derived_estimated_time: "Derived estimated time" - derived_start_date: "Derived start date" + derived_due_date: "Türetilmiş bitiş tarihi" + derived_estimated_time: "Türetilmiş tahmini süre" + derived_start_date: "Türetilmiş başlangıç tarihi" display_sums: "Toplamları görüntüle" due_date: "Bitiş tarihi" estimated_hours: "Tahmini süre" From faf6a01fbd1ca7c3b9fc9f7d816e2e664c5d915a Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Wed, 12 Aug 2020 14:20:50 +0200 Subject: [PATCH 28/29] Showing progress bar with completion percentage --- .../upload-progress.component.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/modules/common/notifications/upload-progress.component.ts b/frontend/src/app/modules/common/notifications/upload-progress.component.ts index f2a137cbf5..69c96abb14 100644 --- a/frontend/src/app/modules/common/notifications/upload-progress.component.ts +++ b/frontend/src/app/modules/common/notifications/upload-progress.component.ts @@ -26,7 +26,7 @@ // See docs/COPYRIGHT.rdoc for more details. //++ -import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; import {UploadFile, UploadHttpEvent, UploadInProgress} from "core-components/api/op-file-upload/op-file-upload.service"; import {HttpErrorResponse, HttpEventType, HttpProgressEvent} from "@angular/common/http"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; @@ -38,7 +38,8 @@ import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixi template: `
  • - {{value}}% + +

    0%

    @@ -51,11 +52,24 @@ export class UploadProgressComponent extends UntilDestroyedMixin implements OnIn @Output() public onError = new EventEmitter(); @Output() public onSuccess = new EventEmitter(); + @ViewChild('progressBar') + progressBar:ElementRef; + @ViewChild('progressPercentage') + progressPercentage:ElementRef; + public file:UploadFile; - public value:number = 0; public error:boolean = false; public completed = false; + set value(value:number) { + this.progressBar.nativeElement.value = value; + this.progressPercentage.nativeElement.innerText = `${value}%`; + + if (value === 100) { + this.progressBar.nativeElement.style.display = 'none'; + } + } + constructor(protected readonly I18n:I18nService) { super(); } From cab702b4a6594134cea9c611204c58a23e3cc269 Mon Sep 17 00:00:00 2001 From: Travis CI User Date: Thu, 13 Aug 2020 08:22:56 +0000 Subject: [PATCH 29/29] update locales from crowdin [ci skip] --- config/locales/crowdin/es.yml | 62 +++++++++---------- config/locales/crowdin/js-es.yml | 16 ++--- modules/bim/config/locales/crowdin/es.yml | 2 +- modules/bim/config/locales/crowdin/js-es.yml | 6 +- .../boards/config/locales/crowdin/js-es.yml | 6 +- .../grids/config/locales/crowdin/js-es.yml | 4 +- .../config/locales/crowdin/js-es.yml | 20 +++--- .../reporting/config/locales/crowdin/es.yml | 2 +- 8 files changed, 59 insertions(+), 59 deletions(-) diff --git a/config/locales/crowdin/es.yml b/config/locales/crowdin/es.yml index 7e82bd1610..42e1188e3b 100644 --- a/config/locales/crowdin/es.yml +++ b/config/locales/crowdin/es.yml @@ -78,7 +78,7 @@ es: is_active: mostrado actualmente is_inactive: no mostrado actualmente attribute_help_texts: - note_public: 'Any text and images you add to this field is publically visible to all logged in users!' + note_public: '¡Cualquier texto e imágenes que añadas a este campo es visible públicamente para todos los usuarios conectados!' text_overview: 'En esta vista, puede crear textos de ayuda personalizados para la vista de atributos. Después de definir estos textos, se pueden mostrar al hacer clic en el icono de ayuda junto al atributo al que pertenezcan.' label_plural: 'Textos de ayuda para atributos' show_preview: 'Vista previa del texto' @@ -90,15 +90,15 @@ es: no_results_content_text: Crear un nuevo modo de autenticación background_jobs: status: - error_requeue: "Job experienced an error but is retrying. The error was: %{message}" - cancelled_due_to: "Job was cancelled due to error: %{message}" + error_requeue: "El trabajo experimentó un error pero se está reintentando. El error fue: %{message}" + cancelled_due_to: "El trabajo ha sido cancelado debido al error: %{message}" ldap_auth_sources: technical_warning_html: | - Este formulario LDAP requiere conocimientos técnicos de su configuración de LDAP / activiar directorio
    + Este formulario LDAP requiere conocimientos técnicos para la configuración de su LDAP / Directorio Activo
    Visite nuestra documentación para obtener instrucciones detalladas. attribute_texts: name: Nombre arbitrario de la conexión LDAP - host: Nombre del anfitrion LDAP o dirección IP + host: Nombre del host LDAP o dirección IP login_map: La clave de atributo en LDAP que se utiliza para identificar el inicio de sesión único del usuario. Por lo general, esto será `uid` o`samAccountName`. generic_map: La clave de atributo en LDAP que está asignada al proyecto abierto `%{attribute}` atributo admin_map_html: "Opcional: la clave de atributo en LDAP que si esta presente marca al usuario del proyecto abierto como administrador. Deje en blanco cuando tenga dudas." @@ -280,7 +280,7 @@ es: overview: no_results_title_text: Actualmente no hay paquetes de trabajo asignados a esta versión. wiki: - page_not_editable_index: The requested page does not (yet) exist. You have been redirected to the index of all wiki pages. + page_not_editable_index: La página solicitada no existe (todavía). Has sido redirigido al inicio las páginas de la wiki. no_results_title_text: Actualmente no hay paginas de wiki. index: no_results_content_text: Añadir una nueva página wiki @@ -417,7 +417,7 @@ es: types: "Tipos" versions: "Versiones" work_packages: "Paquetes de trabajo" - templated: 'Template project' + templated: 'Plantilla del proyecto' projects/status: code: 'Estado' explanation: 'Descripción del estado' @@ -489,7 +489,7 @@ es: parent_work_package: "Padre" priority: "Prioridad" progress: "Progreso (%)" - schedule_manually: "Manual scheduling" + schedule_manually: "Programación manual" spent_hours: "Tiempo empleado" spent_time: "Tiempo empleado" subproject: "Subproyecto" @@ -737,9 +737,9 @@ es: date: "Fecha" default_columns: "Columnas predeterminadas" description: "Descripción" - derived_due_date: "Derived finish date" - derived_estimated_time: "Derived estimated time" - derived_start_date: "Derived start date" + derived_due_date: "Fecha final derivada" + derived_estimated_time: "Tiempo estimado derivado" + derived_start_date: "Fecha de comienzo deseada" display_sums: "Mostrar sumas" due_date: "Fecha de finalización" estimated_hours: "Tiempo estimado" @@ -890,7 +890,7 @@ es: - "Oct" - "Nov" - "Dec" - abbr_week: 'Wk' + abbr_week: 'Sem' day_names: - "Domingo" - "Lunes" @@ -1126,8 +1126,8 @@ es: work_package_edit: 'Paquete de trabajo editado' work_package_note: 'Nota de paquete de trabajo añadido' export: - your_work_packages_export: "Your work packages export" - succeeded: "The export has completed successfully." + your_work_packages_export: "Exportar paquetes de trabajo" + succeeded: "La exportación se ha completado correctamente." format: atom: "Atomo" csv: "CSV" @@ -1765,16 +1765,16 @@ es: mail_body_account_information: "Información de su cuenta" mail_body_account_information_external: "Puede usar su %{value} cuenta para ingresar." mail_body_lost_password: "Para cambiar su contraseña, haga clic en el siguiente enlace:" - mail_body_register: "Welcome to OpenProject. Please activate your account by clicking on this link:" - mail_body_register_header_title: "Project member invitation email" - mail_body_register_user: "Dear %{name}, " + mail_body_register: "Bienvenido a OpenProject. Por favor, active su cuenta haciendo clic en este enlace:" + mail_body_register_header_title: "Correo electrónico de invitación al miembro del proyecto" + mail_body_register_user: "Estimado/a %{name}," mail_body_register_links_html: | - Please feel free to browse our youtube channel (%{youtube_link}) where we provide a webinar (%{webinar_link}) - and “Get started” videos (%{get_started_link}) to make your first steps in OpenProject as easy as possible. + Por favor, no dude en navegar por nuestro canal de youtube (%{youtube_link}) donde proporcionamos un webinar (%{webinar_link}) + y videos "Get started" (%{get_started_link}) para hacer que sus primeros pasos en OpenProject sean lo más fáciles posible.
    - If you have any further questions, consult our documentation (%{documentation_link}) or contact us (%{contact_us_link}). - mail_body_register_closing: "Your OpenProject team" - mail_body_register_ending: "Stay connected! Kind regards," + Si tiene más preguntas, consulte nuestra documentación (%{documentation_link}) o póngase en contacto con nosotros (%{contact_us_link}). + mail_body_register_closing: "Tu equipo de OpenProject" + mail_body_register_ending: "¡Mantente conectado! Saludos," mail_body_reminder: "%{count} paquete(s) de trabajo que le fueron asignados vencen en los próximos %{days}:" mail_body_group_reminder: "%{count} paquete(s) de trabajo asignado(s) al grupo “%{group}” vencerán en los próximos %{days} días:" mail_body_wiki_content_added: "La página wiki de '%{id}' ha sido añadida por %{author}." @@ -1930,7 +1930,7 @@ es: permission_manage_project_activities: "Gestionar actividades del proyecto" permission_manage_public_queries: "Administrar vistas públicas" permission_manage_repository: "Gestionar repositorio" - permission_manage_subtasks: "Manage work package hierarchies" + permission_manage_subtasks: "Administrar jerarquías de paquetes de trabajo" permission_manage_versions: "Administrar versiones" permission_manage_wiki: "Administrar wiki" permission_manage_wiki_menu: "Administrar menú wiki" @@ -1967,10 +1967,10 @@ es: title: Cambiar el identificador de proyecto template: copying: > - Your project is being created from the selected template project. You will be notified by mail as soon as the project is available. - use_template: 'Use template' - make_template: 'Set as template' - remove_from_templates: 'Remove from templates' + Tu proyecto está siendo creado a partir de la plantilla seleccionada. Serás notificado por correo electrónico tan pronto como el proyecto esté disponible. + use_template: 'Usar plantilla' + make_template: 'Establecer como plantilla' + remove_from_templates: 'Eliminar de plantillas' archive: are_you_sure: "¿Está seguro que desea archivar el proyecto '%{name}'?" archived: "Archivado" @@ -1992,8 +1992,8 @@ es: assigned_to_role: "Asignación de roles" member_of_group: "Asignación de grupo" assignee_or_group: "Grupo al que pertenece o al que está asignado" - subproject_id: "Including Subproject" - only_subproject_id: "Only subproject" + subproject_id: "Incluyendo Subproyecto" + only_subproject_id: "Sólo subproyecto" name_or_identifier: "Nombre o identificador" repositories: at_identifier: 'en %{identifier}' @@ -2099,8 +2099,8 @@ es: warnings: cannot_annotate: "No se pueden realizar notas sobre este fichero." scheduling: - activated: 'activated' - deactivated: 'deactivated' + activated: 'Habilitado' + deactivated: 'deshabilitado' search_input_placeholder: "Buscar..." setting_email_delivery_method: "Método de envío de correo electrónico" setting_sendmail_location: "Ubicación del ejecutable de sendmail" diff --git a/config/locales/crowdin/js-es.yml b/config/locales/crowdin/js-es.yml index 8401708436..8236cb1884 100644 --- a/config/locales/crowdin/js-es.yml +++ b/config/locales/crowdin/js-es.yml @@ -168,7 +168,7 @@ es: trial: confirmation: "Confirmación de dirección de correo electrónico" confirmation_info: > - We sent you an email on %{date} to %{email}. Please check your inbox and click the confirmation link provided to start your 14 days trial. + Le hemos enviado un correo electrónico a %{email} el %{date}. Por favor, compruebe su bandeja de entrada y haga clic en el enlace de confirmación que le hemos enviado para comenzar con su prueba gratuita de 14 días. form: general_consent: > Estoy de acuerdo con los
    Términos del servicio y la Política de privacidad. @@ -398,7 +398,7 @@ es: label_sum_for: "Suma para" label_subject: "Asunto" label_this_week: "esta semana" - label_today: "Today" + label_today: "Hoy" label_time_entry_plural: "Tiempo empleado" label_up: "Arriba" label_user_plural: "Usuarios" @@ -585,8 +585,8 @@ es: field_value_enter_prompt: "Introduzca un valor para '%{field}'" project_menu_details: "Detalles" scheduling: - manual: 'Manual scheduling' - automatic: 'Automatic scheduling' + manual: 'Programación manual' + automatic: 'Programación automática' sort: sorted_asc: 'Orden ascendiente aplicado ' sorted_dsc: 'Orden descendiente aplicado ' @@ -799,8 +799,8 @@ es: duplicate_query_title: "El nombre de la vista ya existe. ¿Quiere cambiarlo de todos modos?" text_no_results: "No se encontraron vistas que coincidan." scheduling: - is_parent: "The dates of this work package are automatically deduced from its children. Activate 'Manual scheduling' to set the dates." - is_switched_from_manual_to_automatic: "The dates of this work package may need to be recalculated after switching from manual to automatic scheduling due to relationships with other work packages." + is_parent: "Las fechas de este paquete de trabajo son deducidas automáticamente de sus hijos. Active 'Programación manual' para establecer las fechas." + is_switched_from_manual_to_automatic: "Las fechas de este paquete de trabajo pueden necesitar ser recalculadas después de pasar de programación manual a programación automática debido a las relaciones con otros paquetes de trabajo." table: configure_button: 'Configurar tabla de paquetes de trabajo' summary: "Tabla con filas de paquetes de trabajo y columnas con sus atributos." @@ -890,8 +890,8 @@ es: confirm_deletion_children: "Reconozco que TODOS los descendientes de los paquetes de trabajo enumerados se eliminarán recursivamente." deletes_children: "También se eliminarán de forma recursiva todos los paquetes de trabajo secundarios y sus descendientes." destroy_time_entry: - title: "Confirm deletion of time entry" - text: "Are you sure you want to delete the following time entry?" + title: "Confirmar la eliminación de la entrada de tiempo" + text: "¿Realmente quiere eliminar la siguiente entrada de tiempo?" notice_no_results_to_display: "No se pueden mostrar resultados visibles." notice_successful_create: "Creación exitosa." notice_successful_delete: "Eliminado con éxito." diff --git a/modules/bim/config/locales/crowdin/es.yml b/modules/bim/config/locales/crowdin/es.yml index 6142b2c199..1638c3a701 100644 --- a/modules/bim/config/locales/crowdin/es.yml +++ b/modules/bim/config/locales/crowdin/es.yml @@ -101,7 +101,7 @@ es: snapshot_data_blank: "«snapshot_data» tiene que especificarse." unsupported_key: "Se ha incluido una propiedad JSON no admitida." bim/bcf/issue: - uuid_already_taken: "Can't import this BCF issue as there already is another with the same GUID. Could it be that this BCF issue had already been imported into a different project?" + uuid_already_taken: "No se puede importar el defecto de BCF porque ya existe otro con el mismo GUID ¿Podría ser \nque este defecto BCF ya fue importado a un proyecto diferente?" ifc_models: label_ifc_models: 'Modelos IFC' label_new_ifc_model: 'Nuevo modelo IFC' diff --git a/modules/bim/config/locales/crowdin/js-es.yml b/modules/bim/config/locales/crowdin/js-es.yml index 6519683a65..e0c4212a2a 100644 --- a/modules/bim/config/locales/crowdin/js-es.yml +++ b/modules/bim/config/locales/crowdin/js-es.yml @@ -18,8 +18,8 @@ es: manage: 'Administrar modelos' views: viewer: 'Visor' - split: 'Viewer and table' + split: 'Visor y tabla' split_cards: 'Visor y tarjetas' revit: - revit_add_in: "Revit Add-In" - revit_add_in_settings: "Revit Add-In settings" + revit_add_in: "Revisar agregado" + revit_add_in_settings: "Revisar ajustes de complemento" diff --git a/modules/boards/config/locales/crowdin/js-es.yml b/modules/boards/config/locales/crowdin/js-es.yml index cae17cbd5a..fa6efb5ee7 100644 --- a/modules/boards/config/locales/crowdin/js-es.yml +++ b/modules/boards/config/locales/crowdin/js-es.yml @@ -33,18 +33,18 @@ es: board_type: text: 'Tipo de panel' free: 'Panel básico' - select_board_type: 'Please choose the type of board you need.' + select_board_type: 'Por favor elija el tipo de tablero que necesite.' free_text: > Cree un panel donde pueda crear libremente listas y ordenar los paquetes de trabajo que contenga. Al mover los paquetes de trabajo entre las listas, no se cambia del paquete de trabajo en sí. action: 'Panel de acciones' action_by_attribute: 'Panel de acciones (%{attribute})' action_text: > - Create a board with filtered lists on %{attribute} attribute. Moving work packages to other lists will update their attribute. + Crear un tablero con listas filtradas en el atributo %{attribute} . Moviendo los paquetes de trabajo a otras listas actualizará su atributo. action_type: assignee: asignado status: estado version: versión - subproject: subproject + subproject: Subproyecto select_attribute: "Atributo de acción" add_list_modal: warning: diff --git a/modules/grids/config/locales/crowdin/js-es.yml b/modules/grids/config/locales/crowdin/js-es.yml index aeaa7eaead..13b14fc2e9 100644 --- a/modules/grids/config/locales/crowdin/js-es.yml +++ b/modules/grids/config/locales/crowdin/js-es.yml @@ -3,7 +3,7 @@ es: grid: add_widget: 'Agregar widget' remove: 'Quitar widget' - configure: 'Configure widget' + configure: 'Configurar widget' upsale: text: "Algunos widgets, como el widget del gráfico de paquetes de trabajo, solo están disponibles en la " link: 'edición Enterprise.' @@ -40,7 +40,7 @@ es: no_results: 'No hay subproyectos.' time_entries_current_user: title: 'Tiempo que he invertido' - displayed_days: 'Days displayed in the widget:' + displayed_days: 'Días mostrados en el widget:' time_entries_list: title: 'Tiempo de gastos (últimos 7 días)' no_results: 'Sin entradas temporales en los últimos 7 días.' diff --git a/modules/job_status/config/locales/crowdin/js-es.yml b/modules/job_status/config/locales/crowdin/js-es.yml index 9e3af4cf5f..5751a26df1 100644 --- a/modules/job_status/config/locales/crowdin/js-es.yml +++ b/modules/job_status/config/locales/crowdin/js-es.yml @@ -1,14 +1,14 @@ es: js: job_status: - download_starts: 'The download should start automatically.' - click_to_download: 'Or click here to download.' - title: 'Background job status' - redirect: 'You are being redirected.' + download_starts: 'La descarga debería iniciar automáticamente.' + click_to_download: 'O haga click aquí para descargar.' + title: 'Estado de trabajo en segundo plano.' + redirect: 'Usted está siendo redirigido.' generic_messages: - not_found: 'This job could not be found.' - in_queue: 'The job has been queued and will be processed shortly.' - in_process: 'The job is currently being processed.' - error: 'The job has failed to complete.' - cancelled: 'The job has been cancelled due to an error.' - success: 'The job completed successfully.' + not_found: 'No se pudo encontrar esta tarea.' + in_queue: 'La tarea se ha puesto en cola y se procesará en breve.' + in_process: 'La tarea está siendo procesado.' + error: 'La tarea no se pudo completar.' + cancelled: 'La tarea se cancelo debido a un error.' + success: 'La tarea se se completo con éxito.' diff --git a/modules/reporting/config/locales/crowdin/es.yml b/modules/reporting/config/locales/crowdin/es.yml index e22199739a..5edf832e7c 100644 --- a/modules/reporting/config/locales/crowdin/es.yml +++ b/modules/reporting/config/locales/crowdin/es.yml @@ -22,7 +22,7 @@ es: button_save_as: "Guardar informe como..." comments: "Comentario" - cost_reports_title: "Time and costs" + cost_reports_title: "Tiempo y costos" label_cost_report: "Informe costo" label_cost_report_plural: "Reportes de costo" description_drill_down: "Ver detalles"