parent
cc41884048
commit
3f677298ad
@ -0,0 +1,13 @@ |
||||
<widget-header |
||||
[name]="widgetName" |
||||
[icon]="'flag'" |
||||
(onRenamed)="renameWidget($event)"> |
||||
|
||||
<widget-menu |
||||
[resource]="resource"> |
||||
</widget-menu> |
||||
</widget-header> |
||||
|
||||
<div class="grid--widget-content" |
||||
#contentContainer> |
||||
</div> |
@ -0,0 +1,170 @@ |
||||
// -- 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 {Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, Injector, ViewChild, ElementRef} from '@angular/core'; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {ProjectDmService} from "core-app/modules/hal/dm-services/project-dm.service"; |
||||
import {CurrentProjectService} from "core-components/projects/current-project.service"; |
||||
import {SchemaResource} from "core-app/modules/hal/resources/schema-resource"; |
||||
import {DisplayFieldContext, DisplayFieldService} from "core-app/modules/fields/display/display-field.service"; |
||||
import {ProjectResource} from "core-app/modules/hal/resources/project-resource"; |
||||
import {PortalCleanupService} from 'core-app/modules/fields/display/display-portal/portal-cleanup.service'; |
||||
import {DisplayField} from "core-app/modules/fields/display/display-field.module"; |
||||
import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table/state/wp-table-highlighting.service"; |
||||
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; |
||||
|
||||
export const emptyPlaceholder = '-'; |
||||
|
||||
@Component({ |
||||
templateUrl: './project-details.component.html', |
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
providers: [ |
||||
// required by the displayField service to render the fields
|
||||
PortalCleanupService, |
||||
WorkPackageTableHighlightingService, |
||||
IsolatedQuerySpace |
||||
] |
||||
}) |
||||
export class WidgetProjectDetailsComponent extends AbstractWidgetComponent implements OnInit { |
||||
public customFieldsMap:Array<DisplayField> = []; |
||||
|
||||
@ViewChild('contentContainer', { static: true }) readonly contentContainer:ElementRef; |
||||
|
||||
constructor(protected readonly i18n:I18nService, |
||||
protected readonly injector:Injector, |
||||
protected readonly projectDm:ProjectDmService, |
||||
protected readonly currentProject:CurrentProjectService, |
||||
protected readonly displayField:DisplayFieldService, |
||||
protected readonly cdr:ChangeDetectorRef) { |
||||
super(i18n, injector); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.loadAndRender(); |
||||
} |
||||
|
||||
private loadAndRender() { |
||||
Promise.all( |
||||
[this.loadCurrentProject(), |
||||
this.loadProjectSchema()] |
||||
) |
||||
.then(([project, schema]) => { |
||||
this.renderCFs(project, schema as SchemaResource); |
||||
|
||||
this.redraw(); |
||||
}); |
||||
} |
||||
|
||||
private loadCurrentProject() { |
||||
return this.projectDm.load(this.currentProject.id as string); |
||||
} |
||||
|
||||
private loadProjectSchema() { |
||||
return this.projectDm.schema(); |
||||
} |
||||
|
||||
private renderCFs(project:ProjectResource, schema:SchemaResource) { |
||||
const cfFields = this.collectFieldsForCfs(project, schema); |
||||
|
||||
this.sortFieldsLexicographically(cfFields); |
||||
this.renderFields(cfFields); |
||||
} |
||||
|
||||
private collectFieldsForCfs(project:ProjectResource, schema:SchemaResource) { |
||||
let displayFields:Array<DisplayField> = []; |
||||
// passing an arbitrary context to displayField.getField only to satisfy the interface
|
||||
let context:DisplayFieldContext = {injector: this.injector, container: 'table', options: []}; |
||||
|
||||
Object.entries(schema).forEach(([key, keySchema]) => { |
||||
if (key.match(/customField\d+/)) { |
||||
let field = this.displayField.getField(project, key, keySchema, context); |
||||
|
||||
displayFields.push(field); |
||||
} |
||||
}); |
||||
|
||||
return displayFields; |
||||
} |
||||
|
||||
private sortFieldsLexicographically(fields:Array<DisplayField>) { |
||||
fields.sort((a, b) => { return a.label.localeCompare(b.label); }); |
||||
} |
||||
|
||||
private renderFields(fields:Array<DisplayField>) { |
||||
this.contentContainer.nativeElement.innerHTML = ''; |
||||
|
||||
fields.forEach(field => { |
||||
this.contentContainer.nativeElement.appendChild(this.displayKeyValue(field)); |
||||
}); |
||||
} |
||||
|
||||
private displayKeyValue(field:DisplayField) { |
||||
const container = this.containerElement(); |
||||
|
||||
container.appendChild(this.labelElement(field)); |
||||
container.appendChild(this.valueElement(field)); |
||||
|
||||
return container; |
||||
} |
||||
|
||||
private containerElement() { |
||||
const container = document.createElement('div'); |
||||
container.classList.add('attributes-key-value'); |
||||
|
||||
return container; |
||||
} |
||||
|
||||
private labelElement(field:DisplayField) { |
||||
const label = document.createElement('div'); |
||||
label.classList.add('attributes-key-value--key'); |
||||
label.innerText = field.label; |
||||
|
||||
return label; |
||||
} |
||||
|
||||
private valueElement(field:DisplayField) { |
||||
const value = document.createElement('div'); |
||||
value.classList.add('attributes-key-value--value-container'); |
||||
field.render(value, this.getText(field)); |
||||
|
||||
return value; |
||||
} |
||||
|
||||
private getText(field:DisplayField):string { |
||||
if (field.isEmpty()) { |
||||
return emptyPlaceholder; |
||||
} else { |
||||
return field.valueString; |
||||
} |
||||
} |
||||
|
||||
private redraw() { |
||||
this.cdr.detectChanges(); |
||||
} |
||||
} |
@ -0,0 +1,118 @@ |
||||
#-- 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 '../support/pages/dashboard' |
||||
|
||||
describe 'Project details widget on dashboard', type: :feature, js: true do |
||||
let!(:version_cf) { FactoryBot.create(:version_project_custom_field) } |
||||
let!(:bool_cf) { FactoryBot.create(:bool_project_custom_field) } |
||||
let!(:user_cf) { FactoryBot.create(:user_project_custom_field) } |
||||
let!(:int_cf) { FactoryBot.create(:int_project_custom_field) } |
||||
let!(:float_cf) { FactoryBot.create(:float_project_custom_field) } |
||||
let!(:text_cf) { FactoryBot.create(:text_project_custom_field) } |
||||
let!(:string_cf) { FactoryBot.create(:string_project_custom_field) } |
||||
let!(:date_cf) { FactoryBot.create(:date_project_custom_field) } |
||||
|
||||
let(:system_version) { FactoryBot.create(:version, sharing: 'system') } |
||||
|
||||
let!(:project) do |
||||
FactoryBot.create(:project).tap do |p| |
||||
p.add_member(other_user, [role]) |
||||
|
||||
p.send(:"custom_field_#{int_cf.id}=", 5) |
||||
p.send(:"custom_field_#{bool_cf.id}=", true) |
||||
p.send(:"custom_field_#{version_cf.id}=", system_version) |
||||
p.send(:"custom_field_#{float_cf.id}=", 4.5) |
||||
p.send(:"custom_field_#{text_cf.id}=", 'Some **long** text') |
||||
p.send(:"custom_field_#{string_cf.id}=", 'Some small text') |
||||
p.send(:"custom_field_#{date_cf.id}=", Date.today) |
||||
p.send(:"custom_field_#{user_cf.id}=", other_user) |
||||
|
||||
p.save!(validate: false) |
||||
end |
||||
end |
||||
|
||||
let(:permissions) do |
||||
%i[view_dashboards |
||||
manage_dashboards] |
||||
end |
||||
|
||||
let(:role) do |
||||
FactoryBot.create(:role, permissions: permissions) |
||||
end |
||||
|
||||
let(:user) do |
||||
FactoryBot.create(:user, member_in_project: project, member_through_role: role) |
||||
end |
||||
let(:other_user) do |
||||
FactoryBot.create(:user) |
||||
end |
||||
let(:dashboard_page) do |
||||
Pages::Dashboard.new(project) |
||||
end |
||||
|
||||
before do |
||||
login_as user |
||||
|
||||
dashboard_page.visit! |
||||
end |
||||
|
||||
it 'can add the widget and see the description in it' do |
||||
dashboard_page.add_column(3, before_or_after: :before) |
||||
|
||||
dashboard_page.add_widget(2, 3, "Project details") |
||||
|
||||
sleep(0.1) |
||||
|
||||
# As the user lacks the manage_public_queries and save_queries permission, no other widget is present |
||||
details_widget = Components::Grids::GridArea.new('.grid--area.-widgeted:nth-of-type(1)') |
||||
details_widget.resize_to(6, 5) |
||||
details_widget.expect_to_span(2, 3, 7, 6) |
||||
|
||||
within(details_widget.area) do |
||||
expect(page) |
||||
.to have_content("#{int_cf.name}\n5") |
||||
expect(page) |
||||
.to have_content("#{bool_cf.name}\nyes") |
||||
expect(page) |
||||
.to have_content("#{version_cf.name}\n#{system_version.name}") |
||||
expect(page) |
||||
.to have_content("#{float_cf.name}\n4.5") |
||||
expect(page) |
||||
.to have_content("#{text_cf.name}\nSome long text") |
||||
expect(page) |
||||
.to have_content("#{string_cf.name}\nSome small text") |
||||
expect(page) |
||||
.to have_content("#{date_cf.name}\n#{Date.today.strftime('%m/%d/%Y')}") |
||||
expect(page) |
||||
.to have_content("#{user_cf.name}\n#{user.name.split.map(&:first).join}#{user.name}") |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue