parent
3726e22476
commit
e23ee0c669
@ -1,371 +0,0 @@ |
||||
// -- 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 {debugLog} from '../../helpers/debug_output'; |
||||
import {SchemaCacheService} from '../schemas/schema-cache.service'; |
||||
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service'; |
||||
import {WorkPackageNotificationService} from '../wp-edit/wp-notification.service'; |
||||
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; |
||||
import {FormResource} from 'core-app/modules/hal/resources/form-resource'; |
||||
import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; |
||||
import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service'; |
||||
import { |
||||
IWorkPackageCreateService, |
||||
IWorkPackageCreateServiceToken |
||||
} from "core-components/wp-new/wp-create.service.interface"; |
||||
import { |
||||
IWorkPackageEditingService, |
||||
IWorkPackageEditingServiceToken |
||||
} from "core-components/wp-edit-form/work-package-editing.service.interface"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {IFieldSchema} from "core-app/modules/fields/field.base"; |
||||
import {EditChangeset} from 'core-app/modules/fields/changeset/edit-changeset'; |
||||
|
||||
export class WorkPackageChangeset extends EditChangeset<WorkPackageResource> { |
||||
// Injections
|
||||
public wpNotificationsService:WorkPackageNotificationService = this.injector.get(WorkPackageNotificationService); |
||||
public schemaCacheService:SchemaCacheService = this.injector.get(SchemaCacheService); |
||||
public wpCacheService:WorkPackageCacheService = this.injector.get(WorkPackageCacheService); |
||||
public wpCreate:IWorkPackageCreateService = this.injector.get(IWorkPackageCreateServiceToken); |
||||
public wpEditing:IWorkPackageEditingService = this.injector.get(IWorkPackageEditingServiceToken); |
||||
public wpActivity:WorkPackagesActivityService = this.injector.get(WorkPackagesActivityService); |
||||
public halResourceService:HalResourceService = this.injector.get(HalResourceService); |
||||
|
||||
public inFlight:boolean = false; |
||||
|
||||
private wpFormPromise:Promise<FormResource>|null; |
||||
|
||||
public reset(key:string) { |
||||
delete this.changes[key]; |
||||
} |
||||
|
||||
public isChanged(attribute:string) { |
||||
return this.changes[attribute]; |
||||
} |
||||
|
||||
|
||||
public clear() { |
||||
this.changes = {}; |
||||
this.resetForm(); |
||||
this.buildResource(); |
||||
} |
||||
|
||||
/** |
||||
* Remove some of the changes by key |
||||
* @param changes |
||||
*/ |
||||
public clearSome(...changes:string[]) { |
||||
changes.forEach((key) => { |
||||
delete this.changes[key]; |
||||
}); |
||||
} |
||||
|
||||
private resetForm() { |
||||
this.form = null; |
||||
} |
||||
|
||||
public setValue(key:string, val:any) { |
||||
super.setValue(key, val); |
||||
|
||||
// Update the form for fields that may alter the form itself
|
||||
// when the work package is new. Otherwise, the save request afterwards
|
||||
// will update the form automatically.
|
||||
if (this.resource.isNew && (key === 'project' || key === 'type')) { |
||||
this.updateForm(); |
||||
} |
||||
} |
||||
|
||||
public getForm():Promise<FormResource> { |
||||
if (!this.form) { |
||||
return this.updateForm(); |
||||
} else { |
||||
return Promise.resolve(this.form); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Update the form resource from the API. |
||||
*/ |
||||
public updateForm():Promise<FormResource<WorkPackageResource>> { |
||||
let payload = this.buildPayloadFromChanges(); |
||||
|
||||
if (!this.wpFormPromise) { |
||||
this.wpFormPromise = this.resource.$links |
||||
.update(payload) |
||||
.then((form:FormResource) => { |
||||
this.form = form; |
||||
|
||||
this.buildResource(); |
||||
|
||||
this.wpFormPromise = null; |
||||
return form; |
||||
}) |
||||
.catch((error:any) => { |
||||
this.resetForm(); |
||||
|
||||
this.wpFormPromise = null; |
||||
throw error; |
||||
}); |
||||
} |
||||
|
||||
return this.wpFormPromise; |
||||
} |
||||
|
||||
public save():Promise<WorkPackageResource> { |
||||
this.inFlight = true; |
||||
const wasNew = this.resource.isNew; |
||||
|
||||
let promise = new Promise<WorkPackageResource>((resolve, reject) => { |
||||
this.updateForm() |
||||
.then((form) => { |
||||
const payload = this.buildPayloadFromChanges(); |
||||
|
||||
// Reject errors when occurring in form validation
|
||||
const errors = form.getErrors(); |
||||
if (errors !== null) { |
||||
return reject(errors); |
||||
} |
||||
|
||||
this.resource.$links.updateImmediately(payload) |
||||
.then((savedWp:WorkPackageResource) => { |
||||
// Ensure the schema is loaded before updating
|
||||
this.schemaCacheService.ensureLoaded(savedWp).then(() => { |
||||
|
||||
// Clear any previous activities
|
||||
this.wpActivity.clear(this.resource.id!); |
||||
|
||||
// Initialize any potentially new HAL values
|
||||
savedWp.retainFrom(this.resource); |
||||
this.inFlight = false; |
||||
this.resource = savedWp; |
||||
this.wpCacheService.updateWorkPackage(this.resource, true); |
||||
|
||||
if (wasNew) { |
||||
this.resource.overriddenSchema = undefined; |
||||
this.wpCreate.newWorkPackageCreated(this.resource); |
||||
} |
||||
|
||||
// If there is a parent, its view has to be updated as well
|
||||
if (this.resource.parent) { |
||||
this.wpCacheService.loadWorkPackage(this.resource.parent.id.toString(), true); |
||||
} |
||||
this.clear(); |
||||
this.wpEditing.stopEditing(this.resource.id!); |
||||
resolve(this.resource); |
||||
}); |
||||
}) |
||||
.catch((error:any) => { |
||||
// Update the resource anyway
|
||||
this.buildResource(); |
||||
reject(error); |
||||
}) |
||||
.catch(reject); |
||||
}); |
||||
}); |
||||
|
||||
promise |
||||
.catch(() => this.inFlight = false); |
||||
|
||||
return promise; |
||||
} |
||||
|
||||
/** |
||||
* Merge the current changes into the payload resource. |
||||
* |
||||
* @param {FormResource} form |
||||
* @return {any} |
||||
*/ |
||||
private applyChanges(plainPayload:any) { |
||||
// Fall back to the last known state of the work package should the form not be loaded.
|
||||
let reference = this.resource.$source; |
||||
if (this.form) { |
||||
reference = this.form.payload.$source; |
||||
} |
||||
|
||||
_.each(this.changes, (val:any, key:string) => { |
||||
const fieldSchema = this.schema[key]; |
||||
if (!(typeof(fieldSchema) === 'object' && fieldSchema.writable === true)) { |
||||
debugLog(`Trying to write ${key} but is not writable in schema`); |
||||
return; |
||||
} |
||||
|
||||
// Override in _links if it is a linked property
|
||||
if (reference._links[key]) { |
||||
plainPayload._links[key] = this.getLinkedValue(val, fieldSchema); |
||||
} else { |
||||
plainPayload[key] = this.changes[key]; |
||||
} |
||||
}); |
||||
|
||||
return plainPayload; |
||||
} |
||||
|
||||
/** |
||||
* Create the payload from the current changes, and extend it with the current lock version. |
||||
* -- This is the place to add additional logic when the lockVersion changed in between -- |
||||
*/ |
||||
private buildPayloadFromChanges() { |
||||
let payload; |
||||
|
||||
if (this.resource.isNew) { |
||||
// If the work package is new, we need to pass the entire form payload
|
||||
// to let all default values be transmitted (type, status, etc.)
|
||||
if (this.form) { |
||||
payload = this.form.payload.$source; |
||||
} else { |
||||
payload = this.resource.$source; |
||||
} |
||||
|
||||
// Add attachments to be assigned.
|
||||
// They will already be created on the server but now
|
||||
// we need to claim them for the newly created work package.
|
||||
payload['_links']['attachments'] = this.resource |
||||
.attachments |
||||
.elements |
||||
.map((a:HalResource) => { return { href: a.href }; }); |
||||
|
||||
// Explicitly delete the description if it was not set by the user.
|
||||
// if it was set by the user, #applyChanges will set it again.
|
||||
// Otherwise, the backend will set it for us.
|
||||
delete payload.description; |
||||
} else { |
||||
// Otherwise, simply use the bare minimum, which is the lock version.
|
||||
payload = this.minimalPayload; |
||||
} |
||||
|
||||
return this.applyChanges(payload); |
||||
} |
||||
|
||||
private get minimalPayload() { |
||||
return {lockVersion: this.resource.lockVersion, _links: {}}; |
||||
} |
||||
|
||||
/** |
||||
* Extract the link(s) in the given changed value |
||||
*/ |
||||
private getLinkedValue(val:any, fieldSchema:IFieldSchema) { |
||||
// Links should always be nullified as { href: null }, but
|
||||
// this wasn't always the case, so ensure null values are returned as such.
|
||||
if (_.isNil(val)) { |
||||
return {href: null}; |
||||
} |
||||
|
||||
// Test if we either have a CollectionResource or a HAL array,
|
||||
// or a single hal value.
|
||||
let isArrayType = (fieldSchema.type || '').startsWith('[]'); |
||||
let isArray = false; |
||||
|
||||
if (val.forEach || val.elements) { |
||||
isArray = true; |
||||
} |
||||
|
||||
if (isArray && isArrayType) { |
||||
let links:{ href:string }[] = []; |
||||
|
||||
if (val) { |
||||
let elements = (val.forEach && val) || val.elements; |
||||
|
||||
elements.forEach((link:{ href:string }) => { |
||||
if (link.href) { |
||||
links.push({href: link.href}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
return links; |
||||
} else { |
||||
return {href: _.get(val, 'href', null)}; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Check whether the given attribute is writable. |
||||
* @param attribute |
||||
*/ |
||||
public isWritable(attribute:string):boolean { |
||||
const schemaName = this.getSchemaName(attribute); |
||||
const fieldSchema = this.schema[schemaName] as IFieldSchema; |
||||
return fieldSchema && fieldSchema.writable; |
||||
} |
||||
|
||||
public humanName(attribute:string):string { |
||||
const fieldSchema = this.schema[attribute] as IFieldSchema; |
||||
return fieldSchema.name || attribute; |
||||
} |
||||
|
||||
public getSchemaName(attribute:string):string { |
||||
if (this.schema.hasOwnProperty('date') && (attribute === 'startDate' || attribute === 'dueDate')) { |
||||
return 'date'; |
||||
} else { |
||||
return super.getSchemaName(attribute); |
||||
} |
||||
} |
||||
|
||||
private buildResource() { |
||||
let payload = this.sourceFromResourceAndForm(); |
||||
|
||||
if (!payload) { |
||||
return; |
||||
} |
||||
|
||||
const resource = this.halResourceService.createHalResourceOfType('WorkPackage', this.applyChanges(payload)); |
||||
|
||||
if (resource.isNew && this.form) { |
||||
resource.initializeNewResource(this.form); |
||||
} |
||||
|
||||
if (resource.isNew) { |
||||
resource.attachments = this.resource.attachments; |
||||
} |
||||
resource.overriddenSchema = this.schema; |
||||
|
||||
resource.__initialized_at = this.resource.__initialized_at; |
||||
|
||||
this.resource = (resource as WorkPackageResource); |
||||
this.wpEditing.updateValue(this.resource.id!, this); |
||||
} |
||||
|
||||
/** |
||||
* Constructs the source from a combination of the resource |
||||
* and the form payload. The payload takes precedences. |
||||
* That way, values, that stem from the backend take precedence. |
||||
*/ |
||||
private sourceFromResourceAndForm() { |
||||
if (!this.wpCacheService.state(this.resource.id!).value) { |
||||
return null; |
||||
} |
||||
let payload = _.merge({}, |
||||
this.wpCacheService.state(this.resource.id!).value!.$source); |
||||
|
||||
if (this.form) { |
||||
_.merge(payload, this.form.payload.$source); |
||||
} |
||||
|
||||
return payload; |
||||
} |
||||
} |
@ -1,44 +0,0 @@ |
||||
// -- 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 {InjectionToken} from "@angular/core"; |
||||
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; |
||||
import {WorkPackageChangeset} from "core-components/wp-edit-form/work-package-changeset"; |
||||
|
||||
export const IWorkPackageEditingServiceToken = new InjectionToken<IWorkPackageEditingService>('IWorkPackageEditingService'); |
||||
|
||||
/** |
||||
* Export an interface for changeset and form |
||||
* to avoid circular dependency warnigns due to TS imports. |
||||
*/ |
||||
export interface IWorkPackageEditingService { |
||||
updateValue(id:string, changeset:any):void; |
||||
stopEditing(id:string):void; |
||||
changesetFor(wp:WorkPackageResource):WorkPackageChangeset; |
||||
} |
||||
|
@ -0,0 +1,277 @@ |
||||
/** |
||||
* Temporary class living while a work package is being edited |
||||
* Maintains references to: |
||||
* - The source work package (a pristine base) |
||||
* - The open set of changes (a changeset object) |
||||
* - The current form (due to temporary type/project changes) |
||||
* |
||||
* Provides access to: |
||||
* - A projected work package resource with all changes applied |
||||
*/ |
||||
import {FormResource} from "core-app/modules/hal/resources/form-resource"; |
||||
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {debugLog} from "core-app/helpers/debug_output"; |
||||
import {IFieldSchema} from "core-app/modules/fields/field.base"; |
||||
import {InputState} from "reactivestates"; |
||||
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset"; |
||||
|
||||
export class WorkPackageChangeset extends ResourceChangeset<WorkPackageResource> { |
||||
|
||||
/** Reference and load promise for the current form */ |
||||
private wpFormPromise:Promise<FormResource>|null; |
||||
|
||||
/** Flag whether this is currently being saved */ |
||||
public inFlight = false; |
||||
|
||||
constructor(public pristineResource:WorkPackageResource, |
||||
public readonly state?:InputState<WorkPackageChangeset>, |
||||
form?:FormResource) { |
||||
super(pristineResource, form); |
||||
} |
||||
|
||||
/** |
||||
* Push the change to the editing state to notify others. |
||||
* This will happen internally on work-package wide changes |
||||
* |
||||
* (type, project changes) |
||||
*/ |
||||
public push() { |
||||
if (this.state) { |
||||
this.state.putValue(this); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Retrieve the editing value for the given attribute |
||||
* |
||||
* @param {string} key The attribute to read |
||||
* @return {any} Either the value from the overriden change, or the default value |
||||
*/ |
||||
public value(key:string) { |
||||
// Overridden value by user?
|
||||
if (this.changeset.contains(key)) { |
||||
return this.changeset.get(key); |
||||
} |
||||
|
||||
// TODO we might need values from the form (default values on type change?)
|
||||
// Default value from the form?
|
||||
// const payloadValue = _.get(this._form, ['payload', key]);
|
||||
// if (payloadValue !== undefined) {
|
||||
// return payloadValue;
|
||||
// }
|
||||
|
||||
// Return whatever is on the base.
|
||||
return this.pristineResource[key]; |
||||
} |
||||
|
||||
public setValue(key:string, val:any) { |
||||
this.changeset.set(key, val); |
||||
|
||||
// Update the form for fields that may alter the form itself
|
||||
// when the work package is new. Otherwise, the save request afterwards
|
||||
// will update the form automatically.
|
||||
if (this.pristineResource.isNew && (key === 'project' || key === 'type')) { |
||||
this.updateForm().then(() => this.push()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Revert all edits on the resource |
||||
*/ |
||||
public clear() { |
||||
super.clear(); |
||||
this.state && this.state.clear(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the work package being edited |
||||
*/ |
||||
public get workPackageId():string { |
||||
return this.pristineResource.id!.toString(); |
||||
} |
||||
|
||||
/** |
||||
* Return whether the element is writable |
||||
* given the current best schema. |
||||
* |
||||
* @param key |
||||
*/ |
||||
public isWritable(key:string) { |
||||
const fieldSchema = this.schema[key] as IFieldSchema|null; |
||||
return fieldSchema && fieldSchema.writable; |
||||
} |
||||
|
||||
/** |
||||
* Return the best humanized name for this attribute |
||||
* @param attribute |
||||
*/ |
||||
public humanName(attribute:string):string { |
||||
return _.get(this.schema, `${attribute}.name`, attribute); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Build the request attributes against the fresh form |
||||
*/ |
||||
public buildRequestPayload():Promise<[FormResource, Object]> { |
||||
return this |
||||
.updateForm() |
||||
.then(form => [form, this.buildPayloadFromChanges()]) as Promise<[FormResource, Object]>; |
||||
} |
||||
|
||||
/** |
||||
* Returns the current work package form. |
||||
* This may be different from the base form when project or type is changed. |
||||
*/ |
||||
public getForm():Promise<FormResource> { |
||||
if (!this.form) { |
||||
return this.updateForm(); |
||||
} else { |
||||
return Promise.resolve(this.form); |
||||
} |
||||
} |
||||
|
||||
public getSchemaName(attribute:string):string { |
||||
return this.projectedResource.getSchemaName(attribute); |
||||
} |
||||
|
||||
/** |
||||
* Update the form resource from the API. |
||||
*/ |
||||
private updateForm():Promise<FormResource> { |
||||
let payload = this.buildPayloadFromChanges(); |
||||
|
||||
if (!this.wpFormPromise) { |
||||
this.wpFormPromise = this.pristineResource.$links |
||||
.update(payload) |
||||
.then((form:FormResource) => { |
||||
this.wpFormPromise = null; |
||||
this.form = form; |
||||
this.push(); |
||||
return form; |
||||
}) |
||||
.catch((error:any) => { |
||||
this.wpFormPromise = null; |
||||
this.form = null; |
||||
throw error; |
||||
}) as Promise<FormResource>; |
||||
} |
||||
|
||||
return this.wpFormPromise; |
||||
} |
||||
|
||||
/** |
||||
* Create the payload from the current changes, and extend it with the current lock version. |
||||
* -- This is the place to add additional logic when the lockVersion changed in between -- |
||||
*/ |
||||
private buildPayloadFromChanges() { |
||||
let payload; |
||||
|
||||
if (this.pristineResource.isNew) { |
||||
// If the work package is new, we need to pass the entire form payload
|
||||
// to let all default values be transmitted (type, status, etc.)
|
||||
if (this.form) { |
||||
payload = this.form.payload.$source; |
||||
} else { |
||||
payload = this.pristineResource.$source; |
||||
} |
||||
|
||||
// Add attachments to be assigned.
|
||||
// They will already be created on the server but now
|
||||
// we need to claim them for the newly created work package.
|
||||
payload['_links']['attachments'] = this.pristineResource |
||||
.attachments |
||||
.elements |
||||
.map((a:HalResource) => { |
||||
return { href: a.href }; |
||||
}); |
||||
|
||||
|
||||
// Explicitly delete the description if it was not set by the user.
|
||||
// if it was set by the user, #applyChanges will set it again.
|
||||
// Otherwise, the backend will set it for us.
|
||||
delete payload.description; |
||||
|
||||
} else { |
||||
// Otherwise, simply use the bare minimum, which is the lock version.
|
||||
payload = this.minimalPayload; |
||||
} |
||||
|
||||
return this.applyChanges(payload); |
||||
} |
||||
|
||||
private get minimalPayload() { |
||||
return { lockVersion: this.pristineResource.lockVersion, _links: {} }; |
||||
} |
||||
|
||||
/** |
||||
* Merge the current changes into the payload resource. |
||||
* |
||||
* @param {plainPayload:unknown} A set of attributes to merge into the payload |
||||
* @return {any} |
||||
*/ |
||||
private applyChanges(plainPayload:any) { |
||||
// Fall back to the last known state of the work package should the form not be loaded.
|
||||
let reference = this.pristineResource.$source; |
||||
if (this.form) { |
||||
reference = this.form.payload.$source; |
||||
} |
||||
|
||||
_.each(this.changeset.all, (val:unknown, key:string) => { |
||||
const fieldSchema:IFieldSchema|undefined = this.schema[key]; |
||||
if (!(typeof (fieldSchema) === 'object' && fieldSchema.writable)) { |
||||
debugLog(`Trying to write ${key} but is not writable in schema`); |
||||
return; |
||||
} |
||||
|
||||
// Override in _links if it is a linked property
|
||||
if (reference._links[key]) { |
||||
plainPayload._links[key] = this.getLinkedValue(val, fieldSchema); |
||||
} else { |
||||
plainPayload[key] = val; |
||||
} |
||||
}); |
||||
|
||||
return plainPayload; |
||||
} |
||||
|
||||
/** |
||||
* Extract the link(s) in the given changed value |
||||
*/ |
||||
private getLinkedValue(val:any, fieldSchema:IFieldSchema) { |
||||
// Links should always be nullified as { href: null }, but
|
||||
// this wasn't always the case, so ensure null values are returned as such.
|
||||
if (_.isNil(val)) { |
||||
return { href: null }; |
||||
} |
||||
|
||||
// Test if we either have a CollectionResource or a HAL array,
|
||||
// or a single hal value.
|
||||
let isArrayType = (fieldSchema.type || '').startsWith('[]'); |
||||
let isArray = false; |
||||
|
||||
if (val.forEach || val.elements) { |
||||
isArray = true; |
||||
} |
||||
|
||||
if (isArray && isArrayType) { |
||||
let links:{ href:string }[] = []; |
||||
|
||||
if (val) { |
||||
let elements = (val.forEach && val) || val.elements; |
||||
|
||||
elements.forEach((link:{ href:string }) => { |
||||
if (link.href) { |
||||
links.push({ href: link.href }); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
return links; |
||||
} else { |
||||
return { href: _.get(val, 'href', null) }; |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,53 +0,0 @@ |
||||
// -- 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 {InjectionToken} from "@angular/core"; |
||||
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource"; |
||||
import {Observable} from "rxjs"; |
||||
|
||||
export const IWorkPackageCreateServiceToken = new InjectionToken<IWorkPackageCreateService>('IWorkPackageCreateService'); |
||||
|
||||
/** |
||||
* Export an interface for changeset and form |
||||
* to avoid circular dependency warnigns due to TS imports. |
||||
*/ |
||||
export interface IWorkPackageCreateService { |
||||
|
||||
/** |
||||
* Observable for new work packages as created through various sources |
||||
* on the WP frontend. |
||||
*/ |
||||
onNewWorkPackage():Observable<WorkPackageResource>; |
||||
|
||||
/** |
||||
* Notifier callback for new work packages |
||||
* @param wp |
||||
*/ |
||||
newWorkPackageCreated(wp:WorkPackageResource):void; |
||||
} |
||||
|
@ -0,0 +1,56 @@ |
||||
export type ChangeMap = { [attribute:string]:unknown }; |
||||
|
||||
export class Changeset { |
||||
private changes:ChangeMap = {}; |
||||
|
||||
/** |
||||
* Return whether a change value exist for the given attribute key. |
||||
* @param {string} key |
||||
* @return {boolean} |
||||
*/ |
||||
public contains(key:string) { |
||||
return this.changes.hasOwnProperty(key); |
||||
} |
||||
|
||||
/** |
||||
* Get changed attribute names |
||||
* @returns {string[]} |
||||
*/ |
||||
public get changed():string[] { |
||||
return _.keys(this.changes); |
||||
} |
||||
|
||||
/** |
||||
* Returns the live set of the changes. |
||||
*/ |
||||
public get all():ChangeMap { |
||||
return this.changes; |
||||
} |
||||
|
||||
/** |
||||
* Reset one or multiple changes |
||||
* @param key |
||||
*/ |
||||
public reset(...keys:string[]) { |
||||
keys.forEach((k) => delete this.changes[k]); |
||||
} |
||||
|
||||
/** |
||||
* Reset the entire changeset |
||||
*/ |
||||
public clear():void { |
||||
this.changes = {}; |
||||
} |
||||
|
||||
public set(key:string, value:unknown):void { |
||||
this.changes[key] = value; |
||||
} |
||||
|
||||
/** |
||||
* Get a single value from the changeset |
||||
* @param key |
||||
*/ |
||||
public get(key:string):unknown|undefined { |
||||
return this.changes[key]; |
||||
} |
||||
} |
@ -1,73 +0,0 @@ |
||||
import {SchemaResource} from "core-app/modules/hal/resources/schema-resource"; |
||||
import {FormResource} from "core-app/modules/hal/resources/form-resource"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {Injector} from '@angular/core'; |
||||
|
||||
export abstract class EditChangeset<T extends HalResource|{ [key:string]:unknown; }> { |
||||
// The changeset to be applied to the resource
|
||||
public changes:{ [attribute:string]:any } = {}; |
||||
|
||||
public form:FormResource|null; |
||||
|
||||
constructor(readonly injector:Injector, |
||||
public resource:T, |
||||
form?:FormResource) { |
||||
this.form = form || null; |
||||
} |
||||
|
||||
public get empty() { |
||||
return _.isEmpty(this.changes); |
||||
} |
||||
|
||||
/** |
||||
* Get attributes |
||||
* @returns {string[]} |
||||
*/ |
||||
public get changedAttributes() { |
||||
return _.keys(this.changes); |
||||
} |
||||
|
||||
/** |
||||
* Retrieve the editing value for the given attribute |
||||
* |
||||
* @param {string} key The attribute to read |
||||
* @return {any} Either the value from the overriden change, or the default value |
||||
*/ |
||||
public value(key:string) { |
||||
if (this.isOverridden(key)) { |
||||
return this.changes[key]; |
||||
} else { |
||||
return this.resource[key]; |
||||
} |
||||
} |
||||
|
||||
public setValue(key:string, val:any) { |
||||
this.changes[key] = val; |
||||
} |
||||
|
||||
public getSchemaName(attribute:string):string { |
||||
return attribute; |
||||
} |
||||
|
||||
public clear() { |
||||
this.changes = {}; |
||||
} |
||||
|
||||
/** |
||||
* Return whether a change value exist for the given attribute key. |
||||
* @param {string} key |
||||
* @return {boolean} |
||||
*/ |
||||
public isOverridden(key:string) { |
||||
return this.changes.hasOwnProperty(key); |
||||
} |
||||
|
||||
/** |
||||
* Get the best schema currently available, either the default resource schema (must exist). |
||||
* If loaded, return the form schema, which provides better information on writable status |
||||
* and contains available values. |
||||
*/ |
||||
public get schema():SchemaResource { |
||||
return (this.form || this.resource).schema; |
||||
} |
||||
} |
@ -0,0 +1,121 @@ |
||||
import {SchemaResource} from "core-app/modules/hal/resources/schema-resource"; |
||||
import {FormResource} from "core-app/modules/hal/resources/form-resource"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {ChangeMap, Changeset} from "core-app/modules/fields/changeset/changeset"; |
||||
|
||||
export abstract class ResourceChangeset<T extends HalResource|{ [key:string]:unknown; }> { |
||||
/** Maintain a single change set while editing */ |
||||
protected changeset = new Changeset(); |
||||
|
||||
/** The projected resource, which will proxy values from the change set */ |
||||
public projectedResource = new Proxy( |
||||
this.pristineResource, |
||||
{ |
||||
get: (_, key:string) => this.proxyGet(key), |
||||
set: (_, key:string, val:any) => { |
||||
this.setValue(key, val); |
||||
return true; |
||||
}, |
||||
} |
||||
); |
||||
|
||||
public form:FormResource|null; |
||||
|
||||
constructor(public pristineResource:T, form?:FormResource) { |
||||
this.form = form || null; |
||||
} |
||||
|
||||
/** |
||||
* Return whether no changes were made to the work package |
||||
*/ |
||||
public isEmpty() { |
||||
return this.changeset.changed.length === 0; |
||||
} |
||||
|
||||
/** |
||||
* Return a shallow copy of the changes |
||||
*/ |
||||
public get changes():ChangeMap { |
||||
return { ...this.changeset.all }; |
||||
} |
||||
|
||||
/** |
||||
* Return the changed attributes in this change; |
||||
*/ |
||||
public get changedAttributes():string[] { |
||||
return this.changeset.changed; |
||||
} |
||||
|
||||
/** |
||||
* Returns whether the given attribute was changed |
||||
*/ |
||||
public contains(key:string) { |
||||
return this.changeset.contains(key); |
||||
} |
||||
|
||||
/** |
||||
* Proxy getters to base or changeset. |
||||
* Special case for schema , which is overridden. |
||||
* @param key |
||||
*/ |
||||
private proxyGet(key:string) { |
||||
if (key === 'schema') { |
||||
return this.schema; |
||||
} |
||||
|
||||
return this.value(key); |
||||
} |
||||
|
||||
/** |
||||
* Retrieve the editing value for the given attribute |
||||
* |
||||
* @param {string} key The attribute to read |
||||
* @return {any} Either the value from the overriden change, or the default value |
||||
*/ |
||||
public value(key:string) { |
||||
if (this.isOverridden(key)) { |
||||
return this.changes[key]; |
||||
} else { |
||||
return this.pristineResource[key]; |
||||
} |
||||
} |
||||
|
||||
public setValue(key:string, val:any) { |
||||
this.changes[key] = val; |
||||
} |
||||
|
||||
public getSchemaName(attribute:string):string { |
||||
return attribute; |
||||
} |
||||
|
||||
public clear() { |
||||
this.changeset.clear(); |
||||
this.form = null; |
||||
} |
||||
|
||||
/** |
||||
* Reset the given changed attribute |
||||
* @param key |
||||
*/ |
||||
public reset(key:string) { |
||||
this.changeset.reset(key); |
||||
} |
||||
|
||||
/** |
||||
* Return whether a change value exist for the given attribute key. |
||||
* @param {string} key |
||||
* @return {boolean} |
||||
*/ |
||||
public isOverridden(key:string) { |
||||
return this.changes.hasOwnProperty(key); |
||||
} |
||||
|
||||
/** |
||||
* Get the best schema currently available, either the default resource schema (must exist). |
||||
* If loaded, return the form schema, which provides better information on writable status |
||||
* and contains available values. |
||||
*/ |
||||
public get schema():SchemaResource { |
||||
return (this.form || this.pristineResource).schema; |
||||
} |
||||
} |
@ -1,5 +1,5 @@ |
||||
import {EditChangeset} from "core-app/modules/fields/changeset/edit-changeset"; |
||||
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset"; |
||||
|
||||
export class CustomTextChangeset extends EditChangeset<{ [key:string]:unknown; }> { |
||||
export class CustomTextChangeset extends ResourceChangeset<{ [key:string]:unknown; }> { |
||||
|
||||
} |
||||
|
@ -1,6 +1,6 @@ |
||||
import {EditChangeset} from "core-app/modules/fields/changeset/edit-changeset"; |
||||
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset"; |
||||
import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource"; |
||||
|
||||
export class WidgetChangeset extends EditChangeset<GridWidgetResource> { |
||||
export class WidgetChangeset extends ResourceChangeset<GridWidgetResource> { |
||||
|
||||
} |
||||
|
Loading…
Reference in new issue