commit
888d4114ef
@ -0,0 +1,23 @@ |
||||
<h3 class="widget-box--header" |
||||
*ngIf="query" |
||||
cdkDragHandle> |
||||
<span class="grid--area-drag-handle |
||||
icon |
||||
icon-drag-handle" |
||||
cdkDragHandle></span> |
||||
<i class="icon-context icon-view-timeline" aria-hidden="true"></i> |
||||
<editable-toolbar-title [title]="query.name" |
||||
[inFlight]="inFlight" |
||||
(onSave)="renameQuery(query, $event)" |
||||
[editable]="!!query.updateImmediately" |
||||
class="widget-box--header-title"> |
||||
</editable-toolbar-title> |
||||
</h3> |
||||
|
||||
<ng-container wp-isolated-query-space> |
||||
<wp-embedded-table [queryId]="queryId" |
||||
[configuration]="configuration" |
||||
[externalHeight]="true" |
||||
*ngIf="queryId"> |
||||
</wp-embedded-table> |
||||
</ng-container> |
@ -0,0 +1,146 @@ |
||||
import {Component, OnInit, OnDestroy, ViewChild, AfterViewInit} from "@angular/core"; |
||||
import {WidgetWpListComponent} from "core-app/modules/grids/widgets/wp-widget/wp-widget.component"; |
||||
import {WorkPackageTableConfiguration} from "core-components/wp-table/wp-table-configuration"; |
||||
import {QueryResource} from "core-app/modules/hal/resources/query-resource"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed'; |
||||
import {WorkPackageIsolatedQuerySpaceDirective} from "core-app/modules/work_packages/query-space/wp-isolated-query-space.directive"; |
||||
import {skip, take} from 'rxjs/operators'; |
||||
import {UrlParamsHelperService} from "core-components/wp-query/url-params-helper"; |
||||
import {QueryFormDmService} from "core-app/modules/hal/dm-services/query-form-dm.service"; |
||||
import {QueryDmService} from "core-app/modules/hal/dm-services/query-dm.service"; |
||||
import {QueryFormResource} from "core-app/modules/hal/resources/query-form-resource"; |
||||
|
||||
@Component({ |
||||
templateUrl: './wp-table.component.html', |
||||
styleUrls: ['../wp-widget/wp-widget.component.css'] |
||||
}) |
||||
export class WidgetWpTableComponent extends WidgetWpListComponent implements OnInit, OnDestroy, AfterViewInit { |
||||
public text = { title: this.i18n.t('js.grid.widgets.work_packages_table.title') }; |
||||
public queryId:string|null; |
||||
private queryForm:QueryFormResource; |
||||
public inFlight = false; |
||||
public query:QueryResource; |
||||
|
||||
public configuration:Partial<WorkPackageTableConfiguration> = { |
||||
actionsColumnEnabled: false, |
||||
columnMenuEnabled: true, |
||||
hierarchyToggleEnabled: true, |
||||
contextMenuEnabled: false |
||||
}; |
||||
|
||||
constructor(protected i18n:I18nService, |
||||
protected urlParamsHelper:UrlParamsHelperService, |
||||
private readonly queryDm:QueryDmService, |
||||
private readonly queryFormDm:QueryFormDmService) { |
||||
super(i18n); |
||||
} |
||||
|
||||
@ViewChild(WorkPackageIsolatedQuerySpaceDirective) public querySpaceDirective:WorkPackageIsolatedQuerySpaceDirective; |
||||
|
||||
ngOnInit() { |
||||
if (!this.resource.options.queryId) { |
||||
this.createInitial() |
||||
.then((query) => { |
||||
this.resource.options = { queryId: query.id }; |
||||
|
||||
this.resourceChanged.emit(this.resource); |
||||
|
||||
this.queryId = query.id; |
||||
|
||||
super.ngOnInit(); |
||||
}); |
||||
} else { |
||||
this.queryId = this.resource.options.queryId as string; |
||||
|
||||
super.ngOnInit(); |
||||
} |
||||
} |
||||
|
||||
ngAfterViewInit() { |
||||
this |
||||
.querySpaceDirective |
||||
.querySpace |
||||
.query |
||||
.values$() |
||||
.pipe( |
||||
take(1), |
||||
untilComponentDestroyed(this) |
||||
).subscribe((query) => { |
||||
this.query = query; |
||||
this.queryId = query.id; |
||||
}); |
||||
|
||||
this |
||||
.querySpaceDirective |
||||
.querySpace |
||||
.query |
||||
.values$() |
||||
.pipe( |
||||
// 2 because ... well it is a magic number and works
|
||||
skip(2), |
||||
untilComponentDestroyed(this) |
||||
).subscribe((query) => { |
||||
this.queryId = query.id; |
||||
this.ensureFormAndSaveQuery(query); |
||||
}); |
||||
} |
||||
|
||||
ngOnDestroy() { |
||||
// nothing to do
|
||||
} |
||||
|
||||
private ensureFormAndSaveQuery(query:QueryResource) { |
||||
if (this.queryForm) { |
||||
this.saveQuery(query); |
||||
} else { |
||||
this.queryFormDm.load(query).then((form) => { |
||||
this.queryForm = form; |
||||
this.saveQuery(query); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
public renameQuery(query:QueryResource, value:string) { |
||||
query.name = value; |
||||
|
||||
this.ensureFormAndSaveQuery(query); |
||||
} |
||||
|
||||
private saveQuery(query:QueryResource) { |
||||
this.inFlight = true; |
||||
|
||||
this |
||||
.queryDm |
||||
.update(query, this.queryForm) |
||||
.toPromise() |
||||
.then((query) => { |
||||
this.inFlight = false; |
||||
return query; |
||||
}) |
||||
.catch(() => this.inFlight = false); |
||||
} |
||||
|
||||
private createInitial():Promise<QueryResource> { |
||||
return this.queryFormDm |
||||
.loadWithParams( |
||||
{pageSize: 0}, |
||||
undefined, |
||||
null, |
||||
this.buildQueryRequest() |
||||
) |
||||
.then(form => { |
||||
const query = this.queryFormDm.buildQueryResource(form); |
||||
// set a default title
|
||||
query.name = this.text.title; |
||||
|
||||
return this.queryDm.create(query, form); |
||||
}); |
||||
} |
||||
|
||||
private buildQueryRequest() { |
||||
return { |
||||
hidden: true |
||||
}; |
||||
} |
||||
} |
@ -1,14 +0,0 @@ |
||||
#!/usr/bin/env ruby |
||||
# This command will automatically be run when you run "rails" with Rails gems |
||||
# installed from the root of your application. |
||||
|
||||
ENGINE_ROOT = File.expand_path('../..', __FILE__) |
||||
ENGINE_PATH = File.expand_path('../../lib/grids/engine', __FILE__) |
||||
APP_PATH = File.expand_path('../../test/dummy/config/application', __FILE__) |
||||
|
||||
# Set up gems listed in the Gemfile. |
||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) |
||||
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) |
||||
|
||||
require 'rails/all' |
||||
require 'rails/engine/commands' |
@ -0,0 +1,127 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Grids::Configuration |
||||
class Registration |
||||
class << self |
||||
def grid_class(name_string = nil) |
||||
if name_string |
||||
@grid_class = name_string |
||||
end |
||||
|
||||
@grid_class |
||||
end |
||||
|
||||
def to_scope(path = nil) |
||||
if path |
||||
@to_scope = path |
||||
end |
||||
|
||||
@to_scope |
||||
end |
||||
|
||||
def widgets(*widgets) |
||||
if widgets.any? |
||||
@widgets = widgets |
||||
end |
||||
|
||||
@widgets |
||||
end |
||||
|
||||
def widget_strategy(widget_name, &block) |
||||
@widget_strategies ||= {} |
||||
|
||||
if block_given? |
||||
@widget_strategies[widget_name.to_s] = Class.new(Grids::Configuration::WidgetStrategy, &block) |
||||
end |
||||
|
||||
@widget_strategies[widget_name.to_s] ||= Grids::Configuration::WidgetStrategy |
||||
end |
||||
|
||||
def defaults(hash = nil) |
||||
# This is called during code load, which |
||||
# may not have the table available. |
||||
return unless Grids::Widget.table_exists? |
||||
|
||||
if hash |
||||
@defaults = hash |
||||
end |
||||
|
||||
params = @defaults.dup |
||||
params[:widgets] = (params[:widgets] || []).map do |widget| |
||||
Grids::Widget.new(widget) |
||||
end |
||||
|
||||
params |
||||
end |
||||
|
||||
def from_scope(_scope) |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
def all_scopes |
||||
Array(url_helpers.send(@to_scope)) |
||||
end |
||||
|
||||
def visible(_user = User.current) |
||||
::Grids::Grid |
||||
.where(type: grid_class) |
||||
end |
||||
|
||||
def writable?(_grid, _user) |
||||
true |
||||
end |
||||
|
||||
def register! |
||||
unless @grid_class |
||||
raise 'Need to define the grid class first. Use grid_class to do so.' |
||||
end |
||||
unless @widgets |
||||
raise 'Need to define at least one widget first. Use widgets to do so.' |
||||
end |
||||
unless @to_scope |
||||
raise 'Need to define a scope. Use to_scope to do so' |
||||
end |
||||
|
||||
Grids::Configuration.register_grid(@grid_class, self) |
||||
|
||||
widgets.each do |widget| |
||||
Grids::Configuration.register_widget(widget, @grid_class) |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def url_helpers |
||||
@url_helpers ||= OpenProject::StaticRouting::StaticUrlHelpers.new |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,55 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Grids::Configuration |
||||
class WidgetStrategy |
||||
class << self |
||||
def after_destroy(proc = nil) |
||||
if proc |
||||
@after_destroy = proc |
||||
end |
||||
|
||||
@after_destroy ||= -> {} |
||||
end |
||||
|
||||
def allowed(proc = nil) |
||||
if proc |
||||
@allowed = proc |
||||
end |
||||
|
||||
@allowed ||= ->(_user) { true } |
||||
end |
||||
|
||||
def allowed?(user) |
||||
allowed.(user) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,28 +1,4 @@ |
||||
FactoryBot.define do |
||||
factory :grid, class: Grids::Grid do |
||||
end |
||||
|
||||
factory :my_page, class: Grids::MyPage do |
||||
user |
||||
row_count { 7 } |
||||
column_count { 4 } |
||||
widgets do |
||||
[ |
||||
Grids::Widget.new( |
||||
identifier: 'work_packages_assigned', |
||||
start_row: 1, |
||||
end_row: 7, |
||||
start_column: 1, |
||||
end_column: 3 |
||||
), |
||||
Grids::Widget.new( |
||||
identifier: 'work_packages_created', |
||||
start_row: 1, |
||||
end_row: 7, |
||||
start_column: 3, |
||||
end_column: 5 |
||||
) |
||||
] |
||||
end |
||||
end |
||||
end |
||||
|
@ -0,0 +1,7 @@ |
||||
.bundle/ |
||||
log/*.log |
||||
pkg/ |
||||
test/dummy/db/*.sqlite3 |
||||
test/dummy/db/*.sqlite3-journal |
||||
test/dummy/log/*.log |
||||
test/dummy/tmp/ |
@ -0,0 +1,3 @@ |
||||
source 'https://rubygems.org' |
||||
|
||||
gemspec |
@ -0,0 +1,4 @@ |
||||
require "my_page/engine" |
||||
|
||||
module MyPage |
||||
end |
@ -0,0 +1,11 @@ |
||||
module MyPage |
||||
class Engine < ::Rails::Engine |
||||
isolate_namespace MyPage |
||||
|
||||
include OpenProject::Plugins::ActsAsOpEngine |
||||
|
||||
config.to_prepare do |
||||
MyPage::GridRegistration.register! |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,12 @@ |
||||
# encoding: UTF-8 |
||||
|
||||
Gem::Specification.new do |s| |
||||
s.name = "my_page" |
||||
s.version = '1.0.0' |
||||
s.authors = ["OpenProject"] |
||||
s.summary = "OpenProject MyPage." |
||||
|
||||
s.files = Dir["{app,config,db,lib}/**/*"] |
||||
|
||||
s.add_dependency 'grids' |
||||
end |
@ -0,0 +1,61 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- 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 'spec_helper' |
||||
require_relative './shared_examples' |
||||
|
||||
describe Grids::CreateContract do |
||||
include_context 'grid contract' |
||||
include_context 'model contract' |
||||
|
||||
it_behaves_like 'shared grid contract attributes' |
||||
|
||||
describe 'user_id' do |
||||
context 'for a Grids::MyPage' do |
||||
let(:grid) { FactoryBot.build_stubbed(:my_page, default_values) } |
||||
|
||||
it_behaves_like 'is writable' do |
||||
let(:attribute) { :user_id } |
||||
let(:value) { 5 } |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'project_id' do |
||||
context 'for a Grids::MyPage' do |
||||
let(:grid) { FactoryBot.build_stubbed(:my_page, default_values) } |
||||
|
||||
it_behaves_like 'is not writable' do |
||||
let(:attribute) { :project_id } |
||||
let(:value) { 5 } |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,320 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- 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. |
||||
#++ |
||||
|
||||
shared_context 'grid contract' do |
||||
let(:user) { FactoryBot.build_stubbed(:user) } |
||||
let(:instance) { described_class.new(grid, user) } |
||||
let(:default_values) do |
||||
{ |
||||
row_count: 6, |
||||
column_count: 7, |
||||
widgets: [] |
||||
} |
||||
end |
||||
let(:grid) do |
||||
FactoryBot.build_stubbed(:my_page, default_values) |
||||
end |
||||
end |
||||
|
||||
shared_examples_for 'shared grid contract attributes' do |
||||
include_context 'model contract' |
||||
let(:model) { grid } |
||||
|
||||
describe 'widgets' do |
||||
it_behaves_like 'is writable' do |
||||
let(:attribute) { :widgets } |
||||
let(:value) do |
||||
[ |
||||
Grids::Widget.new(start_row: 1, |
||||
end_row: 4, |
||||
start_column: 2, |
||||
end_column: 5, |
||||
identifier: 'work_packages_assigned') |
||||
] |
||||
end |
||||
end |
||||
|
||||
context 'invalid identifier' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 4, |
||||
start_column: 2, |
||||
end_column: 5, |
||||
identifier: 'bogus_identifier') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :inclusion }] |
||||
end |
||||
end |
||||
|
||||
context 'collisions between widgets' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 3, |
||||
start_column: 1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
grid.widgets.build(start_row: 2, |
||||
end_row: 4, |
||||
start_column: 2, |
||||
end_column: 4, |
||||
identifier: 'work_packages_created') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :overlaps }, { error: :overlaps }] |
||||
end |
||||
end |
||||
|
||||
context 'widgets having the same start column as another\'s end column' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 3, |
||||
start_column: 1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 3, |
||||
start_column: 3, |
||||
end_column: 4, |
||||
identifier: 'work_packages_created') |
||||
end |
||||
|
||||
it 'is valid' do |
||||
expect(instance.validate) |
||||
.to be_truthy |
||||
end |
||||
end |
||||
|
||||
context 'widgets having the same start row as another\'s end row' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 3, |
||||
start_column: 1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
grid.widgets.build(start_row: 3, |
||||
end_row: 4, |
||||
start_column: 1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_created') |
||||
end |
||||
|
||||
it 'is valid' do |
||||
expect(instance.validate) |
||||
.to be_truthy |
||||
end |
||||
end |
||||
|
||||
context 'widgets being outside (max) of the grid' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: grid.row_count + 2, |
||||
start_column: 1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :outside }] |
||||
end |
||||
end |
||||
|
||||
context 'widgets being outside (min) of the grid' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 2, |
||||
start_column: -1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :outside }] |
||||
end |
||||
end |
||||
|
||||
context 'widgets spanning the whole grid' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: grid.row_count + 1, |
||||
start_column: 1, |
||||
end_column: grid.column_count + 1, |
||||
identifier: 'work_packages_assigned') |
||||
end |
||||
|
||||
it 'is valid' do |
||||
expect(instance.validate) |
||||
.to be_truthy |
||||
end |
||||
end |
||||
|
||||
context 'widgets having start after end column' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 2, |
||||
start_column: 4, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :end_before_start }] |
||||
end |
||||
end |
||||
|
||||
context 'widgets having start after end row' do |
||||
before do |
||||
grid.widgets.build(start_row: 4, |
||||
end_row: 2, |
||||
start_column: 1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :end_before_start }] |
||||
end |
||||
end |
||||
|
||||
context 'widgets having start equals end column' do |
||||
before do |
||||
grid.widgets.build(start_row: 1, |
||||
end_row: 2, |
||||
start_column: 4, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :end_before_start }] |
||||
end |
||||
end |
||||
|
||||
context 'widgets having start equals end row' do |
||||
before do |
||||
grid.widgets.build(start_row: 2, |
||||
end_row: 2, |
||||
start_column: 1, |
||||
end_column: 3, |
||||
identifier: 'work_packages_assigned') |
||||
end |
||||
|
||||
it 'is invalid' do |
||||
expect(instance.validate) |
||||
.to be_falsey |
||||
end |
||||
|
||||
it 'notes the error' do |
||||
instance.validate |
||||
expect(instance.errors.details[:widgets]) |
||||
.to match_array [{ error: :end_before_start }] |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe 'valid grid subclasses' do |
||||
context 'for a registered subclass' do |
||||
let(:grid) do |
||||
FactoryBot.build_stubbed(:my_page, default_values) |
||||
end |
||||
|
||||
it 'is valid' do |
||||
expect(instance.validate) |
||||
.to be_truthy |
||||
end |
||||
end |
||||
|
||||
context 'for the Grid superclass itself' do |
||||
let(:grid) do |
||||
FactoryBot.build_stubbed(:grid, default_values) |
||||
end |
||||
|
||||
before do |
||||
instance.validate |
||||
end |
||||
|
||||
it 'is invalid for the grid superclass itself' do |
||||
expect(instance.errors.details[:scope]) |
||||
.to match_array [{ error: :inclusion }] |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,39 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- 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 'spec_helper' |
||||
require_relative './shared_examples' |
||||
|
||||
describe Grids::UpdateContract do |
||||
include_context 'model contract' |
||||
include_context 'grid contract' |
||||
|
||||
it_behaves_like 'shared grid contract attributes' |
||||
end |
@ -0,0 +1,25 @@ |
||||
FactoryBot.define do |
||||
factory :my_page, class: Grids::MyPage do |
||||
user |
||||
row_count { 7 } |
||||
column_count { 4 } |
||||
widgets do |
||||
[ |
||||
Grids::Widget.new( |
||||
identifier: 'work_packages_assigned', |
||||
start_row: 1, |
||||
end_row: 7, |
||||
start_column: 1, |
||||
end_column: 3 |
||||
), |
||||
Grids::Widget.new( |
||||
identifier: 'work_packages_created', |
||||
start_row: 1, |
||||
end_row: 7, |
||||
start_column: 3, |
||||
end_column: 5 |
||||
) |
||||
] |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,162 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
describe 'Arbitrary WorkPackage query table widget on my page', type: :feature, js: true do |
||||
let!(:type) { FactoryBot.create :type } |
||||
let!(:other_type) { FactoryBot.create :type } |
||||
let!(:priority) { FactoryBot.create :default_priority } |
||||
let!(:project) { FactoryBot.create :project, types: [type] } |
||||
let!(:other_project) { FactoryBot.create :project, types: [type] } |
||||
let!(:open_status) { FactoryBot.create :default_status } |
||||
let!(:type_work_package) do |
||||
FactoryBot.create :work_package, |
||||
project: project, |
||||
type: type, |
||||
author: user, |
||||
responsible: user |
||||
end |
||||
let!(:other_type_work_package) do |
||||
FactoryBot.create :work_package, |
||||
project: project, |
||||
type: other_type, |
||||
author: user, |
||||
responsible: user |
||||
end |
||||
|
||||
let(:permissions) { %i[view_work_packages add_work_packages save_queries] } |
||||
|
||||
let(:user) do |
||||
FactoryBot.create(:user, |
||||
member_in_project: project, |
||||
member_with_permissions: permissions) |
||||
end |
||||
let(:my_page) do |
||||
Pages::My::Page.new |
||||
end |
||||
|
||||
let(:modal) { ::Components::WorkPackages::TableConfigurationModal.new } |
||||
let(:filters) { ::Components::WorkPackages::TableConfiguration::Filters.new } |
||||
let(:columns) { ::Components::WorkPackages::Columns.new } |
||||
|
||||
before do |
||||
login_as user |
||||
|
||||
my_page.visit! |
||||
end |
||||
|
||||
context 'with the permission to save queries' do |
||||
it 'can add the widget and see the work packages of the filtered for types' do |
||||
my_page.add_column(3, before_or_after: :before) |
||||
|
||||
my_page.add_widget(2, 3, "Work packages") |
||||
|
||||
sleep(1) |
||||
|
||||
filter_area = Components::Grids::GridArea.new('.grid--area.-widgeted:nth-of-type(3)') |
||||
created_area = Components::Grids::GridArea.new('.grid--area', text: "Work packages created by me") |
||||
|
||||
filter_area.expect_to_span(2, 3, 5, 4) |
||||
filter_area.resize_to(6, 4) |
||||
|
||||
filter_area.expect_to_span(2, 3, 7, 5) |
||||
## enlarging the table area will have moved the created area down |
||||
created_area.expect_to_span(7, 4, 13, 6) |
||||
|
||||
# At the beginning, the default query is displayed |
||||
expect(filter_area.area) |
||||
.to have_selector('.subject', text: type_work_package.subject) |
||||
|
||||
expect(filter_area.area) |
||||
.to have_selector('.subject', text: other_type_work_package.subject) |
||||
|
||||
# User has the ability to modify the query |
||||
|
||||
modal.open_and_switch_to('Filters') |
||||
filters.expect_filter_count(2) |
||||
filters.add_filter_by('Type', 'is', type.name) |
||||
modal.save |
||||
|
||||
columns.remove 'Subject' |
||||
|
||||
expect(filter_area.area) |
||||
.to have_selector('.id', text: type_work_package.id) |
||||
|
||||
# as the Subject column is disabled |
||||
expect(filter_area.area) |
||||
.to have_no_selector('.subject', text: type_work_package.subject) |
||||
|
||||
# As other_type is filtered out |
||||
expect(filter_area.area) |
||||
.to have_no_selector('.id', text: other_type_work_package.id) |
||||
|
||||
scroll_to_element(filter_area.area) |
||||
within filter_area.area do |
||||
input = find('.editable-toolbar-title--input') |
||||
input.set('My WP Filter') |
||||
input.native.send_keys(:return) |
||||
end |
||||
|
||||
sleep(1) |
||||
|
||||
# The whole of the configuration survives a reload |
||||
# as it is persisted in the grid |
||||
|
||||
visit root_path |
||||
my_page.visit! |
||||
|
||||
filter_area = Components::Grids::GridArea.new('.grid--area.-widgeted:nth-of-type(3)') |
||||
expect(filter_area.area) |
||||
.to have_selector('.id', text: type_work_package.id) |
||||
|
||||
# as the Subject column is disabled |
||||
expect(filter_area.area) |
||||
.to have_no_selector('.subject', text: type_work_package.subject) |
||||
|
||||
# As other_type is filtered out |
||||
expect(filter_area.area) |
||||
.to have_no_selector('.id', text: other_type_work_package.id) |
||||
|
||||
within filter_area.area do |
||||
expect(find('.editable-toolbar-title--input').value) |
||||
.to eql('My WP Filter') |
||||
end |
||||
end |
||||
end |
||||
|
||||
context 'without the permission to save queries' do |
||||
let(:permissions) { %i[view_work_packages add_work_packages] } |
||||
|
||||
it 'cannot add the widget' do |
||||
my_page.add_column(3, before_or_after: :before) |
||||
|
||||
my_page.expect_unable_to_add_widget(2, 3, "Work packages") |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,76 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
|
||||
require_relative './shared_model' |
||||
|
||||
describe Grids::MyPage, type: :model do |
||||
let(:instance) { described_class.new(row_count: 5, column_count: 5) } |
||||
let(:user) { FactoryBot.build_stubbed(:user) } |
||||
|
||||
it_behaves_like 'grid attributes' |
||||
|
||||
context 'attributes' do |
||||
it '#user' do |
||||
instance.user = user |
||||
expect(instance.user) |
||||
.to eql user |
||||
end |
||||
end |
||||
|
||||
context 'altering widgets' do |
||||
context 'when removing a work_packages_table widget' do |
||||
let(:user) { FactoryBot.create(:user) } |
||||
let(:query) do |
||||
FactoryBot.create(:query, |
||||
user: user, |
||||
hidden: true) |
||||
end |
||||
|
||||
before do |
||||
widget = Grids::Widget.new(identifier: 'work_packages_table', |
||||
start_row: 1, |
||||
end_row: 2, |
||||
start_column: 1, |
||||
end_column: 2, |
||||
options: { queryId: query.id }) |
||||
|
||||
instance.widgets = [widget] |
||||
instance.save! |
||||
end |
||||
|
||||
it 'removes the widget\'s query' do |
||||
instance.widgets = [] |
||||
|
||||
expect(Query.find_by(id: query.id)) |
||||
.to be_nil |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,77 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
shared_examples_for 'grid attributes' do |
||||
describe 'attributes' do |
||||
it '#row_count' do |
||||
instance.row_count = 5 |
||||
expect(instance.row_count) |
||||
.to eql 5 |
||||
end |
||||
|
||||
it '#column_count' do |
||||
instance.column_count = 5 |
||||
expect(instance.column_count) |
||||
.to eql 5 |
||||
end |
||||
|
||||
it '#name' do |
||||
instance.name = 'custom 123' |
||||
expect(instance.name) |
||||
.to eql 'custom 123' |
||||
|
||||
# can be empty |
||||
instance.name = nil |
||||
expect(instance).to be_valid |
||||
end |
||||
|
||||
it '#options' do |
||||
value = { |
||||
some: 'value', |
||||
and: { |
||||
also: 1 |
||||
} |
||||
} |
||||
|
||||
instance.options = value |
||||
expect(instance.options) |
||||
.to eql value |
||||
end |
||||
|
||||
it '#widgets' do |
||||
widgets = [ |
||||
Grids::Widget.new(start_row: 2), |
||||
Grids::Widget.new(start_row: 5) |
||||
] |
||||
|
||||
instance.widgets = widgets |
||||
expect(instance.widgets) |
||||
.to match_array widgets |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,193 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'rack/test' |
||||
|
||||
describe "POST /api/v3/grids/form", type: :request, content_type: :json do |
||||
include Rack::Test::Methods |
||||
include API::V3::Utilities::PathHelper |
||||
|
||||
shared_let(:current_user) do |
||||
FactoryBot.create(:user) |
||||
end |
||||
|
||||
let(:path) { api_v3_paths.create_grid_form } |
||||
let(:params) { {} } |
||||
subject(:response) { last_response } |
||||
|
||||
before do |
||||
login_as(current_user) |
||||
end |
||||
|
||||
describe '#post' do |
||||
before do |
||||
post path, params.to_json, 'CONTENT_TYPE' => 'application/json' |
||||
end |
||||
|
||||
it 'contains a Schema embedding the available values' do |
||||
expect(subject.body) |
||||
.to be_json_eql("Schema".to_json) |
||||
.at_path('_embedded/schema/_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql(my_page_path.to_json) |
||||
.at_path('_embedded/schema/scope/_links/allowedValues/0/href') |
||||
end |
||||
|
||||
context 'with /my/page for the scope value' do |
||||
let(:params) do |
||||
{ |
||||
'_links': { |
||||
'scope': { |
||||
'href': my_page_path |
||||
} |
||||
} |
||||
} |
||||
end |
||||
|
||||
it 'contains default data in the payload' do |
||||
expected = { |
||||
"rowCount": 7, |
||||
"columnCount": 4, |
||||
"options": {}, |
||||
"widgets": [ |
||||
{ |
||||
"_type": "GridWidget", |
||||
identifier: 'work_packages_assigned', |
||||
"options": {}, |
||||
startRow: 1, |
||||
endRow: 7, |
||||
startColumn: 1, |
||||
endColumn: 3 |
||||
}, |
||||
{ |
||||
"_type": "GridWidget", |
||||
identifier: 'work_packages_created', |
||||
"options": {}, |
||||
startRow: 1, |
||||
endRow: 7, |
||||
startColumn: 3, |
||||
endColumn: 5 |
||||
} |
||||
], |
||||
"_links": { |
||||
"scope": { |
||||
"href": "/my/page", |
||||
"type": "text/html" |
||||
} |
||||
} |
||||
} |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql(expected.to_json) |
||||
.at_path('_embedded/payload') |
||||
end |
||||
|
||||
it 'has no validationErrors' do |
||||
expect(subject.body) |
||||
.to be_json_eql({}.to_json) |
||||
.at_path('_embedded/validationErrors') |
||||
end |
||||
|
||||
it 'has a commit link' do |
||||
expect(subject.body) |
||||
.to be_json_eql(api_v3_paths.grids.to_json) |
||||
.at_path('_links/commit/href') |
||||
end |
||||
end |
||||
|
||||
context 'with an unsupported widget identifier' do |
||||
let(:params) do |
||||
{ |
||||
'_links': { |
||||
'scope': { |
||||
'href': my_page_path |
||||
} |
||||
}, |
||||
"widgets": [ |
||||
{ |
||||
"_type": "GridWidget", |
||||
"identifier": "bogus_identifier", |
||||
"startRow": 4, |
||||
"endRow": 5, |
||||
"startColumn": 1, |
||||
"endColumn": 2 |
||||
} |
||||
] |
||||
} |
||||
end |
||||
|
||||
it 'has a validationError on widget' do |
||||
expect(subject.body) |
||||
.to be_json_eql("Widgets is not set to one of the allowed values.".to_json) |
||||
.at_path('_embedded/validationErrors/widgets/message') |
||||
end |
||||
end |
||||
|
||||
context 'with name set' do |
||||
let(:params) do |
||||
{ |
||||
name: 'My custom grid 1', |
||||
'_links': { |
||||
'scope': { |
||||
'href': my_page_path |
||||
} |
||||
} |
||||
} |
||||
end |
||||
|
||||
it 'feeds it back' do |
||||
expect(subject.body) |
||||
.to be_json_eql("My custom grid 1".to_json) |
||||
.at_path('_embedded/payload/name') |
||||
end |
||||
end |
||||
|
||||
context 'with options set' do |
||||
let(:params) do |
||||
{ |
||||
options: { |
||||
foo: 'bar' |
||||
}, |
||||
'_links': { |
||||
'scope': { |
||||
'href': my_page_path |
||||
} |
||||
} |
||||
} |
||||
end |
||||
|
||||
it 'feeds them back' do |
||||
expect(subject.body) |
||||
.to be_json_eql("bar".to_json) |
||||
.at_path('_embedded/payload/options/foo') |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,414 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'rack/test' |
||||
|
||||
describe 'API v3 Grids resource', type: :request, content_type: :json do |
||||
include Rack::Test::Methods |
||||
include API::V3::Utilities::PathHelper |
||||
|
||||
shared_let(:current_user) do |
||||
FactoryBot.create(:user) |
||||
end |
||||
|
||||
let(:my_page_grid) { FactoryBot.create(:my_page, user: current_user) } |
||||
let(:other_user) do |
||||
FactoryBot.create(:user) |
||||
end |
||||
let(:other_my_page_grid) { FactoryBot.create(:my_page, user: other_user) } |
||||
|
||||
before do |
||||
login_as(current_user) |
||||
end |
||||
|
||||
subject(:response) { last_response } |
||||
|
||||
describe '#get INDEX' do |
||||
let(:path) { api_v3_paths.grids } |
||||
|
||||
let(:stored_grids) do |
||||
my_page_grid |
||||
other_my_page_grid |
||||
end |
||||
|
||||
before do |
||||
stored_grids |
||||
|
||||
get path |
||||
end |
||||
|
||||
it 'sends a collection of grids but only those visible to the current user' do |
||||
expect(subject.body) |
||||
.to be_json_eql('Collection'.to_json) |
||||
.at_path('_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql('Grid'.to_json) |
||||
.at_path('_embedded/elements/0/_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql(1.to_json) |
||||
.at_path('total') |
||||
end |
||||
|
||||
context 'with a filter on the scope attribute' do |
||||
shared_let(:other_grid) do |
||||
grid = Grids::Grid.new(row_count: 20, |
||||
column_count: 20) |
||||
grid.save |
||||
|
||||
Grids::Grid |
||||
.where(id: grid.id) |
||||
.update_all(user_id: current_user.id) |
||||
|
||||
grid |
||||
end |
||||
|
||||
let(:stored_grids) do |
||||
my_page_grid |
||||
other_my_page_grid |
||||
other_grid |
||||
end |
||||
|
||||
let(:path) do |
||||
filter = [{ 'scope' => |
||||
{ |
||||
'operator' => '=', |
||||
'values' => [my_page_path] |
||||
} }] |
||||
|
||||
"#{api_v3_paths.grids}?#{{ filters: filter.to_json }.to_query}" |
||||
end |
||||
|
||||
it 'responds with 200 OK' do |
||||
expect(subject.status).to eq(200) |
||||
end |
||||
|
||||
it 'sends only the my page of the current user' do |
||||
expect(subject.body) |
||||
.to be_json_eql('Collection'.to_json) |
||||
.at_path('_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql('Grid'.to_json) |
||||
.at_path('_embedded/elements/0/_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql(1.to_json) |
||||
.at_path('total') |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#get' do |
||||
let(:path) { api_v3_paths.grid(my_page_grid.id) } |
||||
|
||||
let(:stored_grids) do |
||||
my_page_grid |
||||
end |
||||
|
||||
before do |
||||
stored_grids |
||||
|
||||
get path |
||||
end |
||||
|
||||
it 'responds with 200 OK' do |
||||
expect(subject.status).to eq(200) |
||||
end |
||||
|
||||
it 'sends a grid block' do |
||||
expect(subject.body) |
||||
.to be_json_eql('Grid'.to_json) |
||||
.at_path('_type') |
||||
end |
||||
|
||||
it 'identifies the url the grid is stored for' do |
||||
expect(subject.body) |
||||
.to be_json_eql(my_page_path.to_json) |
||||
.at_path('_links/scope/href') |
||||
end |
||||
|
||||
context 'with the page not existing' do |
||||
let(:path) { api_v3_paths.grid(5) } |
||||
|
||||
it 'responds with 404 NOT FOUND' do |
||||
expect(subject.status).to eql 404 |
||||
end |
||||
end |
||||
|
||||
context 'with the grid belonging to someone else' do |
||||
let(:stored_grids) do |
||||
my_page_grid |
||||
other_my_page_grid |
||||
end |
||||
|
||||
let(:path) { api_v3_paths.grid(other_my_page_grid.id) } |
||||
|
||||
it 'responds with 404 NOT FOUND' do |
||||
expect(subject.status).to eql 404 |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#patch' do |
||||
let(:path) { api_v3_paths.grid(my_page_grid.id) } |
||||
|
||||
let(:params) do |
||||
{ |
||||
"rowCount": 10, |
||||
"name": 'foo', |
||||
"columnCount": 15, |
||||
"widgets": [{ |
||||
"identifier": "work_packages_assigned", |
||||
"startRow": 4, |
||||
"endRow": 8, |
||||
"startColumn": 2, |
||||
"endColumn": 5 |
||||
}] |
||||
}.with_indifferent_access |
||||
end |
||||
|
||||
let(:stored_grids) do |
||||
my_page_grid |
||||
end |
||||
|
||||
before do |
||||
stored_grids |
||||
|
||||
patch path, params.to_json, 'CONTENT_TYPE' => 'application/json' |
||||
end |
||||
|
||||
it 'responds with 200 OK' do |
||||
expect(subject.status).to eq(200) |
||||
end |
||||
|
||||
it 'returns the altered grid block' do |
||||
expect(subject.body) |
||||
.to be_json_eql('Grid'.to_json) |
||||
.at_path('_type') |
||||
expect(subject.body) |
||||
.to be_json_eql('foo'.to_json) |
||||
.at_path('name') |
||||
expect(subject.body) |
||||
.to be_json_eql(params['rowCount'].to_json) |
||||
.at_path('rowCount') |
||||
expect(subject.body) |
||||
.to be_json_eql(params['widgets'][0]['identifier'].to_json) |
||||
.at_path('widgets/0/identifier') |
||||
end |
||||
|
||||
it 'perists the changes' do |
||||
expect(my_page_grid.reload.row_count) |
||||
.to eql params['rowCount'] |
||||
end |
||||
|
||||
context 'with invalid params' do |
||||
let(:params) do |
||||
{ |
||||
"rowCount": -5, |
||||
"columnCount": 15, |
||||
"widgets": [{ |
||||
"identifier": "work_packages_assigned", |
||||
"startRow": 4, |
||||
"endRow": 8, |
||||
"startColumn": 2, |
||||
"endColumn": 5 |
||||
}] |
||||
}.with_indifferent_access |
||||
end |
||||
|
||||
it 'responds with 422 and mentions the error' do |
||||
expect(subject.status).to eq 422 |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql('Error'.to_json) |
||||
.at_path('_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql("Widgets is outside of the grid.".to_json) |
||||
.at_path('_embedded/errors/0/message') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql("Number of rows must be greater than 0.".to_json) |
||||
.at_path('_embedded/errors/1/message') |
||||
end |
||||
|
||||
it 'does not persist the changes to widgets' do |
||||
expect(my_page_grid.reload.widgets.count) |
||||
.to eql MyPage::GridRegistration.defaults[:widgets].size |
||||
end |
||||
end |
||||
|
||||
context 'with a scope param' do |
||||
let(:params) do |
||||
{ |
||||
"_links": { |
||||
"scope": { |
||||
"href": '' |
||||
} |
||||
} |
||||
}.with_indifferent_access |
||||
end |
||||
|
||||
it 'responds with 422 and mentions the error' do |
||||
expect(subject.status).to eq 422 |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql('Error'.to_json) |
||||
.at_path('_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql("You must not write a read-only attribute.".to_json) |
||||
.at_path('message') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql("scope".to_json) |
||||
.at_path('_embedded/details/attribute') |
||||
end |
||||
end |
||||
|
||||
context 'with the page not existing' do |
||||
let(:path) { api_v3_paths.grid(5) } |
||||
|
||||
it 'responds with 404 NOT FOUND' do |
||||
expect(subject.status).to eql 404 |
||||
end |
||||
end |
||||
|
||||
context 'with the grid belonging to someone else' do |
||||
let(:stored_grids) do |
||||
my_page_grid |
||||
other_my_page_grid |
||||
end |
||||
|
||||
let(:path) { api_v3_paths.grid(other_my_page_grid.id) } |
||||
|
||||
it 'responds with 404 NOT FOUND' do |
||||
expect(subject.status).to eql 404 |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe '#post' do |
||||
let(:path) { api_v3_paths.grids } |
||||
|
||||
let(:params) do |
||||
{ |
||||
"rowCount": 10, |
||||
"columnCount": 15, |
||||
"widgets": [{ |
||||
"identifier": "work_packages_assigned", |
||||
"startRow": 4, |
||||
"endRow": 8, |
||||
"startColumn": 2, |
||||
"endColumn": 5 |
||||
}], |
||||
"_links": { |
||||
"scope": { |
||||
"href": my_page_path |
||||
} |
||||
} |
||||
}.with_indifferent_access |
||||
end |
||||
|
||||
before do |
||||
post path, params.to_json, 'CONTENT_TYPE' => 'application/json' |
||||
end |
||||
|
||||
it 'responds with 201 CREATED' do |
||||
expect(subject.status).to eq(201) |
||||
end |
||||
|
||||
it 'returns the created grid block' do |
||||
expect(subject.body) |
||||
.to be_json_eql('Grid'.to_json) |
||||
.at_path('_type') |
||||
expect(subject.body) |
||||
.to be_json_eql(params['rowCount'].to_json) |
||||
.at_path('rowCount') |
||||
expect(subject.body) |
||||
.to be_json_eql(params['widgets'][0]['identifier'].to_json) |
||||
.at_path('widgets/0/identifier') |
||||
end |
||||
|
||||
it 'persists the grid' do |
||||
expect(Grids::Grid.count) |
||||
.to eql(1) |
||||
end |
||||
|
||||
context 'with invalid params' do |
||||
let(:params) do |
||||
{ |
||||
"rowCount": -5, |
||||
"columnCount": "sdjfksdfsdfdsf", |
||||
"widgets": [{ |
||||
"identifier": "work_packages_assigned", |
||||
"startRow": 4, |
||||
"endRow": 8, |
||||
"startColumn": 2, |
||||
"endColumn": 5 |
||||
}], |
||||
"_links": { |
||||
"scope": { |
||||
"href": my_page_path |
||||
} |
||||
} |
||||
}.with_indifferent_access |
||||
end |
||||
|
||||
it 'responds with 422' do |
||||
expect(subject.status).to eq(422) |
||||
end |
||||
|
||||
it 'does not create a grid' do |
||||
expect(Grids::Grid.count) |
||||
.to eql(0) |
||||
end |
||||
|
||||
it 'returns the errors' do |
||||
expect(subject.body) |
||||
.to be_json_eql('Error'.to_json) |
||||
.at_path('_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql("Widgets is outside of the grid.".to_json) |
||||
.at_path('_embedded/errors/0/message') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql("Number of rows must be greater than 0.".to_json) |
||||
.at_path('_embedded/errors/1/message') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql("Number of columns must be greater than 0.".to_json) |
||||
.at_path('_embedded/errors/2/message') |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,174 @@ |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2018 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 docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require 'rack/test' |
||||
|
||||
describe "PATCH /api/v3/grids/:id/form", type: :request, content_type: :json do |
||||
include Rack::Test::Methods |
||||
include API::V3::Utilities::PathHelper |
||||
|
||||
shared_let(:current_user) do |
||||
FactoryBot.create(:user) |
||||
end |
||||
|
||||
let(:grid) do |
||||
FactoryBot.create(:my_page, user: current_user) |
||||
end |
||||
let(:path) { api_v3_paths.grid_form(grid.id) } |
||||
let(:params) { {} } |
||||
subject(:response) { last_response } |
||||
|
||||
before do |
||||
login_as(current_user) |
||||
end |
||||
|
||||
describe '#post' do |
||||
before do |
||||
post path, params.to_json, 'CONTENT_TYPE' => 'application/json' |
||||
end |
||||
|
||||
it 'returns 200 OK' do |
||||
expect(subject.status) |
||||
.to eql 200 |
||||
end |
||||
|
||||
it 'is of type form' do |
||||
expect(subject.body) |
||||
.to be_json_eql("Form".to_json) |
||||
.at_path('_type') |
||||
end |
||||
|
||||
it 'contains a Schema disallowing setting scope' do |
||||
expect(subject.body) |
||||
.to be_json_eql("Schema".to_json) |
||||
.at_path('_embedded/schema/_type') |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql(false.to_json) |
||||
.at_path('_embedded/schema/scope/writable') |
||||
end |
||||
|
||||
it 'contains the current data in the payload' do |
||||
expected = { |
||||
rowCount: 7, |
||||
columnCount: 4, |
||||
options: {}, |
||||
widgets: [ |
||||
{ |
||||
"_type": "GridWidget", |
||||
identifier: 'work_packages_assigned', |
||||
options: {}, |
||||
startRow: 1, |
||||
endRow: 7, |
||||
startColumn: 1, |
||||
endColumn: 3 |
||||
}, |
||||
{ |
||||
"_type": "GridWidget", |
||||
identifier: 'work_packages_created', |
||||
options: {}, |
||||
startRow: 1, |
||||
endRow: 7, |
||||
startColumn: 3, |
||||
endColumn: 5 |
||||
} |
||||
], |
||||
"_links": { |
||||
"scope": { |
||||
"href": "/my/page", |
||||
"type": "text/html" |
||||
} |
||||
} |
||||
} |
||||
|
||||
expect(subject.body) |
||||
.to be_json_eql(expected.to_json) |
||||
.at_path('_embedded/payload') |
||||
end |
||||
|
||||
it 'has a commit link' do |
||||
expect(subject.body) |
||||
.to be_json_eql(api_v3_paths.grid(grid.id).to_json) |
||||
.at_path('_links/commit/href') |
||||
end |
||||
|
||||
context 'with some value for the scope value' do |
||||
let(:params) do |
||||
{ |
||||
'_links': { |
||||
'scope': { |
||||
'href': '/some/path' |
||||
} |
||||
} |
||||
} |
||||
end |
||||
|
||||
it 'has a validation error on scope as the value is not writeable' do |
||||
expect(subject.body) |
||||
.to be_json_eql("You must not write a read-only attribute.".to_json) |
||||
.at_path('_embedded/validationErrors/scope/message') |
||||
end |
||||
end |
||||
|
||||
context 'with an unsupported widget identifier' do |
||||
let(:params) do |
||||
{ |
||||
"widgets": [ |
||||
{ |
||||
"_type": "GridWidget", |
||||
"identifier": "bogus_identifier", |
||||
"startRow": 4, |
||||
"endRow": 5, |
||||
"startColumn": 1, |
||||
"endColumn": 2 |
||||
} |
||||
] |
||||
} |
||||
end |
||||
|
||||
it 'has a validationError on widget' do |
||||
expect(subject.body) |
||||
.to be_json_eql("Widgets is not set to one of the allowed values.".to_json) |
||||
.at_path('_embedded/validationErrors/widgets/message') |
||||
end |
||||
end |
||||
|
||||
context 'for another user\'s grid' do |
||||
let(:other_user) { FactoryBot.create(:user) } |
||||
let(:other_grid) { FactoryBot.create(:my_page, user: other_user) } |
||||
|
||||
let(:path) { api_v3_paths.grid_form(other_grid.id) } |
||||
|
||||
it 'returns 404 NOT FOUND' do |
||||
expect(subject.status) |
||||
.to eql 404 |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue