Replace costs-subforms with augment service

pull/7385/head
Oliver Günther 6 years ago
parent 73441bfea0
commit 5167f4fca8
  1. 33
      frontend/doc/PLUGINS.md
  2. 6
      modules/costs/app/views/costlog/edit.html.erb
  3. 75
      modules/costs/frontend/legacy-app/components/budget/cost-budget-subform.directive.ts
  4. 107
      modules/costs/frontend/legacy-app/components/budget/cost-unit-subform.directive.ts
  5. 99
      modules/costs/frontend/legacy-app/components/subform/cost-subform.directive.ts
  6. 103
      modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts
  7. 62
      modules/costs/frontend/module/augment/cost-subform.augment.service.ts
  8. 101
      modules/costs/frontend/module/augment/planned-costs-form.ts
  9. 6
      modules/costs/frontend/module/main.ts

@ -34,10 +34,7 @@ The plugin is now known in the OpenProject application, but their frontends are
This will ensure those plugins with a frontend are symlinked to one of the following locations:
1. `frontend/legacy/app/plugins/` for plugins with a `frontend/legacy-app` folder.
2. `frontend/src/app/modules/plugins/linked/` for plugins with an exported Angular module under `frontend/module/main.ts`.
This will ensure those plugins with a frontend are symlinked to `frontend/src/app/modules/plugins/linked/` for plugins with an exported Angular module under `frontend/module/main.ts`.
@ -45,30 +42,16 @@ This will ensure those plugins with a frontend are symlinked to one of the follo
The [Costs](https://github.com/finnlabs/openproject-costs/) plugin has both legacy components that are still used by Rails templates as well as an entry module file to register to the Angular frontend.
Let's take a look at the file structure:
Let's take a look at the file structure of the costs folder `frontend/`:
```
frontend/
├── legacy-app
   └── components
   ├── budget
     ├── cost-budget-subform.directive.ts
     └── cost-unit-subform.directive.ts
   └── subform
   └── cost-subform.directive.ts
└── module
├── main.ts
└── wp-display
├── wp-display-costs-by-type-field.module.ts
└── wp-display-currency-field.module.ts
module
├── main.ts
└── wp-display
├── wp-display-costs-by-type-field.module.ts
└── wp-display-currency-field.module.ts
```
Anything under `frontend/legacy-app/*` will be symlinked to the core and found by the legacy webpack build. Thus, it will be contained in the legacy bundle and can be accessed with the `activate_angular_js` helper as described in the [legacy documentation](./LEGACY.md).
The Angular frontend entry point is `frontend/module/main.ts` and should export a `PluginModule` ngModule that looks like the following:
```typescript
@ -108,4 +91,4 @@ export class PluginModule { // The name PluginModule is important!
The rake task will generate a Module under `frontend/src/app/modules/plugins/linked-plugin-module.ts` that will import all these plugin modules. This happens by filling an ERB template by the rake task and is performed in `lib/open_project/plugins/frontend_linking/*`
The rake task will generate a Module under `frontend/src/app/modules/plugins/linked-plugin-module.ts` that will import all these plugin modules. This happens by filling an ERB template by the rake task and is performed in `lib/open_project/plugins/frontend_linking/*`

@ -89,7 +89,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<% if @cost_entry.cost_type.nil? %>
<%= f.text_field :units, size: 6, required: true, container_class: '-slim' %>
<% else %>
<% suffix = @cost_entry.units == 1 ? @cost_entry.cost_type.unit : @cost_entry.cost_type.unit_plural %>
<% suffix = @cost_enngtry.units == 1 ? @cost_entry.cost_type.unit : @cost_entry.cost_type.unit_plural %>
<%= f.text_field :units,
size: 6,
required: true,
@ -106,7 +106,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<span class="form--field-container">
<cost-unit-subform obj-id="cost_entry_costs" obj-name="cost_entry[overridden_costs]">
<% if User.current.allowed_to? :view_cost_rates, @cost_entry.project %>
<a href="#" id="cost_entry_costs" class="icon-context icon-edit" title="<%= t(:help_click_to_edit) %>">
<a href="#" id="cost_entry_costs" class="costs--edit-planned-costs-btn icon-context icon-edit" title="<%= t(:help_click_to_edit) %>">
<%= number_to_currency(@cost_entry.real_costs) %>
</a>
<% else %>
@ -116,7 +116,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
<br /><em><%= t(:help_override_rate) %></em>
<% end %>
</cost-unit-subform>
</span>
</span>ng
</div>
<div class="form--field">

@ -52,85 +52,10 @@ export class CostBudgetSubformController {
private $scope:ng.IScope,
private $compile:any) {
this.container = $element.find('.budget-item-container');
this.rowIndex = parseInt(this.$element.attr('item-count') as string);
// Refresh row on changes
$element.on('change', '.budget-item-value', (evt) => {
var row = angular.element(evt.target).closest('.cost_entry');
this.refreshRow(row.attr('id') as string);
});
$element.on('click', '.delete-budget-item', (evt) => {
evt.preventDefault();
var row = angular.element(evt.target).closest('.cost_entry');
row.remove();
return false;
});
// Add new row handler
$element.find('.budget-add-row').click((evt) => {
evt.preventDefault();
this.addBudgetItem();
return false;
});
}
/**
* Refreshes the given row after updating values
*/
public refreshRow(row_identifier:string) {
var row = this.$element.find('#' + row_identifier);
var request = this.buildRefreshRequest(row, row_identifier);
this.$http({
url: this.updateUrl,
method: 'POST',
data: request,
headers: { 'Accept': 'application/json' }
}).then((response:any) => {
_.each(response.data, (val:string, selector:string) => {
jQuery('#' + selector).html(val);
});
}).catch(response => {
this.pluginContext.context!.services.wpNotifications.handleErrorResponse(response);
});
}
/**
* Adds a new empty budget item row with the correct index set
*/
public addBudgetItem() {
let compiledTemplate = this.$compile(this.indexedTemplate)(this.$scope);
this.container.append(compiledTemplate);
this.rowIndex += 1;
}
/**
* Return the next possible new row from rowTemplate,wpNotifications
* with the index set to the current last value.
*/
private get indexedTemplate() {
return this.rowTemplate.replace(/INDEX/g, this.rowIndex.toString());
}
/**
* Returns the params for the update request
*/
private buildRefreshRequest(row:JQuery, row_identifier:string) {
var request:any = {
element_id: row_identifier,
fixed_date: angular.element('#cost_object_fixed_date').val()
};
// Augment common values with specific values for this type
row.find('.budget-item-value').each((_i:number, el:any) => {
var field = angular.element(el);
request[field.data('requestKey')] = field.val() || '0';
});
return request;
}
}
function costsBudgetSubform():any {

@ -1,107 +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.
// ++
export class CostUnitSubformController {
public objId:string;
public objName:string;
constructor(public $element:ng.IAugmentedJQuery) {
this.objId = this.$element.attr('obj-id')!;
this.objName = this.$element.attr('obj-name')!;
// Add new row handler
$element.find('#' + this.objId).click(() => {
this.makeEditable('#' + this.objId, this.objName);
});
}
private getCurrencyValue(str:string) {
var result = str.match(/^\s*(([0-9]+[.,])+[0-9]+) (.+)\s*/);
return result ? new Array(result[1], result[3]) : new Array(str, "");
}
public makeEditable(id:string, name:string) {
var obj = jQuery(id);
this.edit_and_focus(obj, name);
}
private edit_and_focus(obj:any, name:string) {
this.edit(obj, name);
jQuery('#' + obj[0].id + '_edit').focus();
jQuery('#' + obj[0].id + '_edit').select();
}
private edit(obj:any, name:string, obj_value?:any) {
obj.hide();
var obj_value = typeof (obj_value) != 'undefined' ? obj_value : obj[0].innerHTML;
var parsed = this.getCurrencyValue(obj_value);
var value = parsed[0];
var currency = parsed[1];
var form_start = '<section class="form--section" id="' + obj[0].id +
'_section"><div class="form--field"><div class="form--field-container">';
var button = '<div id="' + obj[0].id +
'_cancel" class="form--field-affix -transparent icon icon-close"></div>';
var span = '<div id="' + obj[0].id + '_editor" class="form--text-field-container">';
span += '<input id="' + obj[0].id + '_edit" class="form--text-field" name="' + name + '" value="' + value + '" class="currency" type="text" /> ';
span += '</div>';
var affix = '<div class="form--field-affix" id="' + obj[0].id + '_affix">' +
currency +
'</div>';
var form_end = '</div></div></section>';
jQuery(form_start + button + span + affix + form_end).insertAfter(obj);
var that = this;
jQuery('#' + obj[0].id + '_cancel').on('click', function() {
that.cleanUp(obj)
return false;
});
}
private cleanUp(obj:any) {
jQuery('#' + obj[0].id + '_section').remove();
obj.show();
}
}
function costUnitSubform():any {
return {
restrict: 'E',
scope: {},
bindToController: true,
controller: CostUnitSubformController,
controllerAs: '$ctrl'
};
}
angular.module('OpenProjectLegacy').directive('costUnitSubform', costUnitSubform);

@ -1,99 +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.
// ++
export class CostSubformController {
// Container for rows
private container: ng.IAugmentedJQuery;
// Template for new rows to insert, is rendered with INDEX placeholder
private rowTemplate: string;
// Current row index
public rowIndex: number;
// subform item count as output by rails
public itemCount: string;
constructor(public $element:ng.IAugmentedJQuery) {
this.container = $element.find('.subform-container');
this.rowIndex = parseInt(this.$element.attr('item-count') as string);
$element.on('click', '.delete-row-button,.delete-budget-item', (evt:JQueryEventObject) => {
var row = angular.element(evt.target).closest('.subform-row');
row.remove();
return false;
});
// Add new row handler
$element.find('.add-row-button,.wp-inline-create--add-link').click((evt) => {
evt.preventDefault();
this.addRow();
return false;
});
}
/**
* Adds a new empty budget item row with the correct index set
*/
public addRow() {
this.container.append(this.indexedTemplate);
this.rowIndex += 1;
this.container.find('.costs-date-picker').datepicker();
this.container.find('.subform-row:last-child input:first').focus();
}
/**
* Return the next possible new row from rowTemplate,
* with the index set to the current last value.
*/
private get indexedTemplate() {
return this.rowTemplate.replace(/INDEX/g, this.rowIndex.toString());
}
}
function costsSubform():any {
return {
restrict: 'E',
scope: { itemCount: '@' },
link: (scope:ng.IScope,
element:ng.IAugmentedJQuery,
attr:ng.IAttributes,
ctrl:any) => {
const template = element.find('.subform-row-template');
ctrl.rowTemplate = template[0].outerHTML;
template.remove();
},
bindToController: true,
controller: CostSubformController,
controllerAs: '$ctrl'
};
}
angular.module('OpenProjectLegacy').directive('costsSubform', costsSubform);

@ -0,0 +1,103 @@
// -- 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 {Injectable} from "@angular/core";
import {HttpClient} from '@angular/common/http';
import {WorkPackageNotificationService} from "core-app/components/wp-edit/wp-notification.service";
@Injectable()
export class CostSubformAugmentService {
constructor(private wpNotifications:WorkPackageNotificationService,
private http:HttpClient) {
jQuery('costs-budget-subform').each((i, match) => {
let el = jQuery(match);
const container = el.find('.budget-item-container');
const template:string = el.find('.budget-row-template')[0].outerHTML;
let rowIndex = parseInt(el.attr('item-count') as string);
// Refresh row on changes
el.on('change', '.budget-item-value', (evt) => {
let row = jQuery(evt.target).closest('.cost_entry');
this.refreshRow(el, row.attr('id') as string);
});
el.on('click', '.delete-budget-item', (evt) => {
evt.preventDefault();
jQuery(evt.target).closest('.cost_entry').remove();
return false;
});
// Add new row handler
el.find('.budget-add-row').click((evt) => {
evt.preventDefault();
container.append(template.replace(/INDEX/g, rowIndex.toString()));
rowIndex += 1;
return false;
});
});
}
/**
* Refreshes the given row after updating values
*/
public refreshRow(el:JQuery, row_identifier:string) {
let row = el.find('#' + row_identifier);
let request = this.buildRefreshRequest(row, row_identifier);
this.http
.post(el.attr('update-url')!, request, { headers: { 'Accept': 'application/json' } })
.subscribe(
(data:any) => {
_.each(data, (val:string, selector:string) => {
jQuery('#' + selector).html(val);
});
},
(error:any) => this.wpNotifications.handleRawError(error)
);
}
/**
* Returns the params for the update request
*/
private buildRefreshRequest(row:JQuery, row_identifier:string) {
let request:any = {
element_id: row_identifier,
fixed_date: row.find('#cost_object_fixed_date').val()
};
// Augment common values with specific values for this type
row.find('.budget-item-value').each((_i:number, el:any) => {
let field = jQuery(el);
request[field.data('requestKey')] = field.val() || '0';
});
return request;
}
}

@ -0,0 +1,62 @@
// -- 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 {Injectable} from "@angular/core";
@Injectable()
export class CostSubformAugmentService {
constructor() {
jQuery('costs-subform').each((i, match) => {
let el = jQuery(match);
const container = el.find('.subform-container');
const template = el.find('.subform-row-template')[0].outerHTML;
let rowIndex = parseInt(el.attr('item-count')!);
el.on('click', '.delete-row-button,.delete-budget-item', (evt:any) => {
jQuery(evt.target).closest('.subform-row').remove();
return false;
});
// Add new row handler
el.find('.add-row-button,.wp-inline-create--add-link').click((evt:any) => {
evt.preventDefault();
container.append(template.replace(/INDEX/g, rowIndex.toString()));
rowIndex += 1;
container.find('.costs-date-picker').datepicker();
container.find('.subform-row:last-child input:first').focus();
return false;
});
});
}
}

@ -0,0 +1,101 @@
// -- 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.
// ++
export class PlannedCostsFormAugment {
public obj:JQuery;
public objId:string;
public objName:string;
static listen() {
jQuery(document).on('click', '.costs--edit-planned-costs-btn', (evt) => {
const form = jQuery(evt.target).closest('cost-unit-subform');
new PlannedCostsFormAugment(form);
});
}
constructor(public $element:JQuery) {
this.objId = this.$element.attr('obj-id')!;
this.objName = this.$element.attr('obj-name')!;
this.obj = jQuery(this.objId);
this.makeEditable('#' + this.objId, this.objName);
}
private getCurrencyValue(str:string) {
var result = str.match(/^\s*(([0-9]+[.,])+[0-9]+) (.+)\s*/);
return result ? new Array(result[1], result[3]) : new Array(str, "");
}
public makeEditable(id:string, name:string) {
this.edit_and_focus();
}
private edit_and_focus() {
this.edit();
jQuery('#' + this.objId + '_edit').trigger('focus');
jQuery('#' + this.objId + '_edit').trigger('select');
}
private edit() {
this.obj.hide();
let obj_value = this.obj[0].innerHTML;
let id = this.obj[0].id;
let parsed = this.getCurrencyValue(obj_value);
let value = parsed[0];
let currency = parsed[1];
let name = this.objName;
let template = `
<section class="form--section" id="${id}_section">
<div class="form--field">
<div class="form--field-container">
<div id="${id}_cancel" class="form--field-affix -transparent icon icon-close"></div>';
<div id="${id}_editor" class="form--text-field-container">
<input id="${id}_edit" class="form--text-field" name="${name}" value="${value}" class="currency" type="text" />
</div>
<div class="form--field-affix" id="${id}_affix">${currency}</div>
</div>
</div>
</section>
`;
jQuery(template).insertAfter(this.obj);
let that = this;
jQuery('#' + id + '_cancel').on('click', function () {
jQuery('#' + id + '_section').remove();
that.obj.show();
return false;
});
}
}

@ -30,6 +30,8 @@ import {CostsByTypeDisplayField} from './wp-display/wp-display-costs-by-type-fie
import {CurrencyDisplayField} from './wp-display/wp-display-currency-field.module';
import {BudgetResource} from './hal/resources/budget-resource';
import {multiInput} from 'reactivestates';
import {CostSubformAugmentService} from "./augment/cost-subform.augment.service";
import {PlannedCostsFormAugment} from "core-app/modules/plugins/linked/openproject-costs/augment/planned-costs-form";
export function initializeCostsPlugin() {
return () => {
@ -72,6 +74,10 @@ export function initializeCostsPlugin() {
let states = pluginContext.services.states;
states.add('budgets', multiInput<BudgetResource>());
// Augment previous cost-subforms
new CostSubformAugmentService();
PlannedCostsFormAugment.listen();
});
};
}

Loading…
Cancel
Save