Merge pull request #6834 from opf/feature/grid_my_page
Feature/grid my page [ci skip]pull/7011/head
commit
9d22e7a457
@ -1,139 +0,0 @@ |
||||
//-- 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.
|
||||
//++
|
||||
|
||||
jQuery(document).ready(function($) { |
||||
|
||||
// Add block
|
||||
$('.my-page--block-form').submit(function (evt) { |
||||
var form = $(this); |
||||
var block = $('#block-options').val(); |
||||
evt.preventDefault(); |
||||
|
||||
jQuery |
||||
.ajax( |
||||
{ |
||||
url: form.attr('action'), |
||||
type: 'POST', |
||||
dataType: 'html', |
||||
data: { |
||||
block: block |
||||
} |
||||
}) |
||||
.fail(function(error, text) { |
||||
jQuery(window).trigger( |
||||
'op:notifications:add', |
||||
{ |
||||
type: 'error', |
||||
message: I18n.t('js.error.cannot_save_changes_with_message', { error: _.get(error, 'responseText', 'Internal error') }) |
||||
} |
||||
); |
||||
}) |
||||
.done(function(response) { |
||||
// Add partial to the top container.
|
||||
jQuery("#top").prepend(response); |
||||
|
||||
// Revert options selection back to the default one.
|
||||
jQuery('#block-options option').first().prop('selected', true); |
||||
|
||||
// Disable the option for this block in the blocks-to-add-to-page dropdown.
|
||||
jQuery('#block-options option[value="' + block + '"]').attr("disabled", "true"); |
||||
}); |
||||
|
||||
return false; |
||||
}); |
||||
|
||||
// Remove block
|
||||
$('.my-page--container') |
||||
.on('click', '.my-page--remove-block', function (evt) { |
||||
evt.preventDefault(); |
||||
|
||||
var link = $(this); |
||||
var block = link.data('name'); |
||||
var dasherized = link.data('dasherized'); |
||||
|
||||
jQuery |
||||
.ajax( |
||||
{ |
||||
url: link.attr('href'), |
||||
type: 'POST', |
||||
dataType: 'html', |
||||
data: { |
||||
block: block |
||||
} |
||||
}) |
||||
.fail(function(error) { |
||||
jQuery(window).trigger( |
||||
'op:notifications:add', |
||||
{ |
||||
type: 'error', |
||||
message: I18n.t('js.error.cannot_save_changes_with_message', { error: _.get(error, 'responseText', 'Internal error') }) |
||||
} |
||||
); |
||||
}) |
||||
.done(function() { |
||||
jQuery("#block-" + dasherized).remove(); |
||||
// Enable the option for this block in the blocks-to-add-to-page dropdown.
|
||||
jQuery('#block-options option[value="' + block + '"]').removeAttr("disabled"); |
||||
}); |
||||
|
||||
return false; |
||||
}); |
||||
|
||||
// Canonical list of containers that will exchange draggable elements.
|
||||
var containers = jQuery('.dragula-container').toArray(); |
||||
var drake = dragula(containers); |
||||
|
||||
// On 'el' drop, we fire an Ajax request to persist the order chosen by
|
||||
// the user. Actual ordering details are handled on the server.
|
||||
drake.on('drop', function(el, target, source, sibling){ |
||||
var url = window.gon.my_order_blocks_url; |
||||
|
||||
// Array of target ordered children after this drop.
|
||||
var target_ordered_children = jQuery(target).find('.block-wrapper').map(function(){ |
||||
return jQuery(this).data('name'); |
||||
}).get(); |
||||
|
||||
// Array of source ordered children after this drop.
|
||||
var source_ordered_children = jQuery(source).find('.block-wrapper').map(function(){ |
||||
return jQuery(this).data('name'); |
||||
}).get(); |
||||
|
||||
// We send the source, target, and the new order of the children in both
|
||||
// containers to the server.
|
||||
jQuery.ajax({ |
||||
url: url, |
||||
type: 'POST', |
||||
data: { |
||||
target: jQuery(target).attr('id'), |
||||
source: jQuery(source).attr('id'), |
||||
target_ordered_children: target_ordered_children, |
||||
source_ordered_children: source_ordered_children |
||||
} |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,127 @@ |
||||
$grid--gap-width: 20px |
||||
$grid--header-width: 20px |
||||
|
||||
@mixin grid--commons |
||||
display: grid |
||||
grid-column-gap: $grid--gap-width |
||||
grid-row-gap: $grid--gap-width |
||||
|
||||
.grid--container |
||||
@include grid--commons |
||||
|
||||
.grid--area |
||||
overflow: hidden |
||||
|
||||
&.-drop-target |
||||
background-color: $gray-light |
||||
|
||||
&.-resize-target |
||||
z-index: 1000 |
||||
|
||||
&.-widgeted |
||||
z-index: 10 |
||||
@include widget-box--style |
||||
margin: 0 |
||||
padding: 20px |
||||
|
||||
.widget-box |
||||
height: 100% |
||||
|
||||
&.-resizing |
||||
border: 1px solid $primary-color |
||||
z-index: 100 |
||||
|
||||
&.-addable |
||||
display: flex |
||||
flex-direction: column |
||||
justify-content: center |
||||
align-items: center |
||||
|
||||
.resizer |
||||
margin-top: 0 |
||||
margin-left: auto |
||||
margin-right: -20px |
||||
position: relative |
||||
|
||||
.cdk-drag-handle |
||||
cursor: grab |
||||
|
||||
.grid--area-content |
||||
height: 100% |
||||
|
||||
ng-component |
||||
display: flex |
||||
flex-direction: column |
||||
height: 100% |
||||
|
||||
.grid--widget-content |
||||
height: 100% |
||||
overflow-x: auto |
||||
overflow-y: auto |
||||
|
||||
.grid--widget-limited-text |
||||
max-height: 5rem |
||||
position: relative |
||||
overflow: hidden |
||||
|
||||
&:before |
||||
content: '' |
||||
width: 100% |
||||
height: 100% |
||||
position: absolute |
||||
left: 0 |
||||
top: 0 |
||||
background: linear-gradient(to bottom, rgba($body-background, 0) 60%, rgba($body-background, 1)) |
||||
|
||||
.grid--widget-add |
||||
padding: 15px |
||||
background-color: $gray |
||||
border-radius: 50% |
||||
|
||||
&:before |
||||
@include icon-font-common |
||||
@include icon-mixin-add |
||||
|
||||
.grid--widget-remove |
||||
float: right |
||||
margin-top: -10px |
||||
margin-right: -10px |
||||
cursor: pointer |
||||
|
||||
&:before |
||||
@include icon-font-common |
||||
@include icon-mixin-remove |
||||
font-size: 0.75em |
||||
|
||||
.grid--column-headers |
||||
@include grid--commons |
||||
grid-template-rows: $grid--header-width |
||||
|
||||
.grid--row-headers |
||||
@include grid--commons |
||||
grid-template-columns: $grid--header-width |
||||
float: left |
||||
margin-left: -$grid--header-width |
||||
|
||||
.grid--header |
||||
background-color: $gray-light |
||||
transition: background-color 4s ease |
||||
|
||||
&:hover |
||||
background-color: $gray |
||||
transition: background-color 1s ease |
||||
|
||||
.grid--addable-widget |
||||
min-width: 400px |
||||
padding: 20px 5px |
||||
border-bottom: 1px solid $gray |
||||
cursor: pointer |
||||
background: none |
||||
transition: background 4s ease |
||||
|
||||
&:hover |
||||
background: $gray-light |
||||
transition: background 1s ease |
||||
|
||||
&:last-of-type |
||||
border-bottom: none |
@ -0,0 +1,7 @@ |
||||
.resizer |
||||
width: 0 |
||||
height: 0 |
||||
border-bottom: 20px solid $primary-color |
||||
border-left: 20px solid transparent |
||||
cursor: nwse-resize |
||||
|
@ -0,0 +1,68 @@ |
||||
#-- 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 Queries::Filters::Shared::MeValueFilter |
||||
## |
||||
# Return the values object with the me value |
||||
# mapped to the current user. |
||||
def values_replaced |
||||
vals = values.clone |
||||
|
||||
if vals.delete(me_value_key) |
||||
if User.current.logged? |
||||
vals.push(User.current.id.to_s) |
||||
else |
||||
vals.push('0') |
||||
end |
||||
end |
||||
|
||||
vals |
||||
end |
||||
|
||||
protected |
||||
|
||||
## |
||||
# Returns the me value if the user is logged |
||||
def me_allowed_value |
||||
values = [] |
||||
if User.current.logged? |
||||
values << [me_label, me_value_key] |
||||
end |
||||
values |
||||
end |
||||
|
||||
def me_label |
||||
I18n.t(:label_me) |
||||
end |
||||
|
||||
def me_value_key |
||||
::Queries::Filters::MeValue::KEY |
||||
end |
||||
end |
@ -1,46 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
|
||||
<div id="block-<%= block_name.dasherize %>" class="widget-box block-wrapper" data-name='<%= block_name %>'> |
||||
|
||||
<% content_for "#{block_name}-remove-block" do %> |
||||
<div class="box-actions"> |
||||
<%= link_to '', |
||||
{ action: "remove_block", block: block_name }, |
||||
class: "icon icon-close close-icon my-page--remove-block", |
||||
data: { name: block_name, dasherized: block_name.dasherize }, |
||||
title: l(:button_remove_widget) |
||||
%> |
||||
</div> |
||||
<% end %> |
||||
|
||||
<div class='handle'> |
||||
<%= render partial: "my/blocks/#{block_name}", locals: { user: user, edit: true, block_name: block_name } %> |
||||
</div> |
||||
</div> |
@ -1,12 +0,0 @@ |
||||
<% unless blocks.nil? || blocks.empty? %> |
||||
<% blocks.each do |block_name| %> |
||||
<% next unless MyController.available_blocks.keys.include? block_name %> |
||||
<% if edit %> |
||||
<%= render partial: "my/block", locals: { block_name: block_name, user: @user } %> |
||||
<% else %> |
||||
<div class="widget-box"> |
||||
<%= render partial: "my/blocks/#{block_name}", locals: { edit: false, user: @user } %> |
||||
</div> |
||||
<% end %> |
||||
<% end %> |
||||
<% end %> |
@ -1,4 +0,0 @@ |
||||
<%= render partial: 'block', |
||||
locals: { user: @user, |
||||
block_name: @block |
||||
} %> |
@ -1,45 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
|
||||
<% if defined? block_name %> |
||||
<%= content_for "#{block_name}-remove-block" %> |
||||
<% end %> |
||||
|
||||
<h3 class="widget-box--header"> |
||||
<%= op_icon('icon-context icon-calendar') %> |
||||
<span class="widget-box--header-title"><%=l(:label_calendar)%></span> |
||||
</h3> |
||||
|
||||
<% if edit %> |
||||
<div class="macro -embedded-table ck-widget"> |
||||
<%= t('js.editor.macro.embedded_calendar.text') %> |
||||
</div> |
||||
<% else %> |
||||
<wp-embedded-calendar></wp-embedded-calendar> |
||||
<% end %> |
@ -1,7 +0,0 @@ |
||||
<% if edit %> |
||||
<div class="macro -embedded-table ck-widget"> |
||||
<%= t('js.editor.macro.embedded_table.text') %> |
||||
</div> |
||||
<% else %> |
||||
<%= render partial: 'work_packages/me_list_simple', locals: { user_filter: user_filter } %> |
||||
<% end %> |
@ -1,46 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
|
||||
<% if defined? block_name %> |
||||
<%= content_for "#{block_name}-remove-block" %> |
||||
<% end %> |
||||
|
||||
<h3 class="widget-box--header"> |
||||
<%= op_icon('icon-context icon-assigned-to-me') %> |
||||
<span class="widget-box--header-title"><%=l(:label_assigned_to_me_work_packages)%></span> |
||||
</h3> |
||||
|
||||
<%= render partial: 'my/blocks/embedded_work_package_table', |
||||
locals: { edit: edit, user_filter: :assignee } %> |
||||
|
||||
<% content_for :header_tags do %> |
||||
<%= auto_discovery_link_tag(:atom, |
||||
work_packages_assigned_to_me_path({format: 'atom', key: User.current.rss_key}), |
||||
{title: l(:label_assigned_to_me_work_packages)}) %> |
||||
<% end %> |
@ -1,46 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
|
||||
<% if defined? block_name %> |
||||
<%= content_for "#{block_name}-remove-block" %> |
||||
<% end %> |
||||
|
||||
<h3 class="widget-box--header"> |
||||
<%= op_icon('icon-context icon-reported-by-me') %> |
||||
<span class="widget-box--header-title"><%=l(:label_reported_work_packages)%></span> |
||||
</h3> |
||||
|
||||
<%= render partial: 'my/blocks/embedded_work_package_table', |
||||
locals: { edit: edit, user_filter: :author } %> |
||||
|
||||
<% content_for :header_tags do %> |
||||
<%= auto_discovery_link_tag(:atom, |
||||
work_packages_reported_by_me_path({format: 'atom', key: User.current.rss_key}), |
||||
{title: l(:label_reported_work_packages)}) %> |
||||
<% end %> |
@ -1,39 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
<% if defined? block_name %> |
||||
<%= content_for "#{block_name}-remove-block" %> |
||||
<% end %> |
||||
|
||||
<h3 class="widget-box--header"> |
||||
<%= op_icon('icon-context icon-preview') %> |
||||
<span class="widget-box--header-title"><%=l(:label_watched_work_packages)%></span> |
||||
</h3> |
||||
|
||||
<%= render partial: 'my/blocks/embedded_work_package_table', |
||||
locals: { edit: edit, user_filter: :watcher } %> |
@ -1,63 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
<% news = News.limit(10) |
||||
.order("#{News.table_name}.created_on DESC") |
||||
.where("#{News.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})") |
||||
.includes(:project, :author) unless @user.projects.empty? %> |
||||
|
||||
<% if defined? block_name %> |
||||
<%= content_for "#{block_name}-remove-block" %> |
||||
<% end %> |
||||
|
||||
<h3 class="widget-box--header"> |
||||
<%= op_icon('icon-context icon-news') %> |
||||
<span class="widget-box--header-title"><%= l(:label_news_latest) %></span> |
||||
</h3> |
||||
|
||||
<% news = News.latest(count: 3) %> |
||||
|
||||
<% if news.empty? %> |
||||
<%= no_results_box(custom_title: t('news.my_page.no_results_title_text')) %> |
||||
<% end %> |
||||
|
||||
<% unless news.empty? %> |
||||
<ul class="widget-box--arrow-links"> |
||||
<% news.each do |news| %> |
||||
<li class="-widget-box--arrow-multiline"> |
||||
<div> |
||||
<%= avatar(news.author, {class: 'avatar-mini'}) %> |
||||
<%= link_to_project(news.project) + ': ' %> |
||||
<%= link_to h(news.title), news_path(news) %> |
||||
<br/> |
||||
<p class="widget-box--additional-info"><%= authoring news.created_on, news.author %></p> |
||||
</div> |
||||
</li> |
||||
<% end %> |
||||
</ul> |
||||
<% end %> |
@ -1,142 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
|
||||
<% if defined? block_name %> |
||||
<%= content_for "#{block_name}-remove-block" %> |
||||
<% end %> |
||||
|
||||
<h3 class="widget-box--header"> |
||||
<%= op_icon('icon-context icon-time') %> |
||||
<span class="widget-box--header-title"><%=l(:label_spent_time)%> (<%= l(:label_last_n_days, 7) %>)</span> |
||||
</h3> |
||||
|
||||
<% |
||||
entries = TimeEntry.where(["#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", @user.id, Date.today - 6, Date.today]) |
||||
.includes(:activity, :project, { work_package: [:type, :status] }) |
||||
.references(:projects) |
||||
.order("#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{::Type.table_name}.position ASC, #{WorkPackage.table_name}.id ASC") |
||||
entries_by_day = entries.group_by(&:spent_on) |
||||
%> |
||||
|
||||
<div class="total-hours"> |
||||
<p><%= l(:label_total) %>: <%= html_hours("%.2f" % entries.sum(:hours).to_f) %></p> |
||||
</div> |
||||
|
||||
<% if entries.any? %> |
||||
<div class="generic-table--container"> |
||||
<div class="generic-table--results-container"> |
||||
<table class="generic-table time-entries"> |
||||
<colgroup> |
||||
<col highlight-col> |
||||
<col highlight-col> |
||||
<col highlight-col> |
||||
<col highlight-col> |
||||
<col> |
||||
</colgroup> |
||||
<thead> |
||||
<tr> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span> |
||||
<%= l(:label_activity) %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span> |
||||
<%= Project.model_name.human %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span> |
||||
<%= TimeEntry.human_attribute_name(:comments) %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span> |
||||
<%= TimeEntry.human_attribute_name(:hours) %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th><div class="generic-table--empty-header"></div></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<% entries_by_day.keys.sort.reverse.each do |day| %> |
||||
<tr> |
||||
<td><strong><%= day == Date.today ? l(:label_today).titleize : format_date(day) %></strong></td> |
||||
<td></td> |
||||
<td></td> |
||||
<td class="hours"><em><%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %></em></td> |
||||
<td></td> |
||||
</tr> |
||||
<% entries_by_day[day].each do |entry| -%> |
||||
<tr class="time-entry" style="border-bottom: 1px solid #f5f5f5;"> |
||||
<td class="activity"><%=h entry.activity %></td> |
||||
<td class="subject"><%=h entry.project %> <%= ' - '.html_safe + link_to_work_package(entry.work_package, truncate: 50) if entry.work_package%></td> |
||||
<td class="comments"><%=h entry.comments %></td> |
||||
<td class="hours"><%= html_hours("%.2f" % entry.hours) %></td> |
||||
<td class="buttons"> |
||||
<% if entry.editable_by?(@user) -%> |
||||
<%= link_to icon_wrapper('icon-context icon-edit', ''), |
||||
{controller: '/timelog', action: 'edit', id: entry}, |
||||
alt: l(:button_edit), |
||||
class: 'no-decoration-on-hover', |
||||
title: l(:button_edit) %> |
||||
<%= link_to icon_wrapper('icon-context icon-delete', ''), |
||||
{controller: '/timelog', action: 'destroy', id: entry}, |
||||
data: { confirm: l(:text_are_you_sure) }, |
||||
class: 'no-decoration-on-hover', |
||||
method: :delete, |
||||
alt: l(:button_delete), |
||||
title: l(:button_delete) %> |
||||
<% end -%> |
||||
</td> |
||||
</tr> |
||||
<% end -%> |
||||
<% end -%> |
||||
</tbody> |
||||
</table> |
||||
|
||||
</div> |
||||
</div> |
||||
<% end %> |
@ -1,45 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
<% if defined? block_name %> |
||||
<%= content_for "#{block_name}-remove-block" %> |
||||
<% end %> |
||||
|
||||
<h3 class="widget-box--header"> |
||||
<%= op_icon('icon-context icon-assigned-to-me') %> |
||||
<span class="widget-box--header-title"><%=l(:label_responsible_for_work_packages)%></span> |
||||
</h3> |
||||
|
||||
<%= render partial: 'my/blocks/embedded_work_package_table', |
||||
locals: { edit: edit, user_filter: :responsible } %> |
||||
|
||||
<% content_for :header_tags do %> |
||||
<%= auto_discovery_link_tag(:atom, |
||||
work_packages_responsible_for_path({format: 'atom', key: User.current.rss_key}), |
||||
{title: l(:label_responsible_for_work_packages)}) %> |
||||
<% end %> |
@ -1,71 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
<%= javascript_include_tag 'my_page' %> |
||||
<%= nonced_javascript_tag do %> |
||||
<%= include_gon(need_tag: false) -%> |
||||
<% end %> |
||||
|
||||
<%= toolbar title: l(:label_my_page) do %> |
||||
<%= styled_form_tag({ action: "add_block" }, class: 'my-page--block-form') do %> |
||||
<% options = "<option disabled selected>--#{t(:button_add)}--</option>" |
||||
.html_safe |
||||
.concat(options_for_select(@block_options)) %> |
||||
|
||||
<li class="toolbar-item"> |
||||
<%= styled_select_tag 'block', options, id: 'block-options',class: '-small' %> |
||||
</li> |
||||
|
||||
<li class="toolbar-item"> |
||||
<%= submit_tag l(:button_add), class: 'button' %> |
||||
</li> |
||||
|
||||
<li class="toolbar-item"> |
||||
<%= link_to({action: 'page'}, class: 'button') do %> |
||||
<%= op_icon('button--icon icon-cancel') %> |
||||
<span class="button--text"><%= l(:button_save_back) %></span> |
||||
<% end %> |
||||
</li> |
||||
<% end %> |
||||
<% end %> |
||||
|
||||
<h4><%=l(:label_visible_elements) %></h4> |
||||
<div id='visible-grid' class="my-page--container"> |
||||
<div id="top" class="dragula-container grid-content block-receiver"> |
||||
<%= render partial: 'block_container', locals: { edit: true, blocks: @blocks['top'] } %> |
||||
</div> |
||||
<div class="grid-block widget-boxes"> |
||||
<% %w(left right).each do |position| %> |
||||
<div id="<%= position %>" class="dragula-container grid-content block-receiver"> |
||||
<%= render partial: 'block_container', locals: { edit: true, blocks: @blocks[position] } %> |
||||
</div> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
|
||||
<% html_title(l(:label_my_page)) -%> |
@ -1,46 +0,0 @@ |
||||
<%#-- 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. |
||||
|
||||
++#%> |
||||
|
||||
<% |
||||
query_properties = { |
||||
'columns[]': %w(id project type subject), |
||||
filters: |
||||
JSON.dump([{ user_filter => { operator: '=', values: ['me'] } }, { status: { operator: 'o', values: []}}]).to_s |
||||
} |
||||
|
||||
configuration = { |
||||
actionsColumnEnabled: false, |
||||
columnMenuEnabled: false, |
||||
contextMenuEnabled: false |
||||
} |
||||
%> |
||||
|
||||
<wp-embedded-table-entry query-props="<%= query_properties.to_json %>" |
||||
configuration="<%= configuration.to_json %>"> |
||||
</wp-embedded-table-entry> |
@ -0,0 +1,32 @@ |
||||
class CreateGrid < ActiveRecord::Migration[5.1] |
||||
def change |
||||
create_grids |
||||
create_grid_widgets |
||||
end |
||||
|
||||
private |
||||
|
||||
def create_grids |
||||
create_table :grids do |t| |
||||
t.integer :row_count, null: false |
||||
t.integer :column_count, null: false |
||||
t.string :type |
||||
|
||||
t.references :user |
||||
|
||||
t.timestamps |
||||
end |
||||
end |
||||
|
||||
def create_grid_widgets |
||||
create_table :grid_widgets do |t| |
||||
t.integer :start_row, null: false |
||||
t.integer :end_row, null: false |
||||
t.integer :start_column, null: false |
||||
t.integer :end_column, null: false |
||||
t.string :identifier |
||||
t.text :options |
||||
t.references :grid |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,3 @@ |
||||
<h2 [textContent]="text.title"></h2> |
||||
|
||||
<grid *ngIf="grid" [grid]="grid"></grid> |
@ -0,0 +1,74 @@ |
||||
import {Component, OnInit} from "@angular/core"; |
||||
import {GridDmService} from "core-app/modules/hal/dm-services/grid-dm.service"; |
||||
import {GridResource} from "core-app/modules/hal/resources/grid-resource"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {HalResourceService} from "core-app/modules/hal/services/hal-resource.service"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
|
||||
@Component({ |
||||
templateUrl: './my-page.component.html' |
||||
}) |
||||
export class MyPageComponent implements OnInit { |
||||
public text = { title: this.i18n.t('js.label_my_page') }; |
||||
|
||||
constructor(readonly gridDm:GridDmService, |
||||
readonly pathHelper:PathHelperService, |
||||
readonly halResourceService:HalResourceService, |
||||
readonly i18n:I18nService) {} |
||||
|
||||
public grid:GridResource; |
||||
|
||||
ngOnInit() { |
||||
this |
||||
.loadMyPage() |
||||
.then((grid) => { |
||||
this.grid = grid; |
||||
}); |
||||
} |
||||
|
||||
// If a page with the current page exists (scoped to the current user by the backend)
|
||||
// that page will be used to initialized the grid.
|
||||
// If it does not exist, fetch the form and then create a new resource.
|
||||
// The created resource is then used to initialize the grid.
|
||||
private loadMyPage():Promise<GridResource> { |
||||
return this |
||||
.gridDm |
||||
.list({ filters: [['page', '=', [this.pathHelper.myPagePath()]]] }) |
||||
.then(collection => { |
||||
if (collection.total === 0) { |
||||
return this.myPageForm(); |
||||
} else { |
||||
return (collection.elements[0] as GridResource); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private myPageForm():Promise<GridResource> { |
||||
return new Promise<GridResource>((resolve, reject) => { |
||||
let payload = { |
||||
'_links': { |
||||
'page': { |
||||
'href': this.pathHelper.myPagePath() |
||||
} |
||||
} |
||||
}; |
||||
|
||||
this |
||||
.gridDm |
||||
.createForm(payload) |
||||
.then((form) => { |
||||
let source = form.payload.$source; |
||||
|
||||
let resource = this.halResourceService.createHalResource(source) as GridResource; |
||||
|
||||
this.gridDm.create(resource, form.schema) |
||||
.then((resource) => { |
||||
resolve(resource); |
||||
}) |
||||
.catch(() => { |
||||
reject(); |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
<i class="icon-info1" aria-hidden="true"></i> |
||||
<span class="generic-table--no-results-title" |
||||
[textContent]="title"> |
||||
</span> |
@ -0,0 +1,41 @@ |
||||
//-- 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, Input, HostBinding} from '@angular/core'; |
||||
|
||||
@Component({ |
||||
templateUrl: './no-results.component.html', |
||||
selector: 'no-results' |
||||
}) |
||||
|
||||
export class NoResultsComponent { |
||||
@Input() |
||||
title:string; |
||||
|
||||
@HostBinding('class.generic-table--no-results-container') setHostClass = true; |
||||
} |
@ -0,0 +1,40 @@ |
||||
// -- 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 { |
||||
SimpleResource |
||||
} from 'core-app/modules/common/path-helper/apiv3/path-resources'; |
||||
|
||||
export class Apiv3GridPaths extends SimpleResource { |
||||
constructor(basePath:string, gridId:string|number) { |
||||
super(basePath, gridId); |
||||
} |
||||
|
||||
// Static paths
|
||||
readonly form = new SimpleResource(this.path, 'form'); |
||||
} |
@ -0,0 +1,47 @@ |
||||
// -- 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 { |
||||
SimpleResource, |
||||
SimpleResourceCollection |
||||
} from 'core-app/modules/common/path-helper/apiv3/path-resources'; |
||||
import {Apiv3GridPaths} from "core-app/modules/common/path-helper/apiv3/grids/apiv3-grid-paths"; |
||||
|
||||
export class Apiv3GridsPaths extends SimpleResourceCollection { |
||||
constructor(basePath:string) { |
||||
super(basePath, 'grids'); |
||||
} |
||||
|
||||
public id(gridId:string|number) { |
||||
return new Apiv3GridPaths(this.path, gridId); |
||||
} |
||||
|
||||
public form() { |
||||
return new SimpleResource(this.path, 'form'); |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
// -- 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 { |
||||
SimpleResource |
||||
} from 'core-app/modules/common/path-helper/apiv3/path-resources'; |
||||
|
||||
export class Apiv3NewsPaths extends SimpleResource { |
||||
constructor(basePath:string, newsId:string|number) { |
||||
super(basePath, newsId); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
// -- 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 { |
||||
SimpleResourceCollection |
||||
} from 'core-app/modules/common/path-helper/apiv3/path-resources'; |
||||
import {Apiv3NewsPaths} from "core-app/modules/common/path-helper/apiv3/news/apiv3-news-paths"; |
||||
|
||||
export class Apiv3NewsesPaths extends SimpleResourceCollection { |
||||
constructor(basePath:string) { |
||||
super(basePath, 'news'); |
||||
} |
||||
|
||||
public id(gridId:string|number) { |
||||
return new Apiv3NewsPaths(this.path, gridId); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
// -- 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 { |
||||
SimpleResourceCollection |
||||
} from 'core-app/modules/common/path-helper/apiv3/path-resources'; |
||||
import {Apiv3NewsPaths} from "core-app/modules/common/path-helper/apiv3/news/apiv3-news-paths"; |
||||
|
||||
export class Apiv3TimeEntriesPaths extends SimpleResourceCollection { |
||||
constructor(basePath:string) { |
||||
super(basePath, 'time_entries'); |
||||
} |
||||
|
||||
public id(gridId:string|number) { |
||||
return new Apiv3NewsPaths(this.path, gridId); |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
// -- 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 { |
||||
SimpleResource |
||||
} from 'core-app/modules/common/path-helper/apiv3/path-resources'; |
||||
|
||||
export class Apiv3TimeEntryPaths extends SimpleResource { |
||||
constructor(basePath:string, newsId:string|number) { |
||||
super(basePath, newsId); |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
export class GridArea { |
||||
private storedGuid:string; |
||||
public startRow:number; |
||||
public endRow:number; |
||||
public startColumn:number; |
||||
public endColumn:number; |
||||
|
||||
constructor(startRow:number, endRow:number, startColumn:number, endColumn:number) { |
||||
this.startRow = startRow; |
||||
this.endRow = endRow; |
||||
this.startColumn = startColumn; |
||||
this.endColumn = endColumn; |
||||
} |
||||
|
||||
public get guid():string { |
||||
if (!this.storedGuid) { |
||||
this.storedGuid = this.newGuid(); |
||||
} |
||||
|
||||
return this.storedGuid; |
||||
} |
||||
|
||||
private newGuid() { |
||||
function s4() { |
||||
return Math.floor((1 + Math.random()) * 0x10000) |
||||
.toString(16) |
||||
.substring(1); |
||||
} |
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,65 @@ |
||||
import {GridWidgetResource} from "app/modules/hal/resources/grid-widget-resource"; |
||||
import {GridArea} from "app/modules/grids/areas/grid-area"; |
||||
|
||||
export class GridWidgetArea extends GridArea { |
||||
public widget:GridWidgetResource; |
||||
|
||||
constructor(widget:GridWidgetResource) { |
||||
super(widget.startRow, |
||||
widget.endRow, |
||||
widget.startColumn, |
||||
widget.endColumn); |
||||
|
||||
this.widget = widget; |
||||
} |
||||
|
||||
public moveRight() { |
||||
this.startColumn++; |
||||
this.endColumn++; |
||||
} |
||||
|
||||
public moveLeft() { |
||||
this.startColumn--; |
||||
this.endColumn--; |
||||
} |
||||
|
||||
public growColumn() { |
||||
this.endColumn++; |
||||
} |
||||
|
||||
public overlaps(otherArea:GridWidgetArea) { |
||||
return this.rowOverlaps(otherArea) && |
||||
this.columnOverlaps(otherArea); |
||||
} |
||||
|
||||
public rowOverlaps(otherArea:GridWidgetArea) { |
||||
return this.startRow < otherArea.endRow && |
||||
this.endRow >= otherArea.endRow || |
||||
this.startRow <= otherArea.startRow && |
||||
this.endRow > otherArea.startRow || |
||||
this.startRow > otherArea.startRow && |
||||
this.endRow < otherArea.endRow; |
||||
} |
||||
|
||||
public columnOverlaps(otherArea:GridWidgetArea) { |
||||
return this.startColumn < otherArea.endColumn && |
||||
this.endColumn >= otherArea.endColumn || |
||||
this.startColumn <= otherArea.startColumn && |
||||
this.endColumn > otherArea.startColumn || |
||||
this.startColumn > otherArea.startColumn && |
||||
this.endColumn < otherArea.endColumn; |
||||
} |
||||
|
||||
public startColumnOverlaps(otherArea:GridWidgetArea) { |
||||
return this.startColumn < otherArea.startColumn && |
||||
this.endColumn > otherArea.startColumn && |
||||
this.rowOverlaps(otherArea); |
||||
} |
||||
|
||||
public writeAreaChangeToWidget() { |
||||
this.widget.startRow = this.startRow; |
||||
this.widget.endRow = this.endRow; |
||||
this.widget.startColumn = this.startColumn; |
||||
this.widget.endColumn = this.endColumn; |
||||
} |
||||
} |
@ -0,0 +1,107 @@ |
||||
//-- 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 {Directive, ElementRef, Input} from '@angular/core'; |
||||
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; |
||||
|
||||
import {OpContextMenuTrigger} from 'core-components/op-context-menu/handlers/op-context-menu-trigger.directive'; |
||||
import {OPContextMenuService} from 'core-components/op-context-menu/op-context-menu.service'; |
||||
import {GridComponent} from "core-app/modules/grids/grid/grid.component"; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
|
||||
@Directive({ |
||||
selector: '[gridColumnContextMenu]' |
||||
}) |
||||
export class GridColumnContextMenu extends OpContextMenuTrigger { |
||||
@Input('gridColumnContextMenu-columnNumber') public columnNumber:number; |
||||
|
||||
constructor(readonly elementRef:ElementRef, |
||||
readonly opContextMenu:OPContextMenuService, |
||||
readonly I18n:I18nService, |
||||
readonly layout:GridAreaService) { |
||||
|
||||
super(elementRef, opContextMenu); |
||||
} |
||||
|
||||
protected open(evt:JQueryEventObject) { |
||||
this.buildItems(); |
||||
this.opContextMenu.show(this, evt); |
||||
} |
||||
|
||||
public get locals() { |
||||
return { |
||||
items: this.items |
||||
}; |
||||
} |
||||
|
||||
public positionArgs(openerEvent:JQueryEventObject):any { |
||||
return { |
||||
my: 'right top', |
||||
at: 'right bottom', |
||||
of: this.$element, |
||||
collision: 'flipfit' |
||||
}; |
||||
} |
||||
|
||||
private buildItems() { |
||||
let grid = this.layout; |
||||
let columnNumber = this.columnNumber; |
||||
|
||||
let items = [ |
||||
{ |
||||
linkText: I18n.t('js.label_add_column_before'), |
||||
onClick: () => { |
||||
grid.addColumn(columnNumber - 1); |
||||
return true; |
||||
} |
||||
}, |
||||
{ |
||||
linkText: I18n.t('js.label_add_column_after'), |
||||
onClick: () => { |
||||
grid.addColumn(columnNumber); |
||||
return true; |
||||
} |
||||
} |
||||
]; |
||||
|
||||
if (grid.numColumns > 1) { |
||||
items.push( |
||||
{ |
||||
linkText: I18n.t('js.label_remove_column'), |
||||
onClick: () => { |
||||
grid.removeColumn(columnNumber); |
||||
return true; |
||||
} |
||||
} |
||||
); |
||||
} |
||||
|
||||
this.items = items; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,106 @@ |
||||
//-- 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 {Directive, ElementRef, Input} from '@angular/core'; |
||||
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; |
||||
|
||||
import {OpContextMenuTrigger} from 'core-components/op-context-menu/handlers/op-context-menu-trigger.directive'; |
||||
import {OPContextMenuService} from 'core-components/op-context-menu/op-context-menu.service'; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
|
||||
@Directive({ |
||||
selector: '[gridRowContextMenu]' |
||||
}) |
||||
export class GridRowContextMenu extends OpContextMenuTrigger { |
||||
@Input('gridRowContextMenu-rowNumber') public rowNumber:number; |
||||
|
||||
constructor(readonly elementRef:ElementRef, |
||||
readonly opContextMenu:OPContextMenuService, |
||||
readonly I18n:I18nService, |
||||
readonly layout:GridAreaService) { |
||||
|
||||
super(elementRef, opContextMenu); |
||||
} |
||||
|
||||
protected open(evt:JQueryEventObject) { |
||||
this.buildItems(); |
||||
this.opContextMenu.show(this, evt); |
||||
} |
||||
|
||||
public get locals() { |
||||
return { |
||||
items: this.items |
||||
}; |
||||
} |
||||
|
||||
public positionArgs(openerEvent:JQueryEventObject):any { |
||||
return { |
||||
my: 'left top', |
||||
at: 'right top', |
||||
of: this.$element, |
||||
collision: 'flipfit' |
||||
}; |
||||
} |
||||
|
||||
private buildItems() { |
||||
let grid = this.layout; |
||||
let rowNumber = this.rowNumber; |
||||
|
||||
let items = [ |
||||
{ |
||||
linkText: I18n.t('js.label_add_row_before'), |
||||
onClick: () => { |
||||
grid.addRow(rowNumber - 1); |
||||
return true; |
||||
} |
||||
}, |
||||
{ |
||||
linkText: I18n.t('js.label_add_row_after'), |
||||
onClick: () => { |
||||
grid.addRow(rowNumber); |
||||
return true; |
||||
} |
||||
} |
||||
]; |
||||
|
||||
if (grid.numRows > 1) { |
||||
items.push( |
||||
{ |
||||
linkText: I18n.t('js.label_remove_row'), |
||||
onClick: () => { |
||||
grid.removeRow(rowNumber); |
||||
return true; |
||||
} |
||||
} |
||||
); |
||||
} |
||||
|
||||
this.items = items; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,104 @@ |
||||
import {Injectable, Injector} from "@angular/core"; |
||||
import {OpModalService} from "app/components/op-modals/op-modal.service"; |
||||
import {AddGridWidgetModal} from "app/modules/grids/widgets/add/add.modal"; |
||||
import {GridWidgetResource} from "app/modules/hal/resources/grid-widget-resource"; |
||||
import {GridArea} from "app/modules/grids/areas/grid-area"; |
||||
import {HalResourceService} from "app/modules/hal/services/hal-resource.service"; |
||||
import {GridWidgetArea} from "core-app/modules/grids/areas/grid-widget-area"; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
import {GridDragAndDropService} from "core-app/modules/grids/grid/drag-and-drop.service"; |
||||
import {GridResizeService} from "core-app/modules/grids/grid/resize.service"; |
||||
|
||||
@Injectable() |
||||
export class GridAddWidgetService { |
||||
|
||||
constructor(readonly opModalService:OpModalService, |
||||
readonly injector:Injector, |
||||
readonly halResource:HalResourceService, |
||||
readonly layout:GridAreaService, |
||||
readonly drag:GridDragAndDropService, |
||||
readonly resize:GridResizeService) { |
||||
} |
||||
|
||||
public isAddable(area:GridArea) { |
||||
return !this.drag.currentlyDragging && |
||||
!this.resize.currentlyResizing && |
||||
this.layout.mousedOverArea === area && |
||||
this.layout.widgetAreaIds.includes(area.guid); |
||||
} |
||||
|
||||
public widget(area:GridArea) { |
||||
this |
||||
.select(area) |
||||
.then((widgetResource) => { |
||||
// try to set it to a 2 x 3 layout
|
||||
// but shrink if that is outside the grid or
|
||||
// overlaps any other widget
|
||||
let newArea = new GridWidgetArea(widgetResource); |
||||
|
||||
newArea.endColumn = newArea.endColumn + 1; |
||||
newArea.endRow = newArea.endRow + 2; |
||||
|
||||
let maxRow:number = this.layout.numRows + 1; |
||||
let maxColumn:number = this.layout.numColumns + 1; |
||||
|
||||
this.layout.widgetAreas.forEach((existingArea) => { |
||||
if (newArea.startColumnOverlaps(existingArea) && |
||||
maxColumn > existingArea.startColumn) { |
||||
maxColumn = existingArea.startColumn; |
||||
} |
||||
}); |
||||
|
||||
if (maxColumn < newArea.endColumn) { |
||||
newArea.endColumn = maxColumn; |
||||
} |
||||
|
||||
this.layout.widgetAreas.forEach((existingArea) => { |
||||
if (newArea.overlaps(existingArea) && |
||||
maxRow > existingArea.startRow) { |
||||
maxRow = existingArea.startRow; |
||||
} |
||||
}); |
||||
|
||||
if (maxRow < newArea.endRow) { |
||||
newArea.endRow = maxRow; |
||||
} |
||||
|
||||
newArea.writeAreaChangeToWidget(); |
||||
|
||||
this.layout.widgetResources.push(newArea.widget); |
||||
|
||||
this.layout.buildAreas(); |
||||
}) |
||||
.catch(() => { |
||||
// user didn't select a widget
|
||||
}); |
||||
} |
||||
|
||||
private select(area:GridArea) { |
||||
return new Promise<GridWidgetResource>((resolve, reject) => { |
||||
const modal = this.opModalService.show(AddGridWidgetModal, { }); |
||||
modal.closingEvent.subscribe((modal:AddGridWidgetModal) => { |
||||
let registered = modal.chosenWidget; |
||||
|
||||
if (!registered) { |
||||
reject(); |
||||
return; |
||||
} |
||||
|
||||
let source = { |
||||
_type: 'GridWidget', |
||||
identifier: registered.identifier, |
||||
startRow: area.startRow, |
||||
endRow: area.endRow, |
||||
startColumn: area.startColumn, |
||||
endColumn: area.endColumn |
||||
}; |
||||
|
||||
let resource = this.halResource.createHalResource(source) as GridWidgetResource; |
||||
|
||||
resolve(resource); |
||||
}); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,220 @@ |
||||
import {Injectable} from '@angular/core'; |
||||
import {GridWidgetArea} from "app/modules/grids/areas/grid-widget-area"; |
||||
import {GridArea} from "core-app/modules/grids/areas/grid-area"; |
||||
import {GridDmService} from "core-app/modules/hal/dm-services/grid-dm.service"; |
||||
import {GridResource} from "core-app/modules/hal/resources/grid-resource"; |
||||
import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource"; |
||||
import {SchemaResource} from "core-app/modules/hal/resources/schema-resource"; |
||||
|
||||
|
||||
@Injectable() |
||||
export class GridAreaService { |
||||
|
||||
private resource:GridResource; |
||||
private schema:SchemaResource; |
||||
|
||||
public numColumns:number = 0; |
||||
public numRows:number = 0; |
||||
public gridAreas:GridArea[]; |
||||
public widgetAreas:GridWidgetArea[]; |
||||
public widgetAreaIds:string[]; |
||||
public widgetResources:GridWidgetResource[] = []; |
||||
public mousedOverArea:GridArea|null; |
||||
|
||||
constructor (private gridDm:GridDmService) { |
||||
} |
||||
|
||||
public set gridResource(value:GridResource) { |
||||
this.resource = value; |
||||
|
||||
this.fetchSchema(); |
||||
|
||||
this.numRows = this.resource.rowCount; |
||||
this.numColumns = this.resource.columnCount; |
||||
|
||||
this.widgetResources = this.resource.widgets; |
||||
|
||||
this.buildAreas(false); |
||||
} |
||||
|
||||
public setMousedOverArea(area:GridArea) { |
||||
this.mousedOverArea = area; |
||||
} |
||||
|
||||
public buildAreas(save = true) { |
||||
this.gridAreas = this.buildGridAreas(); |
||||
this.widgetAreaIds = this.buildWidgetAreaIds(); |
||||
this.widgetAreas = this.buildGridWidgetAreas(); |
||||
|
||||
this.resource.widgets = this.widgetResources; |
||||
this.resource.rowCount = this.numRows; |
||||
this.resource.columnCount = this.numColumns; |
||||
|
||||
if (save) { |
||||
this.gridDm.update(this.resource, this.schema); |
||||
} |
||||
} |
||||
|
||||
private buildGridAreas() { |
||||
let cells:GridArea[] = []; |
||||
|
||||
for (let row = 1; row <= this.numRows; row++) { |
||||
cells.push(...this.buildGridAreasRow(row)); |
||||
} |
||||
|
||||
return cells; |
||||
} |
||||
|
||||
private buildGridAreasColumn(column:number) { |
||||
let cells:GridArea[] = []; |
||||
|
||||
for (let row = 1; row <= this.numRows; row++) { |
||||
let cell = new GridArea(row, |
||||
row + 1, |
||||
column, |
||||
column + 1); |
||||
|
||||
cells.push(cell); |
||||
} |
||||
|
||||
return cells; |
||||
} |
||||
|
||||
private buildGridAreasRow(row:number) { |
||||
let cells:GridArea[] = []; |
||||
|
||||
for (let column = 1; column <= this.numColumns; column++) { |
||||
let cell = new GridArea(row, |
||||
row + 1, |
||||
column, |
||||
column + 1); |
||||
|
||||
cells.push(cell); |
||||
} |
||||
|
||||
return cells; |
||||
} |
||||
|
||||
private buildGridWidgetAreas() { |
||||
return this.widgetResources.map((widget) => { |
||||
return new GridWidgetArea(widget); |
||||
}); |
||||
} |
||||
|
||||
// persist all changes to the areas caused by dragging/resizing
|
||||
// to the widget
|
||||
public writeAreaChangesToWidgets() { |
||||
this.widgetAreas.forEach((area) => { |
||||
area.writeAreaChangeToWidget(); |
||||
}); |
||||
} |
||||
|
||||
public addColumn(column:number) { |
||||
this.numColumns++; |
||||
|
||||
this.widgetResources.filter((widget) => { |
||||
return widget.startColumn > column; |
||||
}).forEach((widget) => { |
||||
widget.startColumn++; |
||||
widget.endColumn++; |
||||
}); |
||||
|
||||
this.buildAreas(); |
||||
} |
||||
|
||||
public addRow(row:number) { |
||||
this.numRows++; |
||||
|
||||
this.widgetResources.filter((widget) => { |
||||
return widget.startRow > row; |
||||
}).forEach((widget) => { |
||||
widget.startRow++; |
||||
widget.endRow++; |
||||
}); |
||||
|
||||
this.buildAreas(); |
||||
} |
||||
|
||||
public removeColumn(column:number) { |
||||
this.numColumns--; |
||||
|
||||
// remove widgets that only span the removed column
|
||||
this.widgetResources = this.widgetResources.filter((widget) => { |
||||
return !(widget.startColumn === column && widget.endColumn === column + 1); |
||||
}); |
||||
|
||||
//shrink widgets that span more than the removed column
|
||||
this.widgetResources.forEach((widget) => { |
||||
if (widget.startColumn <= column && widget.endColumn >= column + 1) { |
||||
//shrink widgets that span more than the removed column
|
||||
widget.endColumn--; |
||||
} |
||||
}); |
||||
|
||||
// move all widgets that are after the removed column
|
||||
// so that they appear to keep their place.
|
||||
this.widgetResources.filter((widget) => { |
||||
return widget.startColumn > column; |
||||
}).forEach((widget) => { |
||||
widget.startColumn--; |
||||
widget.endColumn--; |
||||
}); |
||||
|
||||
this.buildAreas(); |
||||
} |
||||
|
||||
public removeRow(row:number) { |
||||
this.numRows--; |
||||
|
||||
// remove widgets that only span the removed row
|
||||
this.widgetResources = this.widgetResources.filter((widget) => { |
||||
return !(widget.startRow === row && widget.endRow === row + 1); |
||||
}); |
||||
|
||||
//shrink widgets that span more than the removed row
|
||||
this.widgetResources.forEach((widget) => { |
||||
if (widget.startRow <= row && widget.endRow >= row + 1) { |
||||
//shrink widgets that span more than the removed row
|
||||
widget.endRow--; |
||||
} |
||||
}); |
||||
|
||||
// move all widgets that are after the removed row
|
||||
// so that they appear to keep their place.
|
||||
this.widgetResources.filter((widget) => { |
||||
return widget.startRow > row; |
||||
}).forEach((widget) => { |
||||
widget.startRow--; |
||||
widget.endRow--; |
||||
}); |
||||
|
||||
this.buildAreas(); |
||||
} |
||||
|
||||
public resetAreas(ignoredArea:GridWidgetArea|null = null) { |
||||
this.widgetAreas.filter((area) => { |
||||
return !ignoredArea || area.guid !== ignoredArea.guid; |
||||
}).forEach((area) => { |
||||
area.startRow = area.widget.startRow; |
||||
area.endRow = area.widget.endRow; |
||||
area.startColumn = area.widget.startColumn; |
||||
area.endColumn = area.widget.endColumn; |
||||
}); |
||||
|
||||
this.numRows = this.resource.rowCount; |
||||
this.numColumns = this.resource.columnCount; |
||||
} |
||||
|
||||
private buildWidgetAreaIds() { |
||||
return this.gridAreas.map((area) => { |
||||
return area.guid; |
||||
}); |
||||
} |
||||
|
||||
private fetchSchema() { |
||||
this.gridDm.updateForm(this.resource) |
||||
.then((form) => { |
||||
this.schema = form.schema; |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,113 @@ |
||||
import {Injectable} from '@angular/core'; |
||||
import {GridWidgetArea} from "core-app/modules/grids/areas/grid-widget-area"; |
||||
import {CdkDragEnd, CdkDragEnter, CdkDragExit, CdkDragDrop} from '@angular/cdk/drag-drop'; |
||||
import {GridArea} from "core-app/modules/grids/areas/grid-area"; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
import {GridMoveService} from "core-app/modules/grids/grid/move.service"; |
||||
|
||||
@Injectable() |
||||
export class GridDragAndDropService { |
||||
public draggedArea:GridWidgetArea|null; |
||||
public placeholderArea:GridWidgetArea|null; |
||||
|
||||
constructor(readonly layout:GridAreaService, |
||||
readonly move:GridMoveService) { |
||||
|
||||
} |
||||
|
||||
public entered(event:CdkDragEnter<GridArea>) { |
||||
if (this.draggedArea) { |
||||
let dropArea = event.container.data; |
||||
this.layout.resetAreas(this.draggedArea); |
||||
this.moveAreasOnDragging(dropArea); |
||||
} |
||||
} |
||||
|
||||
public exited(event:CdkDragExit<GridArea>) { |
||||
// prevent flickering when dragging within the area spanned
|
||||
// by the dragged element. Otherwise, cdk drag fires an entered event on every
|
||||
// moved pixel.
|
||||
if (this.draggedArea) { |
||||
this.draggedArea.endRow = this.draggedArea.startRow + 1; |
||||
this.draggedArea.endColumn = this.draggedArea.startColumn + 1; |
||||
} |
||||
} |
||||
|
||||
private moveAreasOnDragging(dropArea:GridArea) { |
||||
if (!this.placeholderArea) { |
||||
return; |
||||
} |
||||
let widgetArea = this.draggedArea!; |
||||
|
||||
// we cannot use the widget's original area as moving it while dragging confuses cdkDrag
|
||||
this.placeholderArea.startRow = dropArea.startRow; |
||||
if (this.placeholderArea.startRow + this.placeholderArea.widget.height > this.layout.numRows + 1) { |
||||
this.placeholderArea.endRow = this.layout.numRows + 1; |
||||
} else { |
||||
this.placeholderArea.endRow = dropArea.startRow + this.placeholderArea.widget.height; |
||||
} |
||||
|
||||
this.placeholderArea.startColumn = dropArea.startColumn; |
||||
if (this.placeholderArea.startColumn + this.placeholderArea.widget.width > this.layout.numColumns + 1) { |
||||
this.placeholderArea.endColumn = this.layout.numColumns + 1; |
||||
} else { |
||||
this.placeholderArea.endColumn = dropArea.startColumn + this.placeholderArea.widget.width; |
||||
} |
||||
|
||||
this.move.down(this.placeholderArea, widgetArea); |
||||
} |
||||
|
||||
public get currentlyDragging() { |
||||
return !!this.draggedArea; |
||||
} |
||||
|
||||
public start(area:GridWidgetArea) { |
||||
this.draggedArea = area; |
||||
this.placeholderArea = new GridWidgetArea(area.widget); |
||||
} |
||||
|
||||
public stop(area:GridWidgetArea, event:CdkDragEnd) { |
||||
if (!this.draggedArea) { |
||||
return; |
||||
} |
||||
|
||||
let dropArea = event.source.dropContainer.data; |
||||
|
||||
// Handle special case of user starting to move the widget but then deciding to
|
||||
// move it back to the original area.
|
||||
if (this.draggedArea.startColumn === dropArea.startColumn && |
||||
this.draggedArea.startRow === dropArea.startRow) { |
||||
this.layout.resetAreas(); |
||||
} |
||||
this.draggedArea = null; |
||||
this.placeholderArea = null; |
||||
} |
||||
|
||||
public drop(event:CdkDragDrop<GridArea>) { |
||||
// this.draggedArea is already reset to null at this point
|
||||
let dropArea = event.container.data; |
||||
let draggedArea = event.previousContainer.data as GridWidgetArea; |
||||
|
||||
// Set the draggedArea's startRow/startColumn properties
|
||||
// to the drop zone ones.
|
||||
// The dragged Area should keep it's height and width normally but will
|
||||
// shrink if the area would otherwise end outside the grid.
|
||||
draggedArea.startRow = dropArea.startRow; |
||||
if (dropArea.startRow + draggedArea.widget.height > this.layout.numRows + 1) { |
||||
draggedArea.endRow = this.layout.numRows + 1; |
||||
} else { |
||||
draggedArea.endRow = dropArea.startRow + draggedArea.widget.height; |
||||
} |
||||
|
||||
draggedArea.startColumn = dropArea.startColumn; |
||||
if (dropArea.startColumn + draggedArea.widget.width > this.layout.numColumns + 1) { |
||||
draggedArea.endColumn = this.layout.numColumns + 1; |
||||
} else { |
||||
draggedArea.endColumn = dropArea.startColumn + draggedArea.widget.width; |
||||
} |
||||
|
||||
this.layout.writeAreaChangesToWidgets(); |
||||
this.layout.buildAreas(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,110 @@ |
||||
|
||||
<!-- Column headers --> |
||||
<div class="grid--column-headers" |
||||
[style.grid-template-columns]="gridColumnStyle"> |
||||
<div *ngFor="let column of columnNumbers" |
||||
[style.grid-row-start]="1" |
||||
[style.grid-row-end]="1" |
||||
[style.grid-column-start]="column" |
||||
[style.grid-column-end]="column + 1" |
||||
class="grid--header" |
||||
gridColumnContextMenu |
||||
[gridColumnContextMenu-columnNumber]="column"> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Row headers --> |
||||
<div class="grid--row-headers" |
||||
[style.grid-template-rows]="gridRowStyle"> |
||||
<div *ngFor="let row of rowNumbers" |
||||
[style.grid-row-start]="row" |
||||
[style.grid-row-end]="row + 1" |
||||
[style.grid-column-start]="1" |
||||
[style.grid-column-end]="2" |
||||
class="grid--header" |
||||
gridRowContextMenu |
||||
[gridRowContextMenu-rowNumber]="row"> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="grid--container" |
||||
[style.grid-template-columns]="gridColumnStyle" |
||||
[style.grid-template-rows]="gridRowStyle"> |
||||
|
||||
<!-- Grid areas that have a widget in them --> |
||||
<div *ngFor="let area of layout.widgetAreas; trackBy: identifyGridArea;" |
||||
class="grid--area -widgeted" |
||||
[style.grid-row-start]="area.startRow" |
||||
[style.grid-row-end]="area.endRow" |
||||
[style.grid-column-start]="area.startColumn" |
||||
[style.grid-column-end]="area.endColumn" |
||||
cdkDropList |
||||
[id]="area.guid" |
||||
[cdkDropListData]="area" |
||||
[cdkDropListConnectedTo]="layout.widgetAreaIds"> |
||||
<div class="grid--area-content widget-box" |
||||
cdkDrag |
||||
(cdkDragStarted)="drag.start(area)" |
||||
(cdkDragEnded)="drag.stop(area, $event)"> |
||||
<div *cdkDragPlaceholder></div> |
||||
<div class="grid--widget-remove" |
||||
(click)="remove.widget(area)"> |
||||
</div> |
||||
<ndc-dynamic [ndcDynamicComponent]="widgetComponent(area.widget)" |
||||
[ndcDynamicOutputs]="{}"> |
||||
</ndc-dynamic> |
||||
</div> |
||||
<resizer *ngIf="!drag.currentlyDragging" |
||||
(end)="resize.end(area, $event)" |
||||
(start)="resize.start(area)" |
||||
(move)="resize.moving($event)"> |
||||
</resizer> |
||||
</div> |
||||
|
||||
<!-- One grid area per cell (row x columns) --> |
||||
<div *ngFor="let area of layout.gridAreas; trackBy: identifyGridArea;" |
||||
class="grid--area" |
||||
[ngClass] = "{'-drop-target': drag.currentlyDragging, |
||||
'-resize-target': resize.isTarget(area), |
||||
'-addable': add.isAddable(area) }" |
||||
[style.grid-row-start]="area.startRow" |
||||
[style.grid-row-end]="area.endRow" |
||||
[style.grid-column-start]="area.startColumn" |
||||
[style.grid-column-end]="area.endColumn" |
||||
cdkDropList |
||||
[id]="area.guid" |
||||
(mouseover)="layout.setMousedOverArea(area)" |
||||
[cdkDropListData]="area" |
||||
(cdkDropListDropped)="drag.drop($event)" |
||||
(cdkDropListEntered)="drag.entered($event)" |
||||
(cdkDropListExited)="drag.exited($event)" |
||||
[cdkDropListConnectedTo]="layout.widgetAreaIds"> |
||||
<div class="grid--widget-add" |
||||
*ngIf="add.isAddable(area)" |
||||
(click)="add.widget(area)"> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Grid area to visualize resizing --> |
||||
<div class="grid--area -resizing" |
||||
*ngIf="resize.placeholderArea" |
||||
[style.grid-row-start]="resize.placeholderArea.startRow" |
||||
[style.grid-row-end]="resize.placeholderArea.endRow" |
||||
[style.grid-column-start]="resize.placeholderArea.startColumn" |
||||
[style.grid-column-end]="resize.placeholderArea.endColumn"> |
||||
</div> |
||||
|
||||
<!-- Grid area used as a placeholder while dragging --> |
||||
<div *ngIf="drag.placeholderArea" |
||||
class="grid--area -widgeted" |
||||
[style.grid-row-start]="drag.placeholderArea.startRow" |
||||
[style.grid-row-end]="drag.placeholderArea.endRow" |
||||
[style.grid-column-start]="drag.placeholderArea.startColumn" |
||||
[style.grid-column-end]="drag.placeholderArea.endColumn"> |
||||
<div class="widget-box"> |
||||
<ndc-dynamic [ndcDynamicComponent]="widgetComponent(drag.placeholderArea.widget)" |
||||
[ndcDynamicOutputs]="{}"> |
||||
</ndc-dynamic> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,97 @@ |
||||
import {Component, |
||||
ComponentRef, |
||||
OnDestroy, |
||||
OnInit, |
||||
Input} from "@angular/core"; |
||||
import {GridResource} from "app/modules/hal/resources/grid-resource"; |
||||
import {GridWidgetResource} from "app/modules/hal/resources/grid-widget-resource"; |
||||
import {debugLog} from "app/helpers/debug_output"; |
||||
import {DomSanitizer} from "@angular/platform-browser"; |
||||
import {GridWidgetsService} from "app/modules/grids/widgets/widgets.service"; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
import {GridArea} from "app/modules/grids/areas/grid-area"; |
||||
import {GridWidgetArea} from "app/modules/grids/areas/grid-widget-area"; |
||||
import {GridMoveService} from "app/modules/grids/grid/move.service"; |
||||
import {GridDragAndDropService} from "core-app/modules/grids/grid/drag-and-drop.service"; |
||||
import {GridResizeService} from "core-app/modules/grids/grid/resize.service"; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
import {GridAddWidgetService} from "core-app/modules/grids/grid/add-widget.service"; |
||||
import {GridRemoveWidgetService} from "core-app/modules/grids/grid/remove-widget.service"; |
||||
|
||||
export interface WidgetRegistration { |
||||
identifier:string; |
||||
component:{ new (...args:any[]):AbstractWidgetComponent }; |
||||
} |
||||
|
||||
@Component({ |
||||
templateUrl: './grid.component.html', |
||||
selector: 'grid', |
||||
providers: [ |
||||
GridAreaService, |
||||
GridMoveService, |
||||
GridDragAndDropService, |
||||
GridResizeService, |
||||
GridAddWidgetService, |
||||
GridRemoveWidgetService |
||||
] |
||||
}) |
||||
export class GridComponent implements OnDestroy, OnInit { |
||||
public uiWidgets:ComponentRef<any>[] = []; |
||||
public GRID_AREA_HEIGHT = 100; |
||||
|
||||
@Input() grid:GridResource; |
||||
|
||||
constructor(private sanitization:DomSanitizer, |
||||
private widgetsService:GridWidgetsService, |
||||
public drag:GridDragAndDropService, |
||||
public resize:GridResizeService, |
||||
public layout:GridAreaService, |
||||
public add:GridAddWidgetService, |
||||
public remove:GridRemoveWidgetService) { |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.layout.gridResource = this.grid; |
||||
} |
||||
|
||||
ngOnDestroy() { |
||||
this.uiWidgets.forEach((widget) => widget.destroy()); |
||||
} |
||||
|
||||
public widgetComponent(widget:GridWidgetResource|null) { |
||||
if (!widget) { |
||||
return null; |
||||
} |
||||
|
||||
let registration = this.widgetsService.registered.find((reg) => reg.identifier === widget.identifier); |
||||
|
||||
if (!registration) { |
||||
debugLog(`No widget registered with identifier ${widget.identifier}`); |
||||
|
||||
return null; |
||||
} else { |
||||
return registration.component; |
||||
} |
||||
} |
||||
|
||||
public get gridColumnStyle() { |
||||
return this.sanitization.bypassSecurityTrustStyle(`repeat(${this.layout.numColumns}, 1fr)`); |
||||
} |
||||
|
||||
// array containing Numbers from 1 to this.numColumns
|
||||
public get columnNumbers() { |
||||
return Array.from(Array(this.layout.numColumns + 1).keys()).slice(1); |
||||
} |
||||
|
||||
public get gridRowStyle() { |
||||
return this.sanitization.bypassSecurityTrustStyle(`repeat(${this.layout.numRows}, ${this.GRID_AREA_HEIGHT}px)`); |
||||
} |
||||
|
||||
public get rowNumbers() { |
||||
return Array.from(Array(this.layout.numRows + 1).keys()).slice(1); |
||||
} |
||||
|
||||
public identifyGridArea(index:number, area:GridArea) { |
||||
return area.guid; |
||||
} |
||||
} |
@ -0,0 +1,101 @@ |
||||
import {Injectable} from '@angular/core'; |
||||
import {GridWidgetArea} from "app/modules/grids/areas/grid-widget-area"; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
|
||||
|
||||
@Injectable() |
||||
export class GridMoveService { |
||||
constructor(private layout:GridAreaService) {} |
||||
|
||||
public down(movedArea:GridWidgetArea|null, ignoreArea:GridWidgetArea) { |
||||
let movedAreas:GridWidgetArea[] = []; |
||||
let remainingAreas:GridWidgetArea[] = this.layout.widgetAreas.slice(0); |
||||
|
||||
if (ignoreArea) { |
||||
remainingAreas = remainingAreas.filter((area) => { |
||||
return area.guid !== ignoreArea.guid; |
||||
}); |
||||
} |
||||
|
||||
remainingAreas.sort((a, b) => { |
||||
return b.startRow - a.startRow; |
||||
}); |
||||
|
||||
while (movedArea !== null) { |
||||
movedAreas.push(movedArea!); |
||||
|
||||
remainingAreas = remainingAreas.filter((area) => { |
||||
return area.guid !== movedArea!.guid; |
||||
}); |
||||
|
||||
movedArea = this.moveOneDown(movedAreas, remainingAreas); |
||||
} |
||||
} |
||||
|
||||
private moveOneDown(anchorAreas:GridWidgetArea[], movableAreas:GridWidgetArea[]) { |
||||
let moveSpecification = this.firstAreaToMove(anchorAreas, movableAreas); |
||||
|
||||
if (moveSpecification) { |
||||
let toMoveArea = moveSpecification[0] as GridWidgetArea; |
||||
let anchorArea = moveSpecification[1] as GridWidgetArea; |
||||
|
||||
let areaHeight = toMoveArea.widget.height; |
||||
|
||||
toMoveArea.startRow = anchorArea.endRow; |
||||
toMoveArea.endRow = toMoveArea.startRow + areaHeight; |
||||
|
||||
if (this.layout.numRows < toMoveArea.endRow - 1) { |
||||
this.layout.numRows = toMoveArea.endRow - 1; |
||||
} |
||||
|
||||
return toMoveArea; |
||||
} else { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
// Return first area that needs to move as it overlaps another area.
|
||||
// There are two groups of areas here. The first (anchorAreas) is considered stable
|
||||
// and as such not fit for being moved. This happens e.g. when the user explicitly
|
||||
// moved a widget or if the area has already been moved in a previous run of this method.
|
||||
// The second group (movableAreas) consists of all areas that are movable.
|
||||
// Once an area out of the second group has been identified that overlaps an area of the first
|
||||
// group, the appropriate reference area for later moving is selected out of the group of all
|
||||
// unmovable areas. The reference area is the bottommost area within the unmovable areas which's
|
||||
// column values (start/end) include the to move area's start column value and which's end row is larger
|
||||
// than the area overlapping the area to move. Unmovable areas which's column values do not include the
|
||||
// start column are to the left or right of the area to move and can thus be ignored.
|
||||
private firstAreaToMove(anchorAreas:GridWidgetArea[], movableAreas:GridWidgetArea[]) { |
||||
let overlappingArea:GridWidgetArea|null = null; |
||||
let toMoveArea:GridWidgetArea|null = null; |
||||
|
||||
movableAreas.forEach((movableArea) => { |
||||
anchorAreas.forEach((anchorArea) => { |
||||
if (anchorArea.overlaps(movableArea)) { |
||||
overlappingArea = anchorArea; |
||||
toMoveArea = movableArea; |
||||
return; |
||||
} |
||||
}); |
||||
|
||||
if (toMoveArea) { |
||||
return; |
||||
} |
||||
}); |
||||
|
||||
if (toMoveArea !== null) { |
||||
let referenceArea = overlappingArea!; |
||||
|
||||
anchorAreas.forEach((anchorArea) => { |
||||
if (anchorArea.endRow > referenceArea.endRow && |
||||
toMoveArea!.startColumn >= anchorArea.startColumn && toMoveArea!.startColumn < anchorArea.endColumn) { |
||||
referenceArea = anchorArea; |
||||
} |
||||
}); |
||||
|
||||
return [toMoveArea, referenceArea]; |
||||
} else { |
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
import {Injectable} from "@angular/core"; |
||||
import {GridWidgetArea} from "core-app/modules/grids/areas/grid-widget-area"; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
|
||||
@Injectable() |
||||
export class GridRemoveWidgetService { |
||||
|
||||
constructor(readonly layout:GridAreaService) { |
||||
} |
||||
|
||||
public widget(area:GridWidgetArea) { |
||||
let removedWidget = area.widget; |
||||
|
||||
this.layout.widgetResources = this.layout.widgetResources.filter((widget) => { |
||||
return widget.identifier !== removedWidget.identifier || |
||||
widget.startColumn !== removedWidget.startColumn || |
||||
widget.endColumn !== removedWidget.endColumn || |
||||
widget.startRow !== removedWidget.startRow || |
||||
widget.endRow !== removedWidget.endRow; |
||||
}); |
||||
|
||||
this.layout.buildAreas(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,72 @@ |
||||
import {Injectable} from '@angular/core'; |
||||
import {GridWidgetArea} from "core-app/modules/grids/areas/grid-widget-area"; |
||||
import {GridArea} from "core-app/modules/grids/areas/grid-area"; |
||||
import {ResizeDelta} from "core-app/modules/common/resizer/resizer.component"; |
||||
import {GridAreaService} from "core-app/modules/grids/grid/area.service"; |
||||
import {GridMoveService} from "core-app/modules/grids/grid/move.service"; |
||||
|
||||
@Injectable() |
||||
export class GridResizeService { |
||||
public placeholderArea:GridWidgetArea|null; |
||||
private resizedArea:GridWidgetArea|null; |
||||
private targetIds:string[]; |
||||
|
||||
constructor(readonly layout:GridAreaService, |
||||
readonly move:GridMoveService) { } |
||||
|
||||
public end(area:GridWidgetArea, deltas:ResizeDelta) { |
||||
if (!this.placeholderArea || |
||||
!this.resizedArea) { |
||||
return; |
||||
} |
||||
|
||||
this.resizedArea.endRow = this.placeholderArea.endRow; |
||||
this.resizedArea.endColumn = this.placeholderArea.endColumn; |
||||
|
||||
this.layout.writeAreaChangesToWidgets(); |
||||
this.layout.buildAreas(); |
||||
|
||||
this.resizedArea = null; |
||||
this.placeholderArea = null; |
||||
} |
||||
|
||||
public start(resizedArea:GridWidgetArea) { |
||||
this.placeholderArea = new GridWidgetArea(resizedArea.widget); |
||||
this.resizedArea = resizedArea; |
||||
|
||||
let resizeTargets = this.layout.gridAreas.filter((area) => { |
||||
return area.startRow >= this.placeholderArea!.startRow && |
||||
area.startColumn >= this.placeholderArea!.startColumn; |
||||
}); |
||||
|
||||
this.targetIds = resizeTargets.map((area) => { |
||||
return area.guid; |
||||
}); |
||||
} |
||||
|
||||
public moving(deltas:ResizeDelta) { |
||||
if (!this.placeholderArea || |
||||
!this.resizedArea || |
||||
!this.layout.mousedOverArea || |
||||
!this.targetIds.includes(this.layout.mousedOverArea.guid)) { |
||||
return; |
||||
} |
||||
|
||||
this.layout.resetAreas(); |
||||
|
||||
this.placeholderArea.endRow = this.layout.mousedOverArea.endRow; |
||||
this.placeholderArea.endColumn = this.layout.mousedOverArea.endColumn; |
||||
|
||||
this.move.down(this.placeholderArea, this.resizedArea); |
||||
} |
||||
|
||||
public isTarget(area:GridArea) { |
||||
let areaId = area.guid; |
||||
|
||||
return this.placeholderArea && this.targetIds.includes(areaId); |
||||
} |
||||
|
||||
public get currentlyResizing() { |
||||
return this.placeholderArea; |
||||
} |
||||
} |
@ -0,0 +1,164 @@ |
||||
// -- 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 {NgModule, APP_INITIALIZER, Injector} from '@angular/core'; |
||||
import {DynamicModule} from 'ng-dynamic-component'; |
||||
import {HookService} from "core-app/modules/plugins/hook-service"; |
||||
import {MyPageComponent} from "core-components/routing/my-page/my-page.component"; |
||||
import {OpenprojectCommonModule} from "core-app/modules/common/openproject-common.module"; |
||||
import {BrowserModule} from '@angular/platform-browser'; |
||||
import {FormsModule} from '@angular/forms'; |
||||
import {DragDropModule} from '@angular/cdk/drag-drop'; |
||||
import {OpenprojectWorkPackagesModule} from "core-app/modules/work_packages/openproject-work-packages.module"; |
||||
import {WidgetWpAssignedComponent} from "core-app/modules/grids/widgets/wp-assigned/wp-assigned.component.ts"; |
||||
import {WidgetWpCreatedComponent} from "core-app/modules/grids/widgets/wp-created/wp-created.component.ts"; |
||||
import {WidgetWpWatchedComponent} from "core-app/modules/grids/widgets/wp-watched/wp-watched.component.ts"; |
||||
import {WidgetWpCalendarComponent} from "core-app/modules/grids/widgets/wp-calendar/wp-calendar.component.ts"; |
||||
import {WidgetTimeEntriesCurrentUserComponent} from "core-app/modules/grids/widgets/time-entries-current-user/time-entries-current-user.component"; |
||||
import {GridWidgetsService} from "core-app/modules/grids/widgets/widgets.service"; |
||||
import {GridComponent} from "core-app/modules/grids/grid/grid.component"; |
||||
import {AddGridWidgetModal} from "core-app/modules/grids/widgets/add/add.modal"; |
||||
import {GridColumnContextMenu} from "core-app/modules/grids/context_menus/column.directive"; |
||||
import {GridRowContextMenu} from "core-app/modules/grids/context_menus/row.directive"; |
||||
import {OpenprojectCalendarModule} from "core-app/modules/calendar/openproject-calendar.module"; |
||||
import {Ng2StateDeclaration, UIRouterModule} from '@uirouter/angular'; |
||||
import {WidgetDocumentsComponent} from "core-app/modules/grids/widgets/documents/documents.component"; |
||||
import {WidgetNewsComponent} from "core-app/modules/grids/widgets/news/news.component"; |
||||
import {WidgetWpAccountableComponent} from './widgets/wp-accountable/wp-accountable.component'; |
||||
|
||||
export const GRID_ROUTES:Ng2StateDeclaration[] = [ |
||||
{ |
||||
name: 'my_page', |
||||
url: '/my/page', |
||||
component: MyPageComponent, |
||||
}, |
||||
]; |
||||
|
||||
|
||||
@NgModule({ |
||||
imports: [ |
||||
BrowserModule, |
||||
FormsModule, |
||||
DragDropModule, |
||||
|
||||
OpenprojectCommonModule, |
||||
OpenprojectWorkPackagesModule, |
||||
OpenprojectCalendarModule, |
||||
|
||||
DynamicModule.withComponents([WidgetDocumentsComponent, |
||||
WidgetNewsComponent, |
||||
WidgetWpAssignedComponent, |
||||
WidgetWpAccountableComponent, |
||||
WidgetWpCreatedComponent, |
||||
WidgetWpWatchedComponent, |
||||
WidgetWpCalendarComponent, |
||||
WidgetTimeEntriesCurrentUserComponent]), |
||||
|
||||
// Routes for grid pages
|
||||
UIRouterModule.forChild({ states: GRID_ROUTES }), |
||||
], |
||||
providers: [ |
||||
{ |
||||
provide: APP_INITIALIZER, |
||||
useFactory: registerWidgets, |
||||
deps: [Injector], |
||||
multi: true |
||||
}, |
||||
GridWidgetsService, |
||||
], |
||||
declarations: [ |
||||
GridComponent, |
||||
WidgetDocumentsComponent, |
||||
WidgetNewsComponent, |
||||
WidgetWpAssignedComponent, |
||||
WidgetWpAccountableComponent, |
||||
WidgetWpCreatedComponent, |
||||
WidgetWpWatchedComponent, |
||||
WidgetWpCalendarComponent, |
||||
WidgetTimeEntriesCurrentUserComponent, |
||||
AddGridWidgetModal, |
||||
|
||||
GridColumnContextMenu, |
||||
GridRowContextMenu, |
||||
|
||||
// MyPage
|
||||
MyPageComponent, |
||||
], |
||||
entryComponents: [ |
||||
AddGridWidgetModal, |
||||
|
||||
// MyPage
|
||||
MyPageComponent, |
||||
], |
||||
exports: [ |
||||
] |
||||
}) |
||||
export class OpenprojectGridsModule { |
||||
} |
||||
|
||||
export function registerWidgets(injector:Injector) { |
||||
return () => { |
||||
const hookService = injector.get(HookService); |
||||
hookService.register('gridWidgets', () => { |
||||
return [ |
||||
{ |
||||
identifier: 'work_packages_assigned', |
||||
component: WidgetWpAssignedComponent |
||||
}, |
||||
{ |
||||
identifier: 'work_packages_accountable', |
||||
component: WidgetWpAccountableComponent |
||||
}, |
||||
{ |
||||
identifier: 'work_packages_created', |
||||
component: WidgetWpCreatedComponent |
||||
}, |
||||
{ |
||||
identifier: 'work_packages_watched', |
||||
component: WidgetWpWatchedComponent |
||||
}, |
||||
{ |
||||
identifier: 'work_packages_calendar', |
||||
component: WidgetWpCalendarComponent |
||||
}, |
||||
{ |
||||
identifier: 'time_entries_current_user', |
||||
component: WidgetTimeEntriesCurrentUserComponent |
||||
}, |
||||
{ |
||||
identifier: 'documents', |
||||
component: WidgetDocumentsComponent |
||||
}, |
||||
{ |
||||
identifier: 'news', |
||||
component: WidgetNewsComponent |
||||
} |
||||
]; |
||||
}); |
||||
}; |
||||
} |
@ -0,0 +1,14 @@ |
||||
import {HostBinding, Input} from "@angular/core"; |
||||
import {GridWidgetResource} from "app/modules/hal/resources/grid-widget-resource"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
|
||||
export abstract class AbstractWidgetComponent { |
||||
@HostBinding('style.grid-column-start') gridColumnStart:number; |
||||
@HostBinding('style.grid-column-end') gridColumnEnd:number; |
||||
@HostBinding('style.grid-row-start') gridRowStart:number; |
||||
@HostBinding('style.grid-row-end') gridRowEnd:number; |
||||
|
||||
@Input() resource:GridWidgetResource; |
||||
|
||||
constructor(protected i18n:I18nService) { } |
||||
} |
@ -0,0 +1,25 @@ |
||||
<div class="op-modal--portal ngdialog-theme-openproject grid--add-modal"> |
||||
<div class="op-modal--modal-container confirm-dialog--modal loading-indicator--location" |
||||
data-indicator-name="modal" |
||||
tabindex="0"> |
||||
<div class="op-modal--modal-header"> |
||||
<a class="op-modal--modal-close-button"> |
||||
<i |
||||
class="icon-close" |
||||
(click)="closeMe($event)" |
||||
[attr.title]="text.close_popup"> |
||||
</i> |
||||
</a> |
||||
<h3 class="icon-context icon-attention" [textContent]="text.title"></h3> |
||||
</div> |
||||
|
||||
<div class="ngdialog-body op-modal--modal-body"> |
||||
<div *ngFor="let widget of selectable;trackBy: trackWidgetBy" |
||||
(click)="select($event, widget)" |
||||
[id]="widget.identifier" |
||||
[textContent]="widget.title" |
||||
class="grid--addable-widget" > |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,48 @@ |
||||
import {Component, ElementRef, Inject, ChangeDetectorRef} from "@angular/core"; |
||||
import {OpModalComponent} from "app/components/op-modals/op-modal.component"; |
||||
import {WidgetRegistration} from "app/modules/grids/grid/grid.component"; |
||||
import {OpModalLocalsToken} from "app/components/op-modals/op-modal.service"; |
||||
import {OpModalLocalsMap} from "app/components/op-modals/op-modal.types"; |
||||
import {GridWidgetsService} from "app/modules/grids/widgets/widgets.service"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
|
||||
@Component({ |
||||
templateUrl: './add.modal.html' |
||||
}) |
||||
export class AddGridWidgetModal extends OpModalComponent { |
||||
|
||||
text = { title: this.i18n.t('js.grid.add_modal.choose_widget'), |
||||
close_popup: this.i18n.t('js.button_close') }; |
||||
|
||||
public chosenWidget:WidgetRegistration; |
||||
|
||||
constructor(readonly elementRef:ElementRef, |
||||
@Inject(OpModalLocalsToken) readonly locals:OpModalLocalsMap, |
||||
readonly cdRef:ChangeDetectorRef, |
||||
readonly widgetsService:GridWidgetsService, |
||||
readonly i18n:I18nService) { |
||||
|
||||
super(locals, cdRef, elementRef); |
||||
} |
||||
|
||||
public get selectable() { |
||||
return this.widgetsService.registered.map((widget) => { |
||||
return { |
||||
identifier: widget.identifier, |
||||
title: this.i18n.t(`js.grid.widgets.${widget.identifier}.title`), |
||||
component: widget.component |
||||
}; |
||||
}).sort((a, b) => { |
||||
return a.title.localeCompare(b.title); |
||||
}); |
||||
} |
||||
|
||||
public select($event:any, widget:WidgetRegistration) { |
||||
this.chosenWidget = widget; |
||||
this.closeMe($event); |
||||
} |
||||
|
||||
public trackWidgetBy(widget:WidgetRegistration) { |
||||
return widget.identifier; |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
<h3 class="widget-box--header" cdkDragHandle> |
||||
<i class="icon-context icon-notes" aria-hidden="true"></i> |
||||
<span class="widget-box--header-title" [textContent]="text.title"></span> |
||||
</h3> |
||||
|
||||
<div class="grid--widget-content"> |
||||
<no-results *ngIf="noEntries" |
||||
[title]="text.noResults"> |
||||
</no-results> |
||||
<ng-container *ngFor="let document of entries"> |
||||
<h4 class="document-category-elements--header"> |
||||
<a [href]="documentPath(document)" |
||||
[textContent]="document.title"> |
||||
</a> |
||||
</h4> |
||||
<p class="document-category-elements--date"> |
||||
<em [textContent]="documentCreated(document)"></em> |
||||
</p> |
||||
|
||||
<div class="wiki grid--widget-limited-text" |
||||
[innerHtml]="documentDescription(document)"> |
||||
</div> |
||||
</ng-container> |
||||
</div> |
@ -0,0 +1,60 @@ |
||||
import {AbstractWidgetComponent} from "core-app/modules/grids/widgets/abstract-widget.component"; |
||||
import {Component, OnInit, SecurityContext} from '@angular/core'; |
||||
import {DocumentResource} from "../../../../../../../modules/documents/frontend/module/hal/resources/document-resource"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {CollectionResource} from "core-app/modules/hal/resources/collection-resource"; |
||||
import {HalResourceService} from "core-app/modules/hal/services/hal-resource.service"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {TimezoneService} from "core-components/datetime/timezone.service"; |
||||
import {DomSanitizer} from '@angular/platform-browser'; |
||||
|
||||
@Component({ |
||||
templateUrl: './documents.component.html', |
||||
}) |
||||
export class WidgetDocumentsComponent extends AbstractWidgetComponent implements OnInit { |
||||
public text = { |
||||
title: this.i18n.t('js.grid.widgets.documents.title'), |
||||
noResults: this.i18n.t('js.grid.widgets.documents.no_results'), |
||||
}; |
||||
|
||||
public entries:DocumentResource[] = []; |
||||
private entriesLoaded = false; |
||||
|
||||
constructor(readonly halResource:HalResourceService, |
||||
readonly pathHelper:PathHelperService, |
||||
readonly i18n:I18nService, |
||||
readonly timezone:TimezoneService, |
||||
readonly domSanitizer:DomSanitizer) { |
||||
super(i18n); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
let orders = JSON.stringify([['created_on', 'desc']]); |
||||
|
||||
let url = `${this.pathHelper.api.v3.apiV3Base}/documents?sortBy=${orders}&pageSize=10`; |
||||
|
||||
this.halResource |
||||
.get<CollectionResource>(url) |
||||
.toPromise() |
||||
.then((collection) => { |
||||
this.entries = collection.elements as DocumentResource[]; |
||||
this.entriesLoaded = true; |
||||
}); |
||||
} |
||||
|
||||
public documentPath(document:DocumentResource) { |
||||
return `${this.pathHelper.appBasePath}/documents/${document.id}`; |
||||
} |
||||
|
||||
public documentCreated(document:DocumentResource) { |
||||
return this.timezone.formattedDatetime(document.createdAt); |
||||
} |
||||
|
||||
public documentDescription(document:DocumentResource) { |
||||
return this.domSanitizer.sanitize(SecurityContext.HTML, document.description.html); |
||||
} |
||||
|
||||
public get noEntries() { |
||||
return !this.entries.length && this.entriesLoaded; |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
<h3 class="widget-box--header" cdkDragHandle> |
||||
<i class="icon-context icon-news" aria-hidden="true"></i> |
||||
<span class="widget-box--header-title" [textContent]="text.title"></span> |
||||
</h3> |
||||
|
||||
<div class="grid--widget-content"> |
||||
<no-results *ngIf="noEntries" |
||||
[title]="text.noResults"> |
||||
</no-results> |
||||
<ul class="widget-box--arrow-links"> |
||||
<li class="-widget-box--arrow-multiline" *ngFor="let news of entries"> |
||||
<div> |
||||
<img class="avatar avatar-mini avatar--fallback" |
||||
data-avatar-fallback-remove="true" |
||||
*ngIf="newsAuthorAvatar(news)" |
||||
[attr.src]="newsAuthorAvatar(news)" |
||||
[attr.title]="newsAuthorName(news)" |
||||
[attr.alt]="newsAuthorName(news)" /> |
||||
<a [href]="newsProjectPath(news)" |
||||
[textContent]="newsProjectName(news)"> |
||||
</a>: |
||||
<a [href]="newsPath(news)" |
||||
[textContent]="news.title"> |
||||
</a> |
||||
|
||||
<p class="widget-box--additional-info"> |
||||
{{text.createdBy}} |
||||
<a [href]="newsAuthorPath(news)" |
||||
[textContent]="newsAuthorName(news)"> |
||||
</a> |
||||
{{text.at}} |
||||
{{newsCreated(news)}} |
||||
</p> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
</div> |
@ -0,0 +1,83 @@ |
||||
import {AbstractWidgetComponent} from "core-app/modules/grids/widgets/abstract-widget.component"; |
||||
import {Component, OnInit} from '@angular/core'; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {HalResourceService} from "core-app/modules/hal/services/hal-resource.service"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {TimezoneService} from "core-components/datetime/timezone.service"; |
||||
import {NewsResource} from "core-app/modules/hal/resources/news-resource"; |
||||
import {UserCacheService} from "core-components/user/user-cache.service"; |
||||
import {UserResource} from "core-app/modules/hal/resources/user-resource"; |
||||
import {NewsDmService} from "core-app/modules/hal/dm-services/news-dm.service"; |
||||
|
||||
@Component({ |
||||
templateUrl: './news.component.html', |
||||
}) |
||||
export class WidgetNewsComponent extends AbstractWidgetComponent implements OnInit { |
||||
public text = { |
||||
title: this.i18n.t('js.grid.widgets.news.title'), |
||||
createdBy: this.i18n.t('js.label_created_by'), |
||||
at: this.i18n.t('js.grid.widgets.news.at'), |
||||
noResults: this.i18n.t('js.grid.widgets.news.no_results'), |
||||
}; |
||||
|
||||
public entries:NewsResource[] = []; |
||||
private entriesLoaded = false; |
||||
|
||||
constructor(readonly halResource:HalResourceService, |
||||
readonly pathHelper:PathHelperService, |
||||
readonly i18n:I18nService, |
||||
readonly timezone:TimezoneService, |
||||
readonly userCache:UserCacheService, |
||||
readonly newsDm:NewsDmService) { |
||||
super(i18n); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.newsDm |
||||
.list({ sortBy: [['created_on', 'desc']], pageSize: 3 }) |
||||
.then((collection) => { |
||||
this.entries = collection.elements as NewsResource[]; |
||||
this.entriesLoaded = true; |
||||
|
||||
this.entries.forEach((entry) => { |
||||
this.userCache |
||||
.require(entry.author.idFromLink) |
||||
.then((user:UserResource) => { |
||||
entry.author = user; |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public newsPath(news:NewsResource) { |
||||
return `${this.pathHelper.appBasePath}/news/${news.id}`; |
||||
} |
||||
|
||||
public newsProjectPath(news:NewsResource) { |
||||
return this.pathHelper.projectPath(news.project.idFromLink); |
||||
} |
||||
|
||||
public newsProjectName(news:NewsResource) { |
||||
return news.project.name; |
||||
} |
||||
|
||||
public newsAuthorName(news:NewsResource) { |
||||
return news.author.name; |
||||
} |
||||
|
||||
public newsAuthorPath(news:NewsResource) { |
||||
return this.pathHelper.userPath(news.author.id); |
||||
} |
||||
|
||||
public newsAuthorAvatar(news:NewsResource) { |
||||
return news.author.avatar; |
||||
} |
||||
|
||||
public newsCreated(news:NewsResource) { |
||||
return this.timezone.formattedDatetime(news.createdAt); |
||||
} |
||||
|
||||
public get noEntries() { |
||||
return !this.entries.length && this.entriesLoaded; |
||||
} |
||||
} |
@ -0,0 +1,100 @@ |
||||
<h3 class="widget-box--header" cdkDragHandle> |
||||
<i class="icon-context icon-time" aria-hidden="true"></i> |
||||
<span class="widget-box--header-title" [textContent]="text.title"></span> |
||||
</h3> |
||||
|
||||
<no-results *ngIf="noEntries" |
||||
[title]="text.noResults"> |
||||
</no-results> |
||||
|
||||
<ng-container *ngIf="!noEntries"> |
||||
<div class="total-hours"> |
||||
<p>Total: <span [textContent]="total"></span></p> |
||||
</div> |
||||
|
||||
<div class="generic-table--results-container" *ngIf="anyEntries"> |
||||
<table class="generic-table time-entries"> |
||||
<colgroup> |
||||
<col highlight-col> |
||||
<col highlight-col> |
||||
<col highlight-col> |
||||
<col highlight-col> |
||||
<col> |
||||
</colgroup> |
||||
<thead class="-sticky"> |
||||
<tr> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.activity"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.workPackage"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.comment"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th> |
||||
<div class="generic-table--sort-header-outer"> |
||||
<div class="generic-table--sort-header"> |
||||
<span [textContent]="text.hour"></span> |
||||
</div> |
||||
</div> |
||||
</th> |
||||
<th><div class="generic-table--empty-header"></div></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr class="time-entry" *ngFor="let item of rows"> |
||||
<td class="activity" |
||||
*ngIf="item.entry" |
||||
[textContent]="activityName(item.entry)"> |
||||
</td> |
||||
<td colspan="3" |
||||
*ngIf="item.sum"> |
||||
<strong [textContent]="item.date"></strong> |
||||
</td> |
||||
<td class="subject" |
||||
*ngIf="item.entry"> |
||||
{{projectName(item.entry)}} - <a [href]="workPackagePath(item.entry)" [textContent]="workPackageName(item.entry)"></a> |
||||
</td> |
||||
<td class="comments" |
||||
*ngIf="item.entry" |
||||
[textContent]="comment(item.entry)"> |
||||
</td> |
||||
<td class="hours" |
||||
*ngIf="item.entry" |
||||
[textContent]="hours(item.entry)"> |
||||
</td> |
||||
<td class="hours" |
||||
*ngIf="item.sum"> |
||||
<em [textContent]="item.sum | number : '1.2-2'"></em> |
||||
</td> |
||||
<td class="buttons"> |
||||
<a [href]="editPath(item.entry)" |
||||
*ngIf="item.entry && item.entry.updateImmediately" |
||||
[title]="text.edit"> |
||||
<op-icon icon-classes="icon-context icon-edit"></op-icon> |
||||
</a> |
||||
<a [href]="deletePath(item.entry)" |
||||
*ngIf="item.entry && item.entry.delete" |
||||
(click)="deleteIfConfirmed($event, item.entry)" |
||||
[title]="text.delete" > |
||||
<op-icon icon-classes="icon-context icon-delete"></op-icon> |
||||
</a> |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
</ng-container> |
@ -0,0 +1,162 @@ |
||||
import {Component, OnInit} from "@angular/core"; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {TimeEntryDmService} from "core-app/modules/hal/dm-services/time-entry-dm.service"; |
||||
import {TimeEntryResource} from "core-app/modules/hal/resources/time-entry-resource"; |
||||
import {TimezoneService} from "core-components/datetime/timezone.service"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {ConfirmDialogService} from "core-components/modals/confirm-dialog/confirm-dialog.service"; |
||||
import {formatNumber} from "@angular/common"; |
||||
import {FilterOperator} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
|
||||
@Component({ |
||||
templateUrl: './time-entries-current-user.component.html', |
||||
}) |
||||
|
||||
export class WidgetTimeEntriesCurrentUserComponent extends AbstractWidgetComponent implements OnInit { |
||||
public text = { |
||||
title: this.i18n.t('js.grid.widgets.time_entries_current_user.title'), |
||||
activity: this.i18n.t('js.time_entry.activity'), |
||||
comment: this.i18n.t('js.time_entry.comment'), |
||||
hour: this.i18n.t('js.time_entry.hours'), |
||||
workPackage: this.i18n.t('js.label_work_package'), |
||||
edit: this.i18n.t('js.button_edit'), |
||||
delete: this.i18n.t('js.button_delete'), |
||||
confirmDelete: { |
||||
text: this.i18n.t('js.text_are_you_sure'), |
||||
title: this.i18n.t('js.modals.form_submit.title') |
||||
}, |
||||
noResults: this.i18n.t('js.grid.widgets.time_entries_current_user.no_results'), |
||||
}; |
||||
public entries:TimeEntryResource[] = []; |
||||
private entriesLoaded = false; |
||||
public rows:{ date:string, sum?:string, entry?:TimeEntryResource}[] = []; |
||||
|
||||
constructor(readonly timeEntryDm:TimeEntryDmService, |
||||
readonly timezone:TimezoneService, |
||||
readonly i18n:I18nService, |
||||
readonly pathHelper:PathHelperService, |
||||
readonly confirmDialog:ConfirmDialogService) { |
||||
super(i18n); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
let filters = [['spentOn', '>t-', ['7']] as [string, FilterOperator, [string]], |
||||
['user_id', '=', ['me']] as [string, FilterOperator, [string]]]; |
||||
|
||||
this.timeEntryDm.list({ filters: filters }) |
||||
.then((collection) => { |
||||
this.buildEntries(collection.elements); |
||||
this.entriesLoaded = true; |
||||
}); |
||||
} |
||||
|
||||
public get total() { |
||||
let duration = this.entries.reduce((current, entry) => { |
||||
return current + this.timezone.toHours(entry.hours); |
||||
}, 0); |
||||
|
||||
return this.i18n.t('js.units.hour', { count: this.formatNumber(duration) }); |
||||
} |
||||
|
||||
public get anyEntries() { |
||||
return !!this.entries.length; |
||||
} |
||||
|
||||
public activityName(entry:TimeEntryResource) { |
||||
return entry.activity.name; |
||||
} |
||||
|
||||
public projectName(entry:TimeEntryResource) { |
||||
return entry.project.name; |
||||
} |
||||
|
||||
public workPackageName(entry:TimeEntryResource) { |
||||
return `#${entry.workPackage.idFromLink}: ${entry.workPackage.name}`; |
||||
} |
||||
|
||||
public workPackageId(entry:TimeEntryResource) { |
||||
return entry.workPackage.idFromLink; |
||||
} |
||||
|
||||
public comment(entry:TimeEntryResource) { |
||||
return entry.comment; |
||||
} |
||||
|
||||
public hours(entry:TimeEntryResource) { |
||||
return this.formatNumber(this.timezone.toHours(entry.hours)); |
||||
} |
||||
|
||||
public editPath(entry:TimeEntryResource) { |
||||
return this.pathHelper.timeEntryEditPath(entry.id); |
||||
} |
||||
|
||||
public deletePath(entry:TimeEntryResource) { |
||||
return this.pathHelper.timeEntryPath(entry.id); |
||||
} |
||||
|
||||
public workPackagePath(entry:TimeEntryResource) { |
||||
return this.pathHelper.workPackagePath(entry.workPackage.idFromLink); |
||||
} |
||||
|
||||
public deleteIfConfirmed(event:Event, entry:TimeEntryResource) { |
||||
event.preventDefault(); |
||||
|
||||
this.confirmDialog.confirm({ |
||||
text: this.text.confirmDelete, |
||||
closeByEscape: true, |
||||
showClose: true, |
||||
closeByDocument: true |
||||
}).then(() => { |
||||
entry.delete().then(() => { |
||||
let newEntries = this.entries.filter((anEntry) => { |
||||
return entry.id !== anEntry.id; |
||||
}); |
||||
|
||||
this.buildEntries(newEntries); |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
// nothing
|
||||
}); |
||||
} |
||||
|
||||
private buildEntries(entries:TimeEntryResource[]) { |
||||
this.entries = entries; |
||||
let sumsByDateSpent:{[key:string]:number} = {}; |
||||
|
||||
entries.forEach((entry) => { |
||||
let date = entry.spentOn; |
||||
|
||||
if (!sumsByDateSpent[date]) { |
||||
sumsByDateSpent[date] = 0; |
||||
} |
||||
|
||||
sumsByDateSpent[date] = sumsByDateSpent[date] + this.timezone.toHours(entry.hours); |
||||
}); |
||||
|
||||
let sortedEntries = entries.sort((a, b) => { |
||||
return b.spentOn.localeCompare(a.spentOn); |
||||
}); |
||||
|
||||
this.rows = []; |
||||
let currentDate:string|null = null; |
||||
sortedEntries.forEach((entry) => { |
||||
if (entry.spentOn !== currentDate) { |
||||
currentDate = entry.spentOn; |
||||
this.rows.push({date: this.timezone.formattedDate(currentDate!), sum: this.formatNumber(sumsByDateSpent[currentDate!])}); |
||||
} |
||||
|
||||
this.rows.push({date: currentDate!, entry: entry}); |
||||
}); |
||||
//entries
|
||||
} |
||||
|
||||
private formatNumber(value:number) { |
||||
return formatNumber(value, this.i18n.locale, '1.2-2'); |
||||
} |
||||
|
||||
public get noEntries() { |
||||
return !this.entries.length && this.entriesLoaded; |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
import {Injectable} from "@angular/core"; |
||||
import {WidgetRegistration} from "app/modules/grids/grid/grid.component"; |
||||
import {HookService} from "app/modules/plugins/hook-service"; |
||||
|
||||
@Injectable() |
||||
export class GridWidgetsService { |
||||
constructor(private Hook:HookService) {} |
||||
|
||||
public get registered() { |
||||
let registeredWidgets:WidgetRegistration[] = []; |
||||
|
||||
_.each(this.Hook.call('gridWidgets'), (registration:WidgetRegistration[]) => { |
||||
registeredWidgets = registeredWidgets.concat(registration); |
||||
}); |
||||
|
||||
return registeredWidgets; |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
import {Component, OnInit} from "@angular/core"; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
import {ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
|
||||
@Component({ |
||||
templateUrl: '../wp-widget/wp-widget.component.html', |
||||
styleUrls: ['../wp-widget/wp-widget.component.css'] |
||||
}) |
||||
|
||||
export class WidgetWpAccountableComponent extends AbstractWidgetComponent implements OnInit { |
||||
public text = { title: this.i18n.t('js.grid.widgets.work_packages_accountable.title') }; |
||||
public queryProps:any; |
||||
public configuration = { "actionsColumnEnabled": false, |
||||
"columnMenuEnabled": false, |
||||
"contextMenuEnabled": false }; |
||||
|
||||
ngOnInit() { |
||||
let filters = new ApiV3FilterBuilder(); |
||||
filters.add('responsible', '=', ["me"]); |
||||
filters.add('status', 'o', []); |
||||
|
||||
this.queryProps = {"columns[]":["id", "project", "type", "subject"], |
||||
"filters":filters.toJson()}; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,26 @@ |
||||
import {Component, OnInit} from "@angular/core"; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
import {ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
|
||||
@Component({ |
||||
templateUrl: '../wp-widget/wp-widget.component.html', |
||||
styleUrls: ['../wp-widget/wp-widget.component.css'] |
||||
}) |
||||
|
||||
export class WidgetWpAssignedComponent extends AbstractWidgetComponent implements OnInit { |
||||
public text = { title: this.i18n.t('js.grid.widgets.work_packages_assigned.title') }; |
||||
public queryProps:any; |
||||
public configuration = { "actionsColumnEnabled": false, |
||||
"columnMenuEnabled": false, |
||||
"contextMenuEnabled": false }; |
||||
|
||||
ngOnInit() { |
||||
let filters = new ApiV3FilterBuilder(); |
||||
filters.add('assignee', '=', ["me"]); |
||||
filters.add('status', 'o', []); |
||||
|
||||
this.queryProps = {"columns[]":["id", "project", "type", "subject"], |
||||
"filters":filters.toJson()}; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,7 @@ |
||||
<h3 class="widget-box--header" cdkDragHandle> |
||||
<i class="icon-context icon-calendar" aria-hidden="true"></i> |
||||
<span class="widget-box--header-title" [textContent]="text.title"></span> |
||||
</h3> |
||||
|
||||
<wp-calendar [static]="true"> |
||||
</wp-calendar> |
@ -0,0 +1,37 @@ |
||||
// -- 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} from '@angular/core'; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
|
||||
@Component({ |
||||
templateUrl: './wp-calendar.component.html', |
||||
}) |
||||
export class WidgetWpCalendarComponent extends AbstractWidgetComponent { |
||||
public text = { title: this.i18n.t('js.grid.widgets.work_packages_calendar.title') }; |
||||
} |
@ -0,0 +1,25 @@ |
||||
import {Component, OnInit} from "@angular/core"; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
import {ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
|
||||
@Component({ |
||||
templateUrl: '../wp-widget/wp-widget.component.html', |
||||
styleUrls: ['../wp-widget/wp-widget.component.css'] |
||||
}) |
||||
export class WidgetWpCreatedComponent extends AbstractWidgetComponent implements OnInit { |
||||
public text = { title: this.i18n.t('js.grid.widgets.work_packages_created.title') }; |
||||
public queryProps:any; |
||||
public configuration = { "actionsColumnEnabled": false, |
||||
"columnMenuEnabled": false, |
||||
"contextMenuEnabled": false }; |
||||
|
||||
ngOnInit() { |
||||
let filters = new ApiV3FilterBuilder(); |
||||
filters.add('author', '=', ["me"]); |
||||
filters.add('status', 'o', []); |
||||
|
||||
this.queryProps = {"columns[]":["id", "project", "type", "subject"], |
||||
"filters":filters.toJson()}; |
||||
|
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
import {Component, OnInit} from "@angular/core"; |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
import {ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
|
||||
@Component({ |
||||
templateUrl: '../wp-widget/wp-widget.component.html', |
||||
styleUrls: ['../wp-widget/wp-widget.component.css'] |
||||
}) |
||||
export class WidgetWpWatchedComponent extends AbstractWidgetComponent implements OnInit { |
||||
public text = { title: this.i18n.t('js.grid.widgets.work_packages_watched.title') }; |
||||
public queryProps:any; |
||||
public configuration = { "actionsColumnEnabled": false, |
||||
"columnMenuEnabled": false, |
||||
"contextMenuEnabled": false }; |
||||
|
||||
ngOnInit() { |
||||
let filters = new ApiV3FilterBuilder(); |
||||
filters.add('watcher', '=', ["me"]); |
||||
filters.add('status', 'o', []); |
||||
|
||||
this.queryProps = {"columns[]":["id", "project", "type", "subject"], |
||||
"filters":filters.toJson()}; |
||||
|
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
wp-embedded-table { |
||||
display: flex; |
||||
flex: 1 1 auto; |
||||
overflow: hidden; |
||||
} |
@ -0,0 +1,9 @@ |
||||
<h3 class="widget-box--header" cdkDragHandle> |
||||
<i class="icon-context icon-assigned-to-me" aria-hidden="true"></i> |
||||
<span class="widget-box--header-title" [textContent]="text.title"></span> |
||||
</h3> |
||||
|
||||
<wp-embedded-table [queryProps]="queryProps" |
||||
[configuration]="configuration" |
||||
[externalHeight]="true"> |
||||
</wp-embedded-table> |
@ -0,0 +1,4 @@ |
||||
import {AbstractWidgetComponent} from "app/modules/grids/widgets/abstract-widget.component"; |
||||
|
||||
export class WidgetWpListComponent extends AbstractWidgetComponent { |
||||
} |
@ -0,0 +1,81 @@ |
||||
//-- 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 {DmListParameter, DmServiceInterface} from "core-app/modules/hal/dm-services/dm.service.interface"; |
||||
import {HalResourceService} from "core-app/modules/hal/services/hal-resource.service"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {CollectionResource} from "core-app/modules/hal/resources/collection-resource"; |
||||
import {ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {Injectable} from '@angular/core'; |
||||
|
||||
// This only needs to be Injectable for the tests to work
|
||||
@Injectable() |
||||
export abstract class AbstractDmService<T extends HalResource> implements DmServiceInterface { |
||||
constructor(protected halResourceService:HalResourceService, |
||||
protected pathHelper:PathHelperService) { |
||||
} |
||||
|
||||
public list(params:DmListParameter|null):Promise<CollectionResource> { |
||||
let queryProps = []; |
||||
|
||||
if (params && params.sortBy) { |
||||
queryProps.push(`sortBy=${JSON.stringify(params.sortBy)}`); |
||||
} |
||||
|
||||
if (params && params.pageSize) { |
||||
queryProps.push(`pageSize=${params.pageSize}`); |
||||
} |
||||
|
||||
if (params && params.filters) { |
||||
let filters = new ApiV3FilterBuilder(); |
||||
|
||||
params.filters.forEach((filterParam) => { |
||||
filters.add(...filterParam); |
||||
}); |
||||
|
||||
queryProps.push(filters.toParams()); |
||||
} |
||||
|
||||
let queryPropsString = ''; |
||||
|
||||
if (queryProps.length) { |
||||
queryPropsString = `?${queryProps.join('&')}`; |
||||
} |
||||
|
||||
return this.halResourceService.get<CollectionResource<T>>(this.listUrl() + queryPropsString).toPromise(); |
||||
} |
||||
|
||||
public one(id:number):Promise<T> { |
||||
return this.halResourceService.get<T>(this.oneUrl(id).toString()).toPromise(); |
||||
} |
||||
|
||||
|
||||
protected abstract listUrl():string; |
||||
protected abstract oneUrl(id:number|string):string; |
||||
} |
@ -0,0 +1,40 @@ |
||||
//-- 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 {CollectionResource} from "core-app/modules/hal/resources/collection-resource"; |
||||
import {FilterOperator} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
|
||||
export interface DmListParameter { |
||||
filters?:[string, FilterOperator, [string]][]; |
||||
sortBy?:[string, string][]; |
||||
pageSize?:number; |
||||
} |
||||
|
||||
export interface DmServiceInterface { |
||||
list(params:DmListParameter):Promise<CollectionResource>; |
||||
} |
@ -0,0 +1,111 @@ |
||||
//-- 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 {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service'; |
||||
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; |
||||
import {GridResource} from "core-app/modules/hal/resources/grid-resource"; |
||||
import {FormResource} from "core-app/modules/hal/resources/form-resource"; |
||||
import {CollectionResource} from "core-app/modules/hal/resources/collection-resource"; |
||||
import {ApiV3FilterBuilder, FilterOperator} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
import {PayloadDmService} from "core-app/modules/hal/dm-services/payload-dm.service"; |
||||
import {SchemaResource} from "core-app/modules/hal/resources/schema-resource"; |
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {AbstractDmService} from "core-app/modules/hal/dm-services/abstract-dm.service"; |
||||
|
||||
@Injectable() |
||||
export class GridDmService extends AbstractDmService<GridResource> { |
||||
constructor(protected halResourceService:HalResourceService, |
||||
protected pathHelper:PathHelperService, |
||||
protected payloadDm:PayloadDmService) { |
||||
super(halResourceService, |
||||
pathHelper); |
||||
} |
||||
|
||||
public createForm(resource:GridResource|null|any = null, schema:SchemaResource|null = null) { |
||||
let payload = this.extractPayload(resource, schema); |
||||
|
||||
return this.halResourceService.post<FormResource>(this.pathHelper.api.v3.grids.form().toString(), |
||||
payload).toPromise(); |
||||
} |
||||
|
||||
public create(resource:GridResource, schema:SchemaResource|null = null):Promise<GridResource> { |
||||
let payload = this.extractPayload(resource, schema); |
||||
|
||||
return this.halResourceService.post<GridResource>(this.pathHelper.api.v3.grids.path, |
||||
payload).toPromise(); |
||||
} |
||||
|
||||
public update(resource:GridResource, schema:SchemaResource):Promise<GridResource> { |
||||
let payload = this.extractPayload(resource, schema); |
||||
|
||||
return this.halResourceService.patch<GridResource>(this.pathHelper.api.v3.grids.id(resource.idFromLink).toString(), |
||||
payload).toPromise(); |
||||
} |
||||
|
||||
public updateForm(resource:GridResource, schema:SchemaResource|null = null) { |
||||
let payload = this.extractPayload(resource, schema); |
||||
|
||||
return this.halResourceService.post<FormResource>(this.pathHelper.api.v3.grids.id(resource.idFromLink).form.toString(), |
||||
payload).toPromise(); |
||||
} |
||||
|
||||
public extractPayload(resource:GridResource|null = null, schema:SchemaResource|null = null) { |
||||
if (resource && schema) { |
||||
let payload = this.payloadDm.extract(resource, schema); |
||||
|
||||
// The widget only states the type of the widget resource but does not explain
|
||||
// the widget itself. We therefore have to do that by hand.
|
||||
if (payload.widgets) { |
||||
payload.widgets = resource.widgets.map((widget) => { |
||||
return { |
||||
startRow: widget.startRow, |
||||
endRow: widget.endRow, |
||||
startColumn: widget.startColumn, |
||||
endColumn: widget.endColumn, |
||||
identifier: widget.identifier |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
return payload; |
||||
} else if (!(resource instanceof HalResource)) { |
||||
return resource; |
||||
} else { |
||||
return {}; |
||||
} |
||||
} |
||||
|
||||
protected listUrl() { |
||||
return this.pathHelper.api.v3.grids.toString(); |
||||
} |
||||
|
||||
protected oneUrl(id:number|string) { |
||||
return this.pathHelper.api.v3.grids.id(id).toString(); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
//-- 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 {AbstractDmService} from "core-app/modules/hal/dm-services/abstract-dm.service"; |
||||
import {NewsResource} from "core-app/modules/hal/resources/news-resource"; |
||||
|
||||
@Injectable() |
||||
export class NewsDmService extends AbstractDmService<NewsResource> { |
||||
protected listUrl() { |
||||
return this.pathHelper.api.v3.news.toString(); |
||||
} |
||||
|
||||
protected oneUrl(id:number|string) { |
||||
return this.pathHelper.api.v3.news.id(id).toString(); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
//-- 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 {AbstractDmService} from "core-app/modules/hal/dm-services/abstract-dm.service"; |
||||
import {TimeEntryResource} from "core-app/modules/hal/resources/time-entry-resource"; |
||||
|
||||
@Injectable() |
||||
export class TimeEntryDmService extends AbstractDmService<TimeEntryResource> { |
||||
protected listUrl() { |
||||
return this.pathHelper.api.v3.time_entries.toString(); |
||||
} |
||||
|
||||
protected oneUrl(id:number|string) { |
||||
return this.pathHelper.api.v3.time_entries.id(id).toString(); |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource"; |
||||
|
||||
export class GridResource extends HalResource { |
||||
public widgets:GridWidgetResource[]; |
||||
|
||||
public $initialize(source:any) { |
||||
super.$initialize(source); |
||||
|
||||
this.widgets = this |
||||
.widgets |
||||
.map((widget:Object) => new GridWidgetResource( |
||||
this.injector, |
||||
widget, |
||||
true, |
||||
this.halInitializer, |
||||
'GridWidget' |
||||
) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,45 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
export class GridWidgetResource extends HalResource { |
||||
public identifier:string; |
||||
public startRow:number; |
||||
public endRow:number; |
||||
public startColumn:number; |
||||
public endColumn:number; |
||||
|
||||
public get height() { |
||||
return this.endRow - this.startRow; |
||||
} |
||||
|
||||
public get width() { |
||||
return this.endColumn - this.startColumn; |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
export class NewsResource extends HalResource { |
||||
} |
@ -0,0 +1,32 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; |
||||
|
||||
export class TimeEntryResource extends HalResource { |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue