Autoload the schemas from the schema states

pull/5063/head
Oliver Günther 8 years ago
parent eea6197bdb
commit 3b9b487406
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 1
      frontend/app/components/api/api-v3/hal-resource-types/hal-resource-types.config.ts
  2. 21
      frontend/app/components/api/api-v3/hal-resources/hal-resource.service.ts
  3. 55
      frontend/app/components/api/api-v3/hal-resources/schema-resource.service.ts
  4. 101
      frontend/app/components/api/api-v3/hal-resources/work-package-resource.service.ts
  5. 13
      frontend/app/components/api/api-work-packages/api-work-packages.service.ts
  6. 10
      frontend/app/components/routing/wp-list/wp-list.controller.test.ts
  7. 3
      frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts
  8. 2
      frontend/app/components/states.service.ts
  9. 4
      frontend/app/components/work-packages/work-package-cache.service.ts
  10. 18
      frontend/app/components/work-packages/wp-display-attr/wp-display-attr.directive.ts
  11. 29
      frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.ts
  12. 52
      frontend/app/components/wp-edit/wp-edit-field/wp-edit-field.directive.ts
  13. 10
      frontend/app/components/wp-edit/wp-edit-form.directive.ts

@ -55,6 +55,7 @@ function halResourceTypesConfig(halResourceTypes:HalResourceTypesService) {
to: 'WorkPackage'
}
},
Schema: 'SchemaResource',
Error: 'ErrorResource',
User: 'UserResource',
Collection: 'CollectionResource'

@ -29,6 +29,7 @@
import {opApiModule} from '../../../../angular-modules';
import {HalLinkInterface} from '../hal-link/hal-link.service';
import {HalResourceFactoryService} from '../hal-resource-factory/hal-resource-factory.service';
import {State} from './../../../../helpers/reactive-fassade';
const ObservableArray:any = require('observable-array');
@ -100,7 +101,27 @@ export class HalResource {
this.$initialize($source);
}
/**
* Return the associated state to this HAL resource, if any.
*/
public get state():State<HalResource>|null {
return null;
}
public $load(force = false):ng.IPromise<HalResource> {
const state = this.state;
if (!this.state) {
return this.$loadResource(force);
}
if (force) {
state.clear();
}
return <ng.IPromise<HalResource>> state.get();
}
protected $loadResource(force = false):ng.IPromise<HalResource> {
if (!force) {
if (this.$loaded) {
return $q.when(this);

@ -0,0 +1,55 @@
//-- 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 {HalResource} from './hal-resource.service';
import {opApiModule} from '../../../../angular-modules';
import {WorkPackageResource} from './work-package-resource.service';
import {States} from '../../../states.service';
import {State} from './../../../../helpers/reactive-fassade';
var states: States;
export class SchemaResource extends HalResource {
public get state() {
return states.schemas.get(this.$link.href);
}
}
function schemaResource(...args) {
[
states,
] = args;
return SchemaResource;
}
schemaResource.$inject = [
'states',
];
opApiModule.factory('SchemaResource', schemaResource);

@ -36,6 +36,8 @@ import {UploadFile} from '../../op-file-upload/op-file-upload.service';
import IQService = angular.IQService;
import IPromise = angular.IPromise;
import ITimeoutService = angular.ITimeoutService;
import {States} from '../../../states.service';
import {SchemaResource} from './schema-resource.service';
interface WorkPackageResourceEmbedded {
activities: CollectionResourceInterface;
@ -50,7 +52,7 @@ interface WorkPackageResourceEmbedded {
project: HalResource|any;
relations: CollectionResourceInterface;
responsible: HalResource|any;
schema: HalResource|any;
schema: SchemaResource|any;
status: HalResource|any;
timeEntries: HalResource[]|any[];
type: HalResource|any;
@ -80,6 +82,7 @@ var $q: IQService;
var $stateParams: any;
var $timeout: ITimeoutService;
var I18n: op.I18n;
var states: States;
var apiWorkPackages: ApiWorkPackagesService;
var wpCacheService: WorkPackageCacheService;
var NotificationsService: any;
@ -113,7 +116,7 @@ export class WorkPackageResource extends HalResource {
public $embedded: WorkPackageResourceEmbedded;
public $links: WorkPackageResourceLinks;
public id: number|string;
public schema;
public schema: SchemaResource|any;
public $pristine: { [attribute: string]: any } = {};
public parentId: number;
public subject: string;
@ -146,6 +149,10 @@ export class WorkPackageResource extends HalResource {
return this.modifiedFields.length > 0;
}
/**
* Returns a reference to the
*/
public get modifiedFields(): string[] {
var modified = [];
@ -190,12 +197,8 @@ export class WorkPackageResource extends HalResource {
* be done automatically, but the backend does not provide typed collections yet.
*/
protected $initialize(source) {
const oldSchema = this.schema;
super.$initialize(source);
// Take over previously loaded schema resource
this.setExistingSchema(oldSchema);
var attachments = this.attachments || {$source: void 0, $loaded: void 0};
this.attachments = new AttachmentCollectionResource(
attachments.$source,
@ -259,18 +262,6 @@ export class WorkPackageResource extends HalResource {
});
}
public requiredValueFor(fieldName): boolean {
var fieldSchema = this.schema[fieldName];
// The field schema may be undefined if a custom field
// is used as a column, but not available for this type.
if (angular.isUndefined(fieldSchema)) {
return false;
}
return !this[fieldName] && fieldSchema.writable && fieldSchema.required;
}
public allowedValuesFor(field): ng.IPromise<HalResource[]> {
var deferred = $q.defer();
@ -319,15 +310,10 @@ export class WorkPackageResource extends HalResource {
this.form
.then(form => {
// Override the current schema with
// the changes from API
this.schema = form.$embedded.schema;
// Take over new values from the form
// this resource doesn't know yet.
this.assignNewValues(form.$embedded.payload);
// Use the newest embedded form
this.schema = form.$embedded.schema;
deferred.resolve(form);
@ -340,11 +326,11 @@ export class WorkPackageResource extends HalResource {
return deferred.promise;
}
public getSchema() {
public loadFormSchema() {
return this.getForm().then(form => {
const schema = form.$embedded.schema;
this.schema = form.$embedded.schema;
angular.forEach(schema, (field, name) => {
angular.forEach(this.schema, (field, name) => {
// Assign only links from schema when an href is set
// and field is writable.
// (exclude plain properties and null values)
@ -355,11 +341,11 @@ export class WorkPackageResource extends HalResource {
const hasAllowedValues = Array.isArray(field.allowedValues) && field.allowedValues.length > 0;
if (isHalField && hasAllowedValues) {
this[name] = _.find(field.allowedValues, {href: this[name].href});
this[name] = _.find(field.allowedValues, {href: this[name].href}) || this[name];
}
});
return schema;
return this.schema;
});
}
@ -373,26 +359,30 @@ export class WorkPackageResource extends HalResource {
const sentValues = Object.keys(this.$pristine);
this.$links.updateImmediately(payload)
.then(workPackage => {
// Initialize any potentially new HAL values
this.$initialize(workPackage);
this.updateActivities();
if (wasNew) {
this.uploadPendingAttachments();
wpCacheService.newWorkPackageCreated(this);
}
// Remove only those pristine values that were submitted
angular.forEach(sentValues, (key) => {
delete this.$pristine[key];
});
.then((workPackage:WorkPackageResource) => {
// Remove the current form, otherwise old form data
// might still be used for the next edit field in #getSchema()
// might still be used for the next edit field to be edited
this.form = null;
deferred.resolve(this);
// Ensure the schema is loaded before updating
workPackage.schema.$load().then((schema) => {
// Initialize any potentially new HAL values
this.$initialize(workPackage);
this.schema = schema;
this.updateActivities();
if (wasNew) {
this.uploadPendingAttachments();
wpCacheService.newWorkPackageCreated(this);
}
// Remove only those pristine values that were submitted
angular.forEach(sentValues, (key) => {
delete this.$pristine[key];
});
deferred.resolve(this);
});
})
.catch(error => {
deferred.reject(error);
@ -460,25 +450,6 @@ export class WorkPackageResource extends HalResource {
});
}
private setExistingSchema(previous) {
// Only when existing schema was loaded
if (!(previous && previous.$loaded)) {
return;
}
// If old schema was a regular schema and both href match
// assume they are matching
if (previous.href === this.schema.href) {
this.schema = previous;
}
// If old schema was embedded into the WP form, decide
// based on its baseSchema href.
if (previous.baseSchema && previous.baseSchema.href === this.schema.href) {
this.schema = previous;
}
}
/**
* Invalidate a set of linked resources of this work package.
* And inform the cache service about the work package update.
@ -557,6 +528,7 @@ function wpResource(...args) {
$stateParams,
$timeout,
I18n,
states,
apiWorkPackages,
wpCacheService,
NotificationsService,
@ -570,6 +542,7 @@ wpResource.$inject = [
'$stateParams',
'$timeout',
'I18n',
'states',
'apiWorkPackages',
'wpCacheService',
'NotificationsService',

@ -55,12 +55,21 @@ export class ApiWorkPackagesService {
*/
public loadWorkPackageById(id:number, force = false) {
const url = this.v3Path.wp({wp: id});
const deferred = this.$q.defer();
return <IPromise<WorkPackageResource>> this.halRequest.get(url, null, {
this.halRequest.get(url, null, {
caching: {
enabled: !force
}
});
})
.then((workPackage:WorkPackageResource) => {
workPackage.schema.$load().then(() => {
deferred.resolve(workPackage);
})
})
.catch(deferred.reject);
return deferred.promise;
}
/**

@ -175,6 +175,12 @@ describe('WorkPackagesListController', () => {
}
};
var workPackage = {
schema: {
'$load': () => { return $q.when(true) }
}
}
wpListServiceMock = {
fromQueryParams() {
return $q.when({
@ -186,9 +192,7 @@ describe('WorkPackagesListController', () => {
resource: {
total: 10
},
work_packages: [
{}
]
work_packages: [ workPackage ]
});
}
};

@ -100,9 +100,6 @@ export class WorkPackageViewController {
* Initialize controller after workPackage resource has been loaded.
*/
protected init() {
// Ensure the schema is being loaded as soon as possible
this.workPackage.schema.$load();
// Set elements
this.workPackage.project.$load().then(() => {
this.projectIdentifier = this.workPackage.project.identifier;

@ -1,10 +1,12 @@
import {MultiState, initStates} from "../helpers/reactive-fassade";
import {WorkPackageResource} from "./api/api-v3/hal-resources/work-package-resource.service";
import {opServicesModule} from "../angular-modules";
import {SchemaResource} from './api/api-v3/hal-resources/schema-resource.service';
export class States {
workPackages = new MultiState<WorkPackageResource>();
schemas = new MultiState<SchemaResource>();
constructor() {
initStates(this, function (msg: any) {

@ -64,7 +64,9 @@ export class WorkPackageCacheService {
? wpState.getCurrentValue() // dirty, use current wp
: wp; // not dirty or unknown, use new wp
this.states.workPackages.put(workPackageId, wpForPublish);
wpForPublish.schema.$load().then(() => {
this.states.workPackages.put(workPackageId, wpForPublish);
});
}
}

@ -93,19 +93,17 @@ export class WorkPackageDisplayAttributeController {
protected updateAttribute(wp) {
this.workPackage = wp;
this.schema.$load().then(() => {
this.field = <DisplayField>this.wpDisplayField.getField(this.workPackage, this.attribute, this.schema[this.attribute]);
this.field = <DisplayField>this.wpDisplayField.getField(this.workPackage, this.attribute, this.schema[this.attribute]);
if (this.field.isManualRenderer) {
this.__d__renderer = this.__d__renderer || this.$element.find(".__d__renderer");
this.field.render(this.__d__renderer, this);
}
if (this.field.isManualRenderer) {
this.__d__renderer = this.__d__renderer || this.$element.find(".__d__renderer");
this.field.render(this.__d__renderer, this);
}
this.$element.attr("aria-label", this.label + " " + this.displayText);
this.$element.attr("aria-label", this.label + " " + this.displayText);
this.__d__cell = this.__d__cell || this.$element.find(".__d__cell");
this.__d__cell.toggleClass("-placeholder", this.isEmpty);
});
this.__d__cell = this.__d__cell || this.$element.find(".__d__cell");
this.__d__cell.toggleClass("-placeholder", this.isEmpty);
}
}

@ -124,27 +124,24 @@ export class WorkPackageSingleViewController {
this.workPackage.attachments.updateElements();
}
this.workPackage.schema.$load().then(schema => {
this.setFocus();
this.setFocus();
var otherGroup: any = _.find(this.groupedFields, {groupName: 'other'});
otherGroup.attributes = [];
var otherGroup: any = _.find(this.groupedFields, {groupName: 'other'});
otherGroup.attributes = [];
angular.forEach(schema, (prop, propName) => {
if (propName.match(/^customField/)) {
otherGroup.attributes.push(propName);
}
});
angular.forEach(this.workPackage.schema, (prop, propName) => {
if (propName.match(/^customField/)) {
otherGroup.attributes.push(propName);
}
});
otherGroup.attributes.sort((leftField, rightField) => {
var getLabel = field => this.singleViewWp.getLabel(field);
var left = getLabel(leftField).toLowerCase();
var right = getLabel(rightField).toLowerCase();
otherGroup.attributes.sort((leftField, rightField) => {
var getLabel = field => this.singleViewWp.getLabel(field);
var left = getLabel(leftField).toLowerCase();
var right = getLabel(rightField).toLowerCase();
return left.localeCompare(right);
});
return left.localeCompare(right);
});
}
}

@ -116,40 +116,38 @@ export class WorkPackageEditFieldController {
}
public initializeField() {
// Activate field when creating a work package
// and the schema requires this field
if (this.workPackage.isNew && this.workPackage.requiredValueFor(this.fieldName)) {
this.activate();
var activeField = this.formCtrl.firstActiveField;
if (!activeField || this.formCtrl.fields[activeField].fieldIndex > this.fieldIndex) {
this.formCtrl.firstActiveField = this.fieldName;
}
}
// Mark the td field if it is inline-editable
// We're resolving the non-form schema here since its loaded anyway for the table
this.workPackage.schema.$load().then(schema => {
var fieldSchema = schema[this.fieldName];
const fieldSchema = this.workPackage.schema[this.fieldName];
this.editable = fieldSchema && fieldSchema.writable;
this.fieldType = fieldSchema && this.wpEditField.fieldType(fieldSchema.type);
this.editable = fieldSchema && fieldSchema.writable;
this.fieldType = fieldSchema && this.wpEditField.fieldType(fieldSchema.type);
this.updateDisplayAttributes();
this.updateDisplayAttributes();
if (fieldSchema) {
this.fieldLabel = this.fieldLabel || fieldSchema.name;
if (fieldSchema) {
this.fieldLabel = this.fieldLabel || fieldSchema.name;
// Activate field when creating a work package
// and the schema requires this field
if (this.workPackage.isNew && this.isRequired() && !this.workPackage[this.fieldName]) {
this.activate();
// Activate the field automatically when in editAllMode
if (this.inEditMode && this.isEditable) {
// Set focus on the first field
if(this.fieldName === 'subject')
this.activate(true);
else
this.activate();
var activeField = this.formCtrl.firstActiveField;
if (!activeField || this.formCtrl.fields[activeField].fieldIndex > this.fieldIndex) {
this.formCtrl.firstActiveField = this.fieldName;
}
}
});
// Activate the field automatically when in editAllMode
if (this.inEditMode && this.isEditable) {
// Set focus on the first field
if (this.fieldName === 'subject')
this.activate(true);
else
this.activate();
}
}
}
public activateIfEditable(event) {
@ -314,7 +312,7 @@ export class WorkPackageEditFieldController {
}
protected buildEditField(): ng.IPromise<any> {
return this.formCtrl.loadSchema().then(schema => {
return this.workPackage.loadFormSchema().then(schema => {
this.field = <EditField>this.wpEditField.getField(this.workPackage, this.fieldName, schema[this.fieldName]);
this.workPackage.storePristine(this.fieldName);
});

@ -63,12 +63,6 @@ export class WorkPackageEditFormController {
});
}
public isFieldRequired() {
return _.filter((this.fields as any), (name: string) => {
return !this.workPackage[name] && this.workPackage.requiredValueFor(name);
});
}
public registerField(field) {
this.fields[field.fieldName] = field;
field.setErrors(this.errorsPerAttribute[field.fieldName] || []);
@ -105,10 +99,6 @@ export class WorkPackageEditFormController {
return this.workPackage.isEditable;
}
public loadSchema() {
return this.workPackage.getSchema();
}
/**
* Update the form and embedded schema.
* In edit-all mode, this allows fields to cause changes to the form (e.g., type switch)

Loading…
Cancel
Save