pull/6140/head
Jens Ulferts 7 years ago
parent 92ce554e53
commit 73d55f9e43
No known key found for this signature in database
GPG Key ID: 3CAA4B1182CF5308
  1. 1
      app/controllers/custom_actions_controller.rb
  2. 7
      app/views/custom_actions/_form.html.erb
  3. 3
      config/locales/js-en.yml
  4. 8
      db/migrate/20180123092002_add_custom_actions.rb
  5. 30
      frontend/app/angular4-modules.ts
  6. 39
      frontend/app/components/common/gon-ref/gon-ref.ts
  7. 12
      frontend/app/components/common/hide-section/add-section-dropdown/add-section-dropdown.component.html
  8. 67
      frontend/app/components/common/hide-section/add-section-dropdown/add-section-dropdown.component.ts
  9. 8
      frontend/app/components/common/hide-section/hide-section-link/hide-section-link.component.html
  10. 38
      frontend/app/components/common/hide-section/hide-section-link/hide-section-link.component.ts
  11. 15
      frontend/app/components/common/hide-section/hide-section.component.ts
  12. 69
      frontend/app/components/common/hide-section/hide-section.service.ts
  13. 5
      frontend/app/components/common/hide-sections/hide-sections.component.html
  14. 45
      spec/features/work_packages/workflow_buttons_spec.rb
  15. 55
      spec/support/pages/admin/new_custom_action.rb

@ -29,6 +29,7 @@
class CustomActionsController < ApplicationController
before_action :require_admin
layout 'admin'
def index
@custom_actions = CustomAction.order_by_name

@ -1,7 +1,7 @@
<%
gon.push({
actions: @custom_action.all_actions.map(&:key)
hide_sections: @custom_action.all_actions.map { |a| { key: a.key, label: a.human_name } }
})
%>
@ -16,7 +16,7 @@ gon.push({
<%= t('custom_actions.actions') %>
</legend>
<hide-sections available-sections="<%= @custom_action.all_actions.map(&:key).join(', ') %>">
<hide-sections>
<% @custom_action.all_actions.each do |action| %>
<hide-section section-name="<%= action.key %>">
<div class="form--field">
@ -47,8 +47,11 @@ gon.push({
container_class: '-slim',
step: 'any' %>
<% end %>
<hide-section-link section-name="<%= action.key %>"></hide-section-link>
</div>
</hide-section>
<% end %>
</hide-sections>
<add-section-dropdown></add-section-dropdown>
</fieldset>

@ -39,6 +39,9 @@ en:
browser_error: "Your browser doesn't support copying to clipboard. Please copy the selected text manually."
copied_successful: "Sucessfully copied to clipboard!"
custom_actions:
add: "Add"
button_add_watcher: "Add watcher"
button_back_to_list_view: "Back to list view"
button_cancel: "Cancel"

@ -0,0 +1,8 @@
class AddCustomActions < ActiveRecord::Migration[5.0]
def change
create_table :custom_actions do |t|
t.string :name
t.text :actions
end
end
end

@ -69,20 +69,11 @@ import {WpWorkflowButtonComponent} from 'core-components/wp-workflow-buttons/wp-
import {WpWorkflowButtonsComponent} from 'core-components/wp-workflow-buttons/wp-workflow-buttons.component';
import {HalRequestService} from 'core-components/api/api-v3/hal-request/hal-request.service';
import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service';
import {HideSectionsComponent} from 'core-components/common/hide-sections/hide-sections.component';
import {Angular4WrapperComponent} from 'core-components/common/angular4-wrapper/angular4-wrapper.component';
import {HideSectionComponent} from 'core-components/common/hide-section/hide-section.component';
import {HideSectionsService} from 'core-components/common/hide-sections/hide-sections.service';
@NgModule({
declarations: [
Angular4WrapperComponent
],
exports: [
Angular4WrapperComponent
]
})
export class Angular4Wrapper { }
import {HideSectionService} from 'core-components/common/hide-section/hide-section.service';
import {AddSectionDropdownComponent} from 'core-components/common/hide-section/add-section-dropdown/add-section-dropdown.component';
import {HideSectionLinkComponent} from 'core-components/common/hide-section/hide-section-link/hide-section-link.component';
import {GonRef} from 'core-components/common/gon-ref/gon-ref';
@NgModule({
imports: [
@ -91,7 +82,8 @@ export class Angular4Wrapper { }
FormsModule
],
providers: [
HideSectionsService,
GonRef,
HideSectionService,
WorkPackagesTableControllerHolder,
upgradeService('wpRelations', WorkPackageRelationsService),
upgradeService('states', States),
@ -132,16 +124,18 @@ export class Angular4Wrapper { }
SortHeaderDirective,
HasDropdownMenuDirective,
WpInlineCreateDirectiveUpgraded,
HideSectionsComponent,
HideSectionComponent
HideSectionComponent,
HideSectionLinkComponent,
AddSectionDropdownComponent
],
entryComponents: [
WorkPackageTablePaginationComponent,
WorkPackagesTableController,
TablePaginationComponent,
WpWorkflowButtonsComponent,
HideSectionsComponent,
HideSectionComponent
HideSectionComponent,
HideSectionLinkComponent,
AddSectionDropdownComponent
]
})
export class OpenProjectModule {

@ -26,36 +26,19 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {Injectable} from '@angular/core';
import { Injectable } from '@angular/core';
@Injectable()
export class HideSectionsService {
protected availableSections:string[] = [];
protected displayedSections:string[] = [];
constructor() {
console.log('HideSectionsService constructed');
}
isDisplayed(name:string) {
return this.displayedSections.indexOf(name) > 0;
}
hide(name:string) {
}
show(name:string) {
this.displayedSections.push(name);
_.remove(this.availableSections, (candidate) => candidate === name);
console.log(this.displayedSections);
}
interface GonWindow extends Window {
gon:{}
}
set all(available:string[]) {
this.availableSections = available;
}
function _gon() : any {
return (<GonWindow> window).gon;
}
get available() {
return this.availableSections;
@Injectable()
export class GonRef {
get(name:string) : any {
return _gon()[name];
}
}

@ -0,0 +1,12 @@
<div class="form--field">
<label class="form--label" for="add-section">{{texts.add}}</label>
<span class="form--field-container">
<span class="form--text-field-container -middle">
<select id="add-section" [(ngModel)]="turnedActive" (ngModelChange)="show()">
<option [ngValue]="null">{{texts.placeholder}}</option>
<option *ngFor="let n of selectable" [ngValue]="n">{{n.label}}</option>
</select>
</span>
</span>
</div>

@ -0,0 +1,67 @@
// -- 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 {opUiComponentsModule} from '../../../../angular-modules';
import {Component, Inject, OnInit} from '@angular/core';
import {downgradeComponent} from '@angular/upgrade/static';
import {HideSectionDefinition, HideSectionService} from 'core-components/common/hide-section/hide-section.service';
import {I18nToken} from 'core-app/angular4-transition-utils';
@Component({
selector: 'add-section-dropdown',
template: require('!!raw-loader!./add-section-dropdown.component.html')
})
export class AddSectionDropdownComponent implements OnInit {
selectable:HideSectionDefinition[] = [];
turnedActive:HideSectionDefinition|null = null;
texts: { [key:string]: string } = {};
constructor(protected hideSections:HideSectionService,
@Inject(I18nToken) protected I18n:op.I18n) {
this.texts = {
placeholder: I18n.t('js.placeholders.default'),
add: I18n.t('js.custom_actions.add')
};
}
ngOnInit() {
this.selectable = this.hideSections.available;
}
show() {
if (this.turnedActive) {
this.hideSections.show(this.turnedActive);
setTimeout(() => { this.turnedActive = null } );
}
}
}
opUiComponentsModule.directive(
'addSectionDropdown',
downgradeComponent({component: AddSectionDropdownComponent})
);

@ -0,0 +1,8 @@
<accessible-by-keyboard
iconClasses="icon-close"
(execute)="hideSection()"
aria-hidden="false"
linkTitle="Remove"
linkAriaLabel="Remove">
<op-icon icon-classes="icon-close" icon-title="Remove"></op-icon>
</accessible-by-keyboard>

@ -26,45 +26,33 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {opUiComponentsModule} from '../../../angular-modules';
import {opUiComponentsModule} from '../../../../angular-modules';
import {Component} from '@angular/core';
import {OnInit, Input} from '@angular/core';
import {downgradeComponent} from '@angular/upgrade/static';
import {HideSectionsService} from 'core-components/common/hide-sections/hide-sections.service';
import {HideSectionService} from 'core-components/common/hide-section/hide-section.service';
@Component({
selector: 'hide-sections',
template: require('!!raw-loader!./hide-sections.component.html')
selector: 'hide-section-link',
template: require('!!raw-loader!./hide-section-link.component.html')
})
export class HideSectionsComponent implements OnInit {
selectable:string[] = [];
//active:string[] = ['Some'];
turnedActive:string;
export class HideSectionLinkComponent implements OnInit {
displayed:boolean = true;
@Input('availableSections') rawAvailableSections:string = '';
@Input('sectionName') sectionName:string;
constructor(protected hideSections:HideSectionsService) {
}
constructor(protected hideSections:HideSectionService) {}
ngOnInit() {
// using _.map(this.rawAvailableSections.split(','), _.trim) works but the typescript compiler believes
// the return values to be boolean[];
this.hideSections.all = _.map(this.rawAvailableSections.split(','), (name) => _.trim(name));
this.selectable = this.hideSections.available;
}
show(name:string) {
this.hideSections.show(name);
hideSection() {
this.hideSections.hideByName(this.sectionName);
return false;
}
//sectionDisplayed(name:string) {
// console.log(name);
// return this.hideSections.isDisplayed(name);
//}
}
opUiComponentsModule.directive(
'hideSections',
downgradeComponent({component: HideSectionsComponent})
'hideSectionLink',
downgradeComponent({component: HideSectionLinkComponent})
);

@ -30,31 +30,26 @@ import {opUiComponentsModule} from '../../../angular-modules';
import {Component} from '@angular/core';
import {OnInit, Input} from '@angular/core';
import {downgradeComponent} from '@angular/upgrade/static';
import {HideSectionsService} from 'core-components/common/hide-sections/hide-sections.service';
import {HideSectionService} from 'core-components/common/hide-section/hide-section.service';
@Component({
selector: 'hide-section',
template: '<span *ngIf="isDisplayed()"><ng-content></ng-content>' +
'<span style="display: flex; flex: 1 0 0"><a href="" (click)="hideSection()">Remove</a></span></span>'
template: '<span *ngIf="isDisplayed()"><ng-content></ng-content></span>'
})
export class HideSectionComponent implements OnInit {
displayed:boolean = true;
@Input('sectionName') sectionName:string;
constructor(protected hideSections:HideSectionsService) {
constructor(protected hideSection:HideSectionService) {
}
ngOnInit() {
}
isDisplayed() {
//console.log('sectionDisplayed');
return this.hideSections.isDisplayed(this.sectionName);
}
hideSection() {
this.hideSections.hide(this.sectionName);
return this.hideSection.isDisplayed(this.sectionName);
}
}

@ -0,0 +1,69 @@
// -- 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 {GonRef} from 'core-components/common/gon-ref/gon-ref';
export interface HideSectionDefinition {
key: string,
label: string
}
@Injectable()
export class HideSectionService {
protected availableSections:HideSectionDefinition[] = [];
protected displayedSections:HideSectionDefinition[] = [];
constructor(protected gonRef:GonRef) {
this.availableSections = gonRef.get('hideSections').sort((a:HideSectionDefinition, b:HideSectionDefinition) => a.label.localeCompare(b.label));
}
isDisplayed(key:string) {
return _.some(this.displayedSections, (candidate) => candidate.key === key);
}
hide(key:string) {
let section = _.remove(this.displayedSections, (candidate) => candidate.key === key);
this.availableSections.push(_.first(section));
this.availableSections.sort((a, b) => a.label.localeCompare(b.label));
}
show(section:HideSectionDefinition) {
_.remove(this.availableSections, (candidate) => candidate === section);
this.displayedSections.push(section);
}
get available() {
return this.availableSections;
}
hideByName(sectionName:string) {
let section = _.remove(this.displayedSections, (candidate) => candidate.key === sectionName);
this.availableSections.push(_.first(section));
this.availableSections.sort((a, b) => a.label.localeCompare(b.label));
}
}

@ -1,5 +0,0 @@
<ng-content></ng-content>
<select [(ngModel)]="turnedActive" (ngModelChange)="show($event)">
<option *ngFor="let n of selectable" [ngValue]="n">{{n}}</option>
</select>

@ -76,6 +76,7 @@ describe 'Workflow buttons', type: :feature, js: true do
role: role,
type: work_package.type)
end
let(:ca_page) { Pages::Admin::NewCustomAction.new }
before do
login_as(admin)
@ -93,13 +94,9 @@ describe 'Workflow buttons', type: :feature, js: true do
click_link 'Custom action'
end
fill_in 'Name', with: 'Unassign'
within '#custom-actions-form--actions' do
select '-', from: 'Assignee'
end
click_button 'Create'
ca_page.set_name('Unassign')
ca_page.add_action('Assignee', '-')
ca_page.create
expect(page)
.to have_current_path(custom_actions_path)
@ -113,13 +110,9 @@ describe 'Workflow buttons', type: :feature, js: true do
click_link 'Custom action'
end
fill_in 'Name', with: 'Close'
within '#custom-actions-form--actions' do
select 'Close', from: 'Status'
end
click_button 'Create'
ca_page.set_name('Close')
ca_page.add_action('Status', 'Close')
ca_page.create
expect(page)
.to have_current_path(custom_actions_path)
@ -133,13 +126,9 @@ describe 'Workflow buttons', type: :feature, js: true do
click_link 'Custom action'
end
fill_in 'Name', with: 'Escalate'
within '#custom-actions-form--actions' do
select immediate_priority.name, from: 'Priority'
end
click_button 'Create'
ca_page.set_name('Escalate')
ca_page.add_action('Priority', immediate_priority.name)
ca_page.create
expect(page)
.to have_current_path(custom_actions_path)
@ -153,15 +142,11 @@ describe 'Workflow buttons', type: :feature, js: true do
click_link 'Custom action'
end
fill_in 'Name', with: 'Reset'
within '#custom-actions-form--actions' do
select default_priority.name, from: 'Priority'
select default_status.name, from: 'Status'
select user.name, from: 'Assignee'
end
click_button 'Create'
ca_page.set_name('Reset')
ca_page.add_action('Priority', default_priority.name)
ca_page.add_action('Status', default_status.name)
ca_page.add_action('Assignee', user.name)
ca_page.create
expect(page)
.to have_current_path(custom_actions_path)

@ -0,0 +1,55 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'support/pages/page'
module Pages
module Admin
class NewCustomAction < ::Pages::Page
def set_name(name)
fill_in 'Name', with: name
end
def add_action(name, value)
within '#custom-actions-form--actions' do
select name, from: 'Add'
select value, from: name
end
end
def create
click_button 'Create'
end
def path
new_custom_action_path
end
end
end
end
Loading…
Cancel
Save