Merge pull request #7339 from opf/feature/version-boards
[30250] Version board editing [ci skip]pull/7343/head
commit
e4b3185d6d
@ -0,0 +1,56 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// 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 doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
import {MultiInputState} from "reactivestates"; |
||||
import {Injectable} from '@angular/core'; |
||||
import {UserResource} from 'core-app/modules/hal/resources/user-resource'; |
||||
import {StateCacheService} from 'core-components/states/state-cache.service'; |
||||
import {UserDmService} from 'core-app/modules/hal/dm-services/user-dm.service'; |
||||
import {States} from 'core-components/states.service'; |
||||
import {StatusDmService} from "core-app/modules/hal/dm-services/status-dm.service"; |
||||
import {StatusResource} from "core-app/modules/hal/resources/status-resource"; |
||||
|
||||
@Injectable() |
||||
export class StatusCacheService extends StateCacheService<StatusResource> { |
||||
|
||||
constructor(readonly states:States, |
||||
readonly statusDm:StatusDmService) { |
||||
super(); |
||||
} |
||||
|
||||
protected load(id:number|string):Promise<StatusResource> { |
||||
return this.statusDm.one(id); |
||||
} |
||||
|
||||
protected loadAll(ids:string[]):Promise<unknown> { |
||||
return this.statusDm.list(); |
||||
} |
||||
|
||||
protected get multiState():MultiInputState<StatusResource> { |
||||
return this.states.statuses; |
||||
} |
||||
} |
@ -0,0 +1,58 @@ |
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// 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 doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
import {MultiInputState} from "reactivestates"; |
||||
import {Injectable} from '@angular/core'; |
||||
import {UserResource} from 'core-app/modules/hal/resources/user-resource'; |
||||
import {StateCacheService} from 'core-components/states/state-cache.service'; |
||||
import {UserDmService} from 'core-app/modules/hal/dm-services/user-dm.service'; |
||||
import {States} from 'core-components/states.service'; |
||||
import {StatusDmService} from "core-app/modules/hal/dm-services/status-dm.service"; |
||||
import {StatusResource} from "core-app/modules/hal/resources/status-resource"; |
||||
import {VersionResource} from "core-app/modules/hal/resources/version-resource"; |
||||
import {VersionDmService} from "core-app/modules/hal/dm-services/version-dm.service"; |
||||
|
||||
@Injectable() |
||||
export class VersionCacheService extends StateCacheService<VersionResource> { |
||||
|
||||
constructor(readonly states:States, |
||||
readonly versionDm:VersionDmService) { |
||||
super(); |
||||
} |
||||
|
||||
protected load(id:number|string):Promise<VersionResource> { |
||||
return this.versionDm.one(id); |
||||
} |
||||
|
||||
protected loadAll(ids:string[]):Promise<unknown> { |
||||
return this.versionDm.list(); |
||||
} |
||||
|
||||
protected get multiState():MultiInputState<VersionResource> { |
||||
return this.states.versions; |
||||
} |
||||
} |
@ -1,133 +0,0 @@ |
||||
import {Injectable} from "@angular/core"; |
||||
import {BoardListsService} from "core-app/modules/boards/board/board-list/board-lists.service"; |
||||
import {Board} from "core-app/modules/boards/board/board"; |
||||
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 {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {FilterOperator} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
import {VersionResource} from "core-app/modules/hal/resources/version-resource"; |
||||
import {VersionDmService} from "core-app/modules/hal/dm-services/version-dm.service"; |
||||
import {CurrentProjectService} from "core-components/projects/current-project.service"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {VersionAutocompleterComponent} from "core-app/modules/common/autocomplete/version-autocompleter.component"; |
||||
|
||||
@Injectable() |
||||
export class BoardVersionActionService implements BoardActionService { |
||||
|
||||
constructor(protected boardListsService:BoardListsService, |
||||
protected I18n:I18nService, |
||||
protected versionDm:VersionDmService, |
||||
protected currentProject:CurrentProjectService, |
||||
protected pathHelper:PathHelperService) { |
||||
} |
||||
|
||||
public get localizedName() { |
||||
return this.I18n.t('js.work_packages.properties.version'); |
||||
} |
||||
|
||||
/** |
||||
* Returns the current filter value if any |
||||
* @param query |
||||
* @returns /api/v3/versions/:id if a version filter exists |
||||
*/ |
||||
public getFilterValue(query:QueryResource):string|undefined { |
||||
const filter = _.find(query.filters, filter => filter.id === 'version'); |
||||
|
||||
if (filter) { |
||||
const value = filter.values[0] as string|HalResource; |
||||
return (value instanceof HalResource) ? value.href! : value; |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
public addActionQueries(board:Board):Promise<Board> { |
||||
return this.getVersions() |
||||
.then((results) => { |
||||
return Promise.all<unknown>( |
||||
results.map((version:VersionResource) => { |
||||
if (version.definingProject.name === this.currentProject.name) { |
||||
return this.addActionQuery(board, version); |
||||
} |
||||
|
||||
return Promise.resolve(board); |
||||
}) |
||||
) |
||||
.then(() => board); |
||||
}); |
||||
} |
||||
|
||||
public addActionQuery(board:Board, value:HalResource):Promise<Board> { |
||||
let params:any = { |
||||
name: value.name, |
||||
}; |
||||
|
||||
let filter = { version: { |
||||
operator: '=' as FilterOperator, |
||||
values: [value.id] |
||||
}}; |
||||
|
||||
return this.boardListsService.addQuery(board, params, [filter]); |
||||
} |
||||
|
||||
/** |
||||
* Return available versions for new lists, given the list of active |
||||
* queries in the board. |
||||
* |
||||
* @param board The board we're looking at |
||||
* @param queries The active set of queries |
||||
*/ |
||||
public getAvailableValues(board:Board, queries:QueryResource[]):Promise<HalResource[]> { |
||||
const active = new Set( |
||||
queries.map(query => this.getFilterValue(query)) |
||||
); |
||||
|
||||
return this.getVersions() |
||||
.then(results => |
||||
results.filter(version => !active.has(version.href!)) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Adds an entry to the list menu to edit the version if allowed |
||||
* @param {HalResource} actionAttributeValue |
||||
* @returns {Promise<any>} |
||||
*/ |
||||
public getAdditionalListMenuItems(actionAttributeValue:HalResource):Promise<any> { |
||||
let items:any = []; |
||||
const actionID = actionAttributeValue.id; |
||||
|
||||
if (actionID) { |
||||
return this.versionDm.one(parseInt(actionID)).then((version) => { |
||||
// Show entry only with correct permissions
|
||||
if (version.$links.update) { |
||||
items.push( |
||||
{ |
||||
linkText: this.I18n.t('js.boards.lists.edit_version'), |
||||
externalAction: () => window.open(this.pathHelper.versionEditPath(actionID), '_blank') |
||||
} |
||||
); |
||||
} |
||||
|
||||
return items; |
||||
}); |
||||
} else { |
||||
return Promise.resolve(items); |
||||
} |
||||
} |
||||
|
||||
public autocompleterComponent() { |
||||
return VersionAutocompleterComponent; |
||||
} |
||||
|
||||
private getVersions():Promise<VersionResource[]> { |
||||
if (this.currentProject.id === null) { |
||||
return Promise.resolve([]); |
||||
} |
||||
|
||||
return this.versionDm |
||||
.listForProject(this.currentProject.id) |
||||
.then(collection => collection.elements.filter(version => version.status === 'open')); |
||||
} |
||||
} |
@ -0,0 +1,223 @@ |
||||
import {Injectable} from "@angular/core"; |
||||
import {BoardListsService} from "core-app/modules/boards/board/board-list/board-lists.service"; |
||||
import {Board} from "core-app/modules/boards/board/board"; |
||||
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 {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {FilterOperator} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
import {VersionResource} from "core-app/modules/hal/resources/version-resource"; |
||||
import {VersionDmService} from "core-app/modules/hal/dm-services/version-dm.service"; |
||||
import {CurrentProjectService} from "core-components/projects/current-project.service"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {VersionAutocompleterComponent} from "core-app/modules/common/autocomplete/version-autocompleter.component"; |
||||
import {OpContextMenuItem} from "core-components/op-context-menu/op-context-menu.types"; |
||||
import {LinkHandling} from "core-app/modules/common/link-handling/link-handling"; |
||||
import {StateService} from "@uirouter/core"; |
||||
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service"; |
||||
import {StatusResource} from "core-app/modules/hal/resources/status-resource"; |
||||
import {VersionCacheService} from "core-components/versions/version-cache.service"; |
||||
import {VersionBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/version/version-board-header.component"; |
||||
|
||||
@Injectable() |
||||
export class BoardVersionActionService implements BoardActionService { |
||||
|
||||
constructor(protected boardListsService:BoardListsService, |
||||
protected I18n:I18nService, |
||||
protected versionDm:VersionDmService, |
||||
protected versionCache:VersionCacheService, |
||||
protected currentProject:CurrentProjectService, |
||||
protected wpNotifications:WorkPackageNotificationService, |
||||
protected state:StateService, |
||||
protected pathHelper:PathHelperService) { |
||||
} |
||||
|
||||
public get localizedName() { |
||||
return this.I18n.t('js.work_packages.properties.version'); |
||||
} |
||||
|
||||
/** |
||||
* Returns the current filter value if any |
||||
* @param query |
||||
* @returns /api/v3/versions/:id if a version filter exists |
||||
*/ |
||||
public getFilterHref(query:QueryResource):string|undefined { |
||||
const filter = _.find(query.filters, filter => filter.id === 'version'); |
||||
|
||||
if (filter) { |
||||
const value = filter.values[0] as string|HalResource; |
||||
return (value instanceof HalResource) ? value.href! : value; |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
/** |
||||
* Returns the loaded status |
||||
* @param query |
||||
*/ |
||||
public getLoadedFilterValue(query:QueryResource):Promise<undefined|VersionResource> { |
||||
const href = this.getFilterHref(query); |
||||
|
||||
if (href) { |
||||
const id = HalResource.idFromLink(href); |
||||
return this.versionCache.require(id); |
||||
} else { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
} |
||||
|
||||
public addActionQueries(board:Board):Promise<Board> { |
||||
return this.getVersions() |
||||
.then((results) => { |
||||
return Promise.all<unknown>( |
||||
results.map((version:VersionResource) => { |
||||
if (version.isOpen() && version.definingProject.name === this.currentProject.name) { |
||||
return this.addActionQuery(board, version); |
||||
} |
||||
|
||||
return Promise.resolve(board); |
||||
}) |
||||
) |
||||
.then(() => board); |
||||
}); |
||||
} |
||||
|
||||
public addActionQuery(board:Board, value:HalResource):Promise<Board> { |
||||
let params:any = { |
||||
name: value.name, |
||||
}; |
||||
|
||||
let filter = { |
||||
version: { |
||||
operator: '=' as FilterOperator, |
||||
values: [value.id] |
||||
} |
||||
}; |
||||
|
||||
return this.boardListsService.addQuery(board, params, [filter]); |
||||
} |
||||
|
||||
/** |
||||
* Return available versions for new lists, given the list of active |
||||
* queries in the board. |
||||
* |
||||
* @param board The board we're looking at |
||||
* @param queries The active set of queries |
||||
*/ |
||||
public getAvailableValues(board:Board, queries:QueryResource[]):Promise<HalResource[]> { |
||||
const active = new Set( |
||||
queries.map(query => this.getFilterHref(query)) |
||||
); |
||||
|
||||
return this.getVersions() |
||||
.then(results => |
||||
results.filter(version => !active.has(version.href!)) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Adds an entry to the list menu to edit the version if allowed |
||||
* @param {QueryResource} active query |
||||
* @returns {Promise<any>} |
||||
*/ |
||||
public getAdditionalListMenuItems(query:QueryResource):Promise<OpContextMenuItem[]> { |
||||
return this |
||||
.getLoadedFilterValue(query) |
||||
.then(version => { |
||||
if (version) { |
||||
return this.buildItemsForVersion(version); |
||||
} else { |
||||
return []; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public autocompleterComponent() { |
||||
return VersionAutocompleterComponent; |
||||
} |
||||
|
||||
public headerComponent() { |
||||
return VersionBoardHeaderComponent; |
||||
} |
||||
|
||||
public dragIntoAllowed(query:QueryResource, value:HalResource|undefined) { |
||||
return value instanceof VersionResource && value.isOpen(); |
||||
} |
||||
|
||||
private getVersions():Promise<VersionResource[]> { |
||||
if (this.currentProject.id === null) { |
||||
return Promise.resolve([]); |
||||
} |
||||
|
||||
return this.versionDm |
||||
.listForProject(this.currentProject.id) |
||||
.then(collection => collection.elements); |
||||
} |
||||
|
||||
private patchVersionStatus(version:VersionResource, newStatus:'open'|'closed'|'locked') { |
||||
this.versionDm |
||||
.patch(version, {status: newStatus }) |
||||
.then((version) => { |
||||
this.versionCache.updateValue(version.id!, version); |
||||
this.state.go('.', {}, { reload: true }); |
||||
}) |
||||
.catch(error => this.wpNotifications.handleRawError(error)); |
||||
} |
||||
|
||||
private buildItemsForVersion(version:VersionResource):OpContextMenuItem[] { |
||||
const id = version.id!; |
||||
return [ |
||||
{ |
||||
// Lock version
|
||||
hidden: !version.isOpen(), |
||||
linkText: this.I18n.t('js.boards.version.lock_version'), |
||||
onClick: () => { |
||||
this.patchVersionStatus(version, 'locked'); |
||||
return true; |
||||
} |
||||
}, |
||||
{ |
||||
// Unlock version
|
||||
hidden: !version.isLocked(), |
||||
linkText: this.I18n.t('js.boards.version.unlock_version'), |
||||
onClick: () => { |
||||
this.patchVersionStatus(version, 'open'); |
||||
return true; |
||||
} |
||||
}, |
||||
{ |
||||
// Close version
|
||||
hidden: version.isClosed(), |
||||
linkText: this.I18n.t('js.boards.version.close_version'), |
||||
onClick: () => { |
||||
this.patchVersionStatus(version, 'closed'); |
||||
return true; |
||||
} |
||||
}, |
||||
{ |
||||
// Open version
|
||||
hidden: !version.isClosed(), |
||||
linkText: this.I18n.t('js.boards.version.open_version'), |
||||
onClick: () => { |
||||
this.patchVersionStatus(version, 'open'); |
||||
return true; |
||||
} |
||||
}, |
||||
{ |
||||
// Edit link
|
||||
hidden: !version.$links.update, |
||||
linkText: this.I18n.t('js.boards.version.edit_version'), |
||||
href: this.pathHelper.versionEditPath(id), |
||||
onClick: (evt:JQuery.Event) => { |
||||
if (!LinkHandling.isClickedWithModifier(evt)) { |
||||
window.open(this.pathHelper.versionEditPath(id), '_blank'); |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
]; |
||||
} |
||||
} |
@ -0,0 +1,48 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// 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 doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
import {ChangeDetectionStrategy, Component, Input} from "@angular/core"; |
||||
import {VersionResource} from "core-app/modules/hal/resources/version-resource"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
|
||||
|
||||
@Component({ |
||||
templateUrl: './version-board-header.html', |
||||
styleUrls: ['./version-board-header.sass'], |
||||
host: { 'class': 'title-container -small' } |
||||
}) |
||||
export class VersionBoardHeaderComponent { |
||||
@Input('resource') public version:VersionResource; |
||||
|
||||
constructor(private I18n:I18nService) { |
||||
} |
||||
|
||||
public text = { |
||||
isLocked: this.I18n.t('js.boards.version.is_locked'), |
||||
isClosed: this.I18n.t('js.boards.version.is_closed') |
||||
}; |
||||
} |
@ -0,0 +1,17 @@ |
||||
<div class="version-board-header" |
||||
[ngClass]="{ '-closed': version.isClosed(), '-locked': version.isLocked() }" |
||||
*ngIf="version"> |
||||
<span *ngIf="version.isLocked()" |
||||
[attr.title]="text.isLocked" |
||||
class="icon-locked icon-context"> |
||||
|
||||
</span> |
||||
<span *ngIf="version.isClosed()" |
||||
[attr.title]="text.isClosed" |
||||
class="icon-remove icon-context"> |
||||
|
||||
</span> |
||||
<h2 [textContent]="version.name" |
||||
class="editable-toolbar-title--fixed"> |
||||
</h2> |
||||
</div> |
@ -0,0 +1,3 @@ |
||||
.version-board-header |
||||
display: flex |
||||
align-items: center |
@ -1,19 +0,0 @@ |
||||
import {Injectable} from "@angular/core"; |
||||
import {Board} from "core-app/modules/boards/board/board"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {QueryResource} from "core-app/modules/hal/resources/query-resource"; |
||||
|
||||
@Injectable() |
||||
export class BoardListService { |
||||
public getActionAttributeValue(board:Board, query:QueryResource) { |
||||
const attribute = board.actionAttribute!; |
||||
const filter = _.find(query.filters, f => f.id === attribute); |
||||
|
||||
if (!(filter && filter.values[0] instanceof HalResource)) { |
||||
return ''; |
||||
} |
||||
const value = filter.values[0] as HalResource; |
||||
return value; |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue