Merge remote-tracking branch 'origin/release/8.0' into dev

pull/6717/head
Oliver Günther 6 years ago
commit 1fdac27e65
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 97
      app/contracts/types/base_contract.rb
  2. 45
      app/models/type/attribute_groups.rb
  3. 21
      app/services/base_type_service.rb
  4. 3
      app/services/create_type_service.rb
  5. 5
      app/services/update_type_service.rb
  6. 36
      config/locales/crowdin/cs.yml
  7. 100
      config/locales/crowdin/fa.yml
  8. 6
      config/locales/crowdin/js-cs.yml
  9. 50
      config/locales/crowdin/js-fa.yml
  10. 3
      config/locales/crowdin/nl.yml
  11. 24
      docs/api/apiv3/endpoints/queries.apib
  12. 16
      docs/api/apiv3/endpoints/work-packages.apib
  13. 4
      frontend/src/app/components/routing/wp-list/wp-list.component.ts
  14. 104
      frontend/src/app/components/wp-list/wp-list.service.ts
  15. 4
      frontend/src/app/components/wp-query-select/wp-static-queries.service.ts
  16. 2
      frontend/src/app/modules/common/loading-indicator/loading-indicator.service.ts
  17. 16
      frontend/src/app/modules/hal/dm-services/query-dm.service.ts
  18. 1
      lib/open_project/text_formatting/matchers/resource_links_matcher.rb
  19. 6
      spec/lib/open_project/text_formatting/markdown/markdown_spec.rb
  20. 45
      spec/models/type/attribute_groups_spec.rb
  21. 4
      spec/services/create_type_service_spec.rb
  22. 21
      spec/services/shared_type_service.rb
  23. 50
      spec/services/update_type_service_spec.rb

@ -0,0 +1,97 @@
#-- 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.
#++
require 'model_contract'
module Types
class BaseContract < ::ModelContract
def self.model
Type
end
attribute :name
attribute :is_in_roadmap
attribute :is_milestone
attribute :is_default
attribute :color_id
attribute :project_ids
attribute :attribute_groups
validate :validate_current_user_is_admin
validate :validate_attribute_group_names
validate :validate_attribute_groups
def validate_current_user_is_admin
unless user.admin?
errors.add(:base, :error_unauthorized)
end
end
def validate_attribute_group_names
seen = Set.new
model.attribute_groups.each do |group|
errors.add(:attribute_groups, :group_without_name) unless group.key.present?
errors.add(:attribute_groups, :duplicate_group, group: group.key) if seen.add?(group.key).nil?
end
end
def validate_attribute_groups
model.attribute_groups_objects.each do |group|
if group.is_a?(Type::QueryGroup)
validate_query_group(group)
else
validate_attribute_group(group)
end
end
end
def validate_query_group(group)
query = group.query
contract_class = query.persisted? ? Queries::UpdateContract : Queries::CreateContract
contract = contract_class.new(query, user)
unless contract.validate
errors.add(:attribute_groups, :query_invalid, group: group.key, details: contract.errors.full_messages.join)
end
end
def validate_attribute_group(group)
valid_attributes = model.work_package_attributes.keys
group.attributes.each do |key|
if key.is_a?(String) && valid_attributes.exclude?(key)
errors.add(:attribute_groups, :attribute_unknown)
end
end
end
end
end

@ -35,9 +35,9 @@ module Type::AttributeGroups
before_save :write_attribute_groups_objects before_save :write_attribute_groups_objects
after_save :unset_attribute_groups_objects after_save :unset_attribute_groups_objects
after_destroy :remove_attribute_groups_queries after_destroy :remove_attribute_groups_queries
validate :validate_attribute_group_names
validate :validate_attribute_groups
serialize :attribute_groups, Array serialize :attribute_groups, Array
attr_accessor :attribute_groups_objects
# Mapping from AR attribute name to a default group # Mapping from AR attribute name to a default group
# May be extended by plugins # May be extended by plugins
@ -135,9 +135,6 @@ module Type::AttributeGroups
self.attribute_groups_objects = nil self.attribute_groups_objects = nil
end end
protected
attr_accessor :attribute_groups_objects
private private
@ -167,44 +164,6 @@ module Type::AttributeGroups
end end
end end
def validate_attribute_group_names
seen = Set.new
attribute_groups.each do |group|
errors.add(:attribute_groups, :group_without_name) unless group.key.present?
errors.add(:attribute_groups, :duplicate_group, group: group.key) if seen.add?(group.key).nil?
end
end
def validate_attribute_groups
attribute_groups_objects.each do |group|
if group.is_a?(Type::QueryGroup)
validate_query_group(group)
else
validate_attribute_group(group)
end
end
end
def validate_query_group(group)
query = group.query
contract_class = query.persisted? ? Queries::UpdateContract : Queries::CreateContract
contract = contract_class.new(query, User.current)
unless contract.validate
errors.add(:attribute_groups, :query_invalid, group: group.key, details: contract.errors.full_messages.join)
end
end
def validate_attribute_group(group)
valid_attributes = work_package_attributes.keys
group.attributes.each do |key|
if key.is_a?(String) && valid_attributes.exclude?(key)
errors.add(:attribute_groups, :attribute_unknown)
end
end
end
## ##
# Get the default attribute groups for this type. # Get the default attribute groups for this type.

@ -29,11 +29,15 @@
class BaseTypeService class BaseTypeService
include Shared::BlockService include Shared::BlockService
include Concerns::Contracted
attr_accessor :contract_class
attr_accessor :type, :user attr_accessor :type, :user
def initialize(user) def initialize(type, user)
self.type = type
self.user = user self.user = user
self.contract_class = ::Types::BaseContract
end end
def call(params, options, &block) def call(params, options, &block)
@ -45,7 +49,10 @@ class BaseTypeService
private private
def update(params, options) def update(params, options)
success = Type.transaction do success = false
errors = type.errors
Type.transaction do
set_scalar_params(params) set_scalar_params(params)
# Only set attribute groups when it exists # Only set attribute groups when it exists
@ -56,17 +63,21 @@ class BaseTypeService
set_active_custom_fields set_active_custom_fields
if type.save success, errors = validate_and_save(type, user)
if success
after_type_save(params, options) after_type_save(params, options)
true
else else
raise(ActiveRecord::Rollback) raise(ActiveRecord::Rollback)
end end
end end
ServiceResult.new(success: success, ServiceResult.new(success: success,
errors: type.errors, errors: errors,
result: type) result: type)
rescue => e
ServiceResult.new(success: false).tap do |result|
result.errors.add(:base, e.message)
end
end end
def set_scalar_params(params) def set_scalar_params(params)

@ -29,8 +29,7 @@
class CreateTypeService < BaseTypeService class CreateTypeService < BaseTypeService
def initialize(user) def initialize(user)
super super Type.new, user
self.type = Type.new
end end
private private

@ -28,11 +28,6 @@
#++ #++
class UpdateTypeService < BaseTypeService class UpdateTypeService < BaseTypeService
def initialize(type, user)
super(user)
self.type = type
end
def call(params) def call(params)
# forbid renaming if it is a standard type # forbid renaming if it is a standard type
params[:type].delete :name if type.is_standard? params[:type].delete :name if type.is_standard?

@ -64,7 +64,7 @@ cs:
index: index:
no_results_title_text: Momentálně zde nejsou žádné barvy. no_results_title_text: Momentálně zde nejsou žádné barvy.
no_results_content_text: Vytvořit novou barvu no_results_content_text: Vytvořit novou barvu
label_no_color: No color label_no_color: Bez barvy
custom_actions: custom_actions:
actions: actions:
name: Akce name: Akce
@ -94,7 +94,7 @@ cs:
no_results_title_text: V současné době nejsou žádná vlastní pole. no_results_title_text: V současné době nejsou žádná vlastní pole.
no_results_content_text: Vytvořit nové vlastní pole no_results_content_text: Vytvořit nové vlastní pole
concatenation: concatenation:
single: or single: nebo
deprecations: deprecations:
old_timeline: old_timeline:
replacement: This timelines module is being replaced by the interactive timeline replacement: This timelines module is being replaced by the interactive timeline
@ -159,8 +159,8 @@ cs:
prioritiies: prioritiies:
edit: edit:
priority_color_text: | priority_color_text: |
Click to assign or change the color of this priority. Klikněte na přiřadit nebo změnit barvu této priority.
It can be used for highlighting work packages in the table. lze použít ke zvýraznění pracovních balíčků v tabulce.
reportings: reportings:
index: index:
no_results_title_text: There are currently no status reportings. no_results_title_text: There are currently no status reportings.
@ -168,8 +168,8 @@ cs:
statuses: statuses:
edit: edit:
status_color_text: | status_color_text: |
Click to assign or change the color of this status. Klikněte na přiřadit nebo změnit barvu této priority.
It is shown in the status button and can be used for highlighting work packages in the table. lze použít ke zvýraznění pracovních balíčků v tabulce.
index: index:
no_results_title_text: Momentálně zde nejsou žádné stavy pracovního balíčku. no_results_title_text: Momentálně zde nejsou žádné stavy pracovního balíčku.
no_results_content_text: Přidat nový stav no_results_content_text: Přidat nový stav
@ -405,7 +405,7 @@ cs:
warn_on_leaving_unsaved: Warn me when leaving a work package with unsaved warn_on_leaving_unsaved: Warn me when leaving a work package with unsaved
changes changes
version: version:
effective_date: Finish date effective_date: Datum dokončení
sharing: Sdílení sharing: Sdílení
wiki_content: wiki_content:
text: Text text: Text
@ -658,7 +658,7 @@ cs:
default_columns: Výchozí sloupce default_columns: Výchozí sloupce
description: Popis description: Popis
display_sums: Zobrazit součty display_sums: Zobrazit součty
due_date: Finish date due_date: Datum dokončení
estimated_hours: Odhadovaný čas estimated_hours: Odhadovaný čas
estimated_time: Odhadovaný čas estimated_time: Odhadovaný čas
firstname: Křestní jméno firstname: Křestní jméno
@ -677,7 +677,7 @@ cs:
password: Heslo password: Heslo
priority: Priorita priority: Priorita
project: Projekt project: Projekt
responsible: Accountable responsible: Odpovědný
role: Role role: Role
roles: Role roles: Role
start_date: Datum zahájení start_date: Datum zahájení
@ -1336,7 +1336,7 @@ cs:
label_group_by: Seskupit podle label_group_by: Seskupit podle
label_group_new: Nová skupina label_group_new: Nová skupina
label_group: Skupina label_group: Skupina
label_group_named: Group %{name} label_group_named: Skupina %{name}
label_group_plural: Skupiny label_group_plural: Skupiny
label_help: Nápověda label_help: Nápověda
label_here: zde label_here: zde
@ -1596,7 +1596,7 @@ cs:
label_used_by_types: Used by types label_used_by_types: Used by types
label_used_in_projects: Used in projects label_used_in_projects: Used in projects
label_user: Uživatel label_user: Uživatel
label_user_named: User %{name} label_user_named: Uživatel %{name}
label_user_activity: Aktivita %{value} label_user_activity: Aktivita %{value}
label_user_anonymous: Anonymní label_user_anonymous: Anonymní
label_user_mail_option_all: Pro všechny události všech mých projektů label_user_mail_option_all: Pro všechny události všech mých projektů
@ -1660,8 +1660,8 @@ cs:
mně mně
label_work_package_view_all_reported_by_me: Zobrazit všechny pracovní balíčky mnou label_work_package_view_all_reported_by_me: Zobrazit všechny pracovní balíčky mnou
nahlášené nahlášené
label_work_package_view_all_responsible_for: View all work packages that I am accountable label_work_package_view_all_responsible_for: Zobrazit všechny pracovní balíčky,
for za které jsem zodpovědný
label_work_package_view_all_watched: Zobrazit všechny sledované balíčky label_work_package_view_all_watched: Zobrazit všechny sledované balíčky
label_work_package_watchers: Sledující label_work_package_watchers: Sledující
label_workflow: Pracovní postup label_workflow: Pracovní postup
@ -1943,7 +1943,7 @@ cs:
permission_manage_members: Správa členů permission_manage_members: Správa členů
permission_manage_news: Spravovat novinky permission_manage_news: Spravovat novinky
permission_manage_project_activities: Spravovat projektové aktivity permission_manage_project_activities: Spravovat projektové aktivity
permission_manage_public_queries: Manage public views permission_manage_public_queries: Spravovat veřejné dotazy
permission_manage_repository: Správa repozitáře permission_manage_repository: Správa repozitáře
permission_manage_subtasks: Spravovat dílčí úkoly permission_manage_subtasks: Spravovat dílčí úkoly
permission_manage_versions: Správovat verze permission_manage_versions: Správovat verze
@ -1952,7 +1952,7 @@ cs:
permission_move_work_packages: Přesun pracovních balíčků permission_move_work_packages: Přesun pracovních balíčků
permission_protect_wiki_pages: Ochrana stránky wiki permission_protect_wiki_pages: Ochrana stránky wiki
permission_rename_wiki_pages: Přejmenovat stránky wiki permission_rename_wiki_pages: Přejmenovat stránky wiki
permission_save_queries: Save views permission_save_queries: Uložit pohled
permission_select_project_modules: Vyberte moduly projektu permission_select_project_modules: Vyberte moduly projektu
permission_manage_types: Vyberte typy permission_manage_types: Vyberte typy
permission_view_calendar: Zobrazit kalendář permission_view_calendar: Zobrazit kalendář
@ -2363,8 +2363,8 @@ cs:
text_journal_aggregation_time_explanation: Combine journals for display if their text_journal_aggregation_time_explanation: Combine journals for display if their
age difference is less than the specified timespan. This will also delay mail age difference is less than the specified timespan. This will also delay mail
notifications by the same amount of time. notifications by the same amount of time.
text_journal_changed: "%{label} changed from %{old} <br/><strong>to</strong> %{new}" text_journal_changed: "%{label} změněn z %{old} <br/><strong>na</strong> %{new}"
text_journal_changed_plain: "%{label} changed from %{old} \nto %{new}" text_journal_changed_plain: "%{label} změněn z %{old} \nna %{new}"
text_journal_changed_no_detail: "%{label} aktualizován" text_journal_changed_no_detail: "%{label} aktualizován"
text_journal_changed_with_diff: "%{label} změněn (%{link})" text_journal_changed_with_diff: "%{label} změněn (%{link})"
text_journal_deleted: "%{label} smazán (%{old})" text_journal_deleted: "%{label} smazán (%{old})"
@ -2545,7 +2545,7 @@ cs:
project_time_filter_relative: "%{start_label} %{startspan}%{startspanunit} ago, project_time_filter_relative: "%{start_label} %{startspan}%{startspanunit} ago,
%{end_label} %{endspan}%{endspanunit} from now" %{end_label} %{endspan}%{endspanunit} from now"
project_filters: Filtrovat projekty project_filters: Filtrovat projekty
project_responsible: Show projects with accountable project_responsible: Zobrazit projekty s odpovědností
project_status: Zobrazit stav projektu project_status: Zobrazit stav projektu
timeframe: Zobrazit časový rámec timeframe: Zobrazit časový rámec
timeframe_end: do timeframe_end: do

@ -671,78 +671,78 @@ fa:
status: Status status: Status
subject: Subject subject: Subject
summary: Summary summary: Summary
title: Title title: عنوان
type: نوع type: نوع
updated_at: Updated on updated_at: به روز شده
updated_on: Updated on updated_on: به روز شده
user: User user: User
version: نسخه version: نسخه
work_package: Work package work_package: Work package
button_add: Add button_add: ﺍﻓﺰﻭﺩﻥ
button_add_member: Add member button_add_member: Add member
button_add_watcher: Add watcher button_add_watcher: Add watcher
button_annotate: Annotate button_annotate: Annotate
button_apply: Apply button_apply: درخواست
button_archive: Archive button_archive: آرشیو
button_back: Back button_back: برگشت
button_cancel: Cancel button_cancel: لغو
button_change: Change button_change: تغییر
button_change_parent_page: Change parent page button_change_parent_page: تغییر صفحه والدین
button_change_password: Change password button_change_password: تغییر رمز عبور
button_check_all: Check all button_check_all: بررسی همه
button_clear: Clear button_clear: پاکسازی
button_close: Close button_close: Close
button_collapse_all: Collapse all button_collapse_all: بستن همه
button_configure: Configure button_configure: ویرایش
button_continue: Continue button_continue: Continue
button_copy: کپی button_copy: کپی
button_copy_and_follow: Copy and follow button_copy_and_follow: کپی کنید و دنبال کردن
button_create: Create button_create: ایجاد
button_create_and_continue: Create and continue button_create_and_continue: ذخیره و ادامه
button_delete: Delete button_delete: حذف
button_decline: Decline button_decline: Decline
button_delete_watcher: Delete watcher %{name} button_delete_watcher: Delete watcher %{name}
button_download: Download button_download: Download
button_duplicate: Duplicate button_duplicate: نسخه برداری
button_edit: Edit button_edit: ویرایش
button_edit_associated_wikipage: 'Edit associated Wiki page: %{page_title}' button_edit_associated_wikipage: 'ویرایش صفحه ویکی مرتبط: %{page_title}'
button_expand_all: Expand all button_expand_all: باز کردن همه
button_filter: Filter button_filter: Filter
button_generate: Generate button_generate: Generate
button_list: List button_list: لیست
button_lock: قفل کردن button_lock: قفل کردن
button_log_time: پیگیری زمان button_log_time: پیگیری زمان
button_login: ورود button_login: ورود
button_move: انتقال button_move: انتقال
button_move_and_follow: Move and follow button_move_and_follow: کپی و دنبال کردن
button_print: چاپ کردن button_print: چاپ کردن
button_quote: نقل قول button_quote: نقل قول
button_remove: Remove button_remove: Remove
button_remove_widget: Remove widget button_remove_widget: حذف ویجت
button_rename: Rename button_rename: تغییر نام
button_replace: Replace button_replace: Replace
button_reply: Reply button_reply: پاسخ دادن
button_reset: Reset button_reset: تنظیم مجدد
button_rollback: Rollback to this version button_rollback: برگرداندن به این نسخه
button_save: Save button_save: ذخیره
button_save_back: Save and back button_save_back: Save and back
button_show: Show button_show: نمایش
button_sort: Sort button_sort: مرتبسازی
button_submit: Submit button_submit: ثبت کردن
button_test: Test button_test: بررسی
button_unarchive: Unarchive button_unarchive: خروج از آرشیو
button_uncheck_all: Uncheck all button_uncheck_all: لغو انتخاب همه
button_unlock: Unlock button_unlock: باز کردن
button_unwatch: Unwatch button_unwatch: عدم دنبال
button_update: به روز رسانی button_update: به روز رسانی
button_upgrade: Upgrade button_upgrade: Upgrade
button_upload: Upload button_upload: Upload
button_view: مشاهده button_view: مشاهده
button_watch: نگاه کردن button_watch: نگاه کردن
button_manage_menu_entry: Configure menu item button_manage_menu_entry: پیکربندی وسایل منو
button_add_menu_entry: Add menu item button_add_menu_entry: افزودن آیتم منو
button_configure_menu_entry: Configure menu item button_configure_menu_entry: پیکربندی وسایل منو
button_delete_menu_entry: Delete menu item button_delete_menu_entry: حذف آیتم های منو
consent: consent:
checkbox_label: I have noted and do consent to the above. checkbox_label: I have noted and do consent to the above.
failure_message: Consent failed, cannot proceed. failure_message: Consent failed, cannot proceed.
@ -1256,7 +1256,7 @@ fa:
label_duplicated_by: duplicated by label_duplicated_by: duplicated by
label_duplicate: duplicate label_duplicate: duplicate
label_duplicates: duplicates label_duplicates: duplicates
label_edit: Edit label_edit: ویرایش
label_enable_multi_select: Toggle multiselect label_enable_multi_select: Toggle multiselect
label_enabled_project_custom_fields: Enabled custom fields label_enabled_project_custom_fields: Enabled custom fields
label_enabled_project_modules: Enabled modules label_enabled_project_modules: Enabled modules
@ -1342,7 +1342,7 @@ fa:
label_ldap_authentication: LDAP authentication label_ldap_authentication: LDAP authentication
label_less_or_equal: "<=" label_less_or_equal: "<="
label_less_than_ago: less than days ago label_less_than_ago: less than days ago
label_list: List label_list: لیست
label_loading: Loading... label_loading: Loading...
label_lock_user: کاربر قفل شده label_lock_user: کاربر قفل شده
label_logged_as: Logged in as label_logged_as: Logged in as
@ -1511,7 +1511,7 @@ fa:
label_settings: Settings label_settings: Settings
label_system_settings: System settings label_system_settings: System settings
label_show_completed_versions: Show completed versions label_show_completed_versions: Show completed versions
label_sort: Sort label_sort: مرتبسازی
label_sort_by: Sort by %{value} label_sort_by: Sort by %{value}
label_sorted_by: sorted by %{value} label_sorted_by: sorted by %{value}
label_sort_higher: Move up label_sort_higher: Move up
@ -2278,7 +2278,7 @@ fa:
text_database_allows_tsv: Database allows TSVector (optional) text_database_allows_tsv: Database allows TSVector (optional)
text_default_administrator_account_changed: Default administrator account changed text_default_administrator_account_changed: Default administrator account changed
text_default_encoding: 'Default: UTF-8' text_default_encoding: 'Default: UTF-8'
text_destroy: Delete text_destroy: حذف
text_destroy_with_associated: 'There are additional objects assossociated with the text_destroy_with_associated: 'There are additional objects assossociated with the
work package(s) that are to be deleted. Those objects are of the following types:' work package(s) that are to be deleted. Those objects are of the following types:'
text_destroy_what_to_do: What do you want to do? text_destroy_what_to_do: What do you want to do?
@ -2402,13 +2402,13 @@ fa:
dates_are_calculated_based_on_sub_elements: Dates are calculated based on sub dates_are_calculated_based_on_sub_elements: Dates are calculated based on sub
elements. elements.
delete_all: Delete all delete_all: Delete all
delete_thing: Delete delete_thing: حذف
duration: Duration duration: Duration
duration_days: duration_days:
one: 1 day one: 1 day
other: "%{count} days" other: "%{count} days"
edit_color: Edit color edit_color: Edit color
edit_thing: Edit edit_thing: ویرایش
edit_timeline: Edit timeline report %{timeline} edit_timeline: Edit timeline report %{timeline}
delete_timeline: Delete timeline report %{timeline} delete_timeline: Delete timeline report %{timeline}
empty: "(empty)" empty: "(empty)"
@ -2617,7 +2617,7 @@ fa:
mail_self_notified: I want to be notified of changes that I make myself mail_self_notified: I want to be notified of changes that I make myself
status_user_and_brute_force: "%{user} and %{brute_force}" status_user_and_brute_force: "%{user} and %{brute_force}"
status_change: تغییر وضعیت status_change: تغییر وضعیت
unlock: Unlock unlock: باز کردن
unlock_and_reset_failed_logins: Unlock and reset failed logins unlock_and_reset_failed_logins: Unlock and reset failed logins
version_status_closed: closed version_status_closed: closed
version_status_locked: locked version_status_locked: locked

@ -63,7 +63,7 @@ cs:
source_code: Toggle Markdown source mode source_code: Toggle Markdown source mode
error_saving_failed: 'Saving the document failed with the following error: %{error}' error_saving_failed: 'Saving the document failed with the following error: %{error}'
mode: mode:
manual: Switch to Markdown source manual: Přepněte do Markdown zdroje
wysiwyg: Switch to WYSIWYG editor wysiwyg: Switch to WYSIWYG editor
macro: macro:
child_pages: child_pages:
@ -415,7 +415,7 @@ cs:
button_deactivate: Hide Gantt chart button_deactivate: Hide Gantt chart
cancel: Zrušit cancel: Zrušit
change: Změna v plánování change: Změna v plánování
due_date: Finish date due_date: Datum dokončení
empty: "(prázdný)" empty: "(prázdný)"
error: An error has occurred. error: An error has occurred.
errors: errors:
@ -566,7 +566,7 @@ cs:
createdAt: Vytvořeno createdAt: Vytvořeno
description: Popis description: Popis
date: Datum date: Datum
dueDate: Finish date dueDate: Datum dokončení
estimatedTime: Odhadovaný čas estimatedTime: Odhadovaný čas
spentTime: Strávený čas spentTime: Strávený čas
category: Kategorie category: Kategorie

@ -24,19 +24,19 @@ fa:
copied_successful: Sucessfully copied to clipboard! copied_successful: Sucessfully copied to clipboard!
button_add_watcher: Add watcher button_add_watcher: Add watcher
button_back_to_list_view: Back to list view button_back_to_list_view: Back to list view
button_cancel: Cancel button_cancel: لغو
button_close: Close button_close: Close
button_check_all: Check all button_check_all: بررسی همه
button_configure-form: Configure form button_configure-form: Configure form
button_confirm: Confirm button_confirm: Confirm
button_continue: Continue button_continue: Continue
button_copy: کپی button_copy: کپی
button_custom-fields: Custom fields button_custom-fields: Custom fields
button_delete: Delete button_delete: حذف
button_delete_watcher: حذف ناظر button_delete_watcher: حذف ناظر
button_details_view: نمای جزییات button_details_view: نمای جزییات
button_duplicate: Duplicate button_duplicate: نسخه برداری
button_edit: Edit button_edit: ویرایش
button_filter: Filter button_filter: Filter
button_list_view: نمای فهرستی button_list_view: نمای فهرستی
button_show_view: Fullscreen view button_show_view: Fullscreen view
@ -47,9 +47,9 @@ fa:
button_close_details: Close details view button_close_details: Close details view
button_open_fullscreen: Open fullscreen view button_open_fullscreen: Open fullscreen view
button_quote: نقل قول button_quote: نقل قول
button_save: Save button_save: ذخیره
button_settings: Settings button_settings: Settings
button_uncheck_all: Uncheck all button_uncheck_all: لغو انتخاب همه
button_update: به روز رسانی button_update: به روز رسانی
button_export-pdf: Download PDF button_export-pdf: Download PDF
button_export-atom: Download Atom button_export-atom: Download Atom
@ -167,7 +167,7 @@ fa:
label_closed_work_packages: closed label_closed_work_packages: closed
label_collapse: Collapse label_collapse: Collapse
label_collapsed: فروریخته label_collapsed: فروریخته
label_collapse_all: Collapse all label_collapse_all: بستن همه
label_comment: نظر label_comment: نظر
label_committed_at: "%{committed_revision_link} at %{date}" label_committed_at: "%{committed_revision_link} at %{date}"
label_committed_link: committed revision %{revision_identifier} label_committed_link: committed revision %{revision_identifier}
@ -178,7 +178,7 @@ fa:
label_equals: is label_equals: is
label_expand: گشودن label_expand: گشودن
label_expanded: بست یافته label_expanded: بست یافته
label_expand_all: Expand all label_expand_all: باز کردن همه
label_expand_project_menu: Expand project menu label_expand_project_menu: Expand project menu
label_export: خروجی label_export: خروجی
label_filename: فایل label_filename: فایل
@ -217,7 +217,7 @@ fa:
label_please_wait: Please wait label_please_wait: Please wait
label_visibility_settings: Visibility settings label_visibility_settings: Visibility settings
label_quote_comment: Quote this comment label_quote_comment: Quote this comment
label_reset: Reset label_reset: تنظیم مجدد
label_remove_columns: Remove selected columns label_remove_columns: Remove selected columns
label_save_as: ذخیره به عنوان label_save_as: ذخیره به عنوان
label_select_watcher: یک ناظر را انتخاب کنید... label_select_watcher: یک ناظر را انتخاب کنید...
@ -250,7 +250,7 @@ fa:
label_watcher_deleted_successfully: ناظر با موفقیت حذف شد! label_watcher_deleted_successfully: ناظر با موفقیت حذف شد!
label_work_package_details_you_are_here: شما بر روی زبانه ی %{tab} در %{type} label_work_package_details_you_are_here: شما بر روی زبانه ی %{tab} در %{type}
%{subject} هستید. %{subject} هستید.
label_unwatch: Unwatch label_unwatch: عدم دنبال
label_unwatch_work_package: عدم نمایش پکیچ وظیفه label_unwatch_work_package: عدم نمایش پکیچ وظیفه
label_uploaded_by: بارگزاری شده توسط label_uploaded_by: بارگزاری شده توسط
label_default_queries: Default views label_default_queries: Default views
@ -410,7 +410,7 @@ fa:
' '
button_activate: Show Gantt chart button_activate: Show Gantt chart
button_deactivate: Hide Gantt chart button_deactivate: Hide Gantt chart
cancel: Cancel cancel: لغو
change: Change in planning change: Change in planning
due_date: Finish date due_date: Finish date
empty: "(empty)" empty: "(empty)"
@ -441,7 +441,7 @@ fa:
really_close_dialog: Do you really want to close the dialog and lose the entered really_close_dialog: Do you really want to close the dialog and lose the entered
data? data?
responsible: مسئول responsible: مسئول
save: Save save: ذخیره
start_date: Start date start_date: Start date
tooManyProjects: بیشتر از %{count} از پروژه ها. لطفا فیلتر بهتری استفاده کنید! tooManyProjects: بیشتر از %{count} از پروژه ها. لطفا فیلتر بهتری استفاده کنید!
selection_mode: selection_mode:
@ -494,7 +494,7 @@ fa:
edit: Bulk edit edit: Bulk edit
copy: Bulk copy copy: Bulk copy
delete: Bulk delete delete: Bulk delete
button_clear: Clear button_clear: پاکسازی
comment_added: The comment was successfully added. comment_added: The comment was successfully added.
comment_send_failed: An error has occurred. Could not submit the comment. comment_send_failed: An error has occurred. Could not submit the comment.
comment_updated: The comment was successfully updated. comment_updated: The comment was successfully updated.
@ -534,7 +534,7 @@ fa:
header: New %{type} header: New %{type}
header_no_type: New work package (Type not yet set) header_no_type: New work package (Type not yet set)
header_with_parent: 'New %{type} (Child of %{parent_type} #%{id})' header_with_parent: 'New %{type} (Child of %{parent_type} #%{id})'
button: Create button: ایجاد
copy: copy:
title: Copy work package title: Copy work package
hierarchy: hierarchy:
@ -575,9 +575,9 @@ fa:
startDate: Start date startDate: Start date
status: Status status: Status
subject: Subject subject: Subject
title: Title title: عنوان
type: نوع type: نوع
updatedAt: Updated on updatedAt: به روز شده
versionName: نسخه versionName: نسخه
version: نسخه version: نسخه
default_queries: default_queries:
@ -682,12 +682,12 @@ fa:
display_hierarchy: Display hierarchy display_hierarchy: Display hierarchy
hide_hierarchy: Hide hierarchy hide_hierarchy: Hide hierarchy
hide_sums: Hide sums hide_sums: Hide sums
save: Save save: ذخیره
save_as: Save as ... save_as: Save as ...
export: Export ... export: Export ...
visibility_settings: Visibility settings ... visibility_settings: Visibility settings ...
page_settings: Rename view ... page_settings: Rename view ...
delete: Delete delete: حذف
filter: Filter filter: Filter
unselected_title: Work package unselected_title: Work package
search_query_label: Search saved views search_query_label: Search saved views
@ -697,10 +697,10 @@ fa:
label_settings: Rename view label_settings: Rename view
label_name: نام label_name: نام
label_delete_page: Delete current page label_delete_page: Delete current page
button_apply: Apply button_apply: درخواست
button_save: Save button_save: ذخیره
button_submit: Submit button_submit: ثبت کردن
button_cancel: Cancel button_cancel: لغو
form_submit: form_submit:
title: Confirm to continue title: Confirm to continue
text: Are you sure you want to perform this action? text: Are you sure you want to perform this action?
@ -723,8 +723,8 @@ fa:
button_edit: "%{attribute}: Edit" button_edit: "%{attribute}: Edit"
button_save: "%{attribute}: Save" button_save: "%{attribute}: Save"
button_cancel: "%{attribute}: Cancel" button_cancel: "%{attribute}: Cancel"
button_save_all: Save button_save_all: ذخیره
button_cancel_all: Cancel button_cancel_all: لغو
link_formatting_help: Text formatting help link_formatting_help: Text formatting help
btn_preview_enable: Preview btn_preview_enable: Preview
btn_preview_disable: غیر فعال کردن پیش نمایش btn_preview_disable: غیر فعال کردن پیش نمایش

@ -2255,8 +2255,7 @@ nl:
setting_welcome_on_homescreen: Toon het Welkom blok op thuisscherm setting_welcome_on_homescreen: Toon het Welkom blok op thuisscherm
setting_wiki_compression: Compressie van wiki geschiedenis setting_wiki_compression: Compressie van wiki geschiedenis
setting_work_package_group_assignment: Sta toewijzingen een groepen toe setting_work_package_group_assignment: Sta toewijzingen een groepen toe
setting_work_package_list_default_highlighting_mode: Default work package highlighting setting_work_package_list_default_highlighting_mode: Standaart werkpakket markeringsmode
mode
settings: settings:
general: Algemeen general: Algemeen
other: Overig other: Overig

@ -256,10 +256,12 @@ Retreive an individual query as identified by the id parameter. Then end point a
+ Parameters + Parameters
+ id (required, integer, `1`) ... Query id + id (required, integer, `1`) ... Query id
+ filters (optional, string, `[{ "assignee": { "operator": "=", "values": ["1", "5"] }" }]`) ... JSON specifying filter conditions. The filters provided as parameters are not applied to the query but are instead used to override the query's persisted filters. All filters also accepted by the work packages endpoint are accepted. + filters = `[{ "status_id": { "operator": "o", "values": null }}]` (optional, string, `[{ "assignee": { "operator": "=", "values": ["1", "5"] }" }]`) ... JSON specifying filter conditions.
The filters provided as parameters are not applied to the query but are instead used to override the query's persisted filters.
All filters also accepted by the work packages endpoint are accepted. If no filter is to be applied, the client should send an empty array (`[]`).
+ offset = `1` (optional, integer, `25`) ... Page number inside the queries' result collection of work packages. + offset = `1` (optional, integer, `25`) ... Page number inside the queries' result collection of work packages.
+ pageSize (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages. + pageSize = `DEPENDING ON CONFIGURATION` (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages.
+ sortBy (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. The sort criteria is applied to the querie's result collection of work packages overriding the query's persisted sort criteria. + sortBy = ["parent", "asc"] (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. The sort criteria is applied to the querie's result collection of work packages overriding the query's persisted sort criteria.
+ groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria. + groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria.
+ showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property. + showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property.
+ timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown. + timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown.
@ -541,10 +543,12 @@ Delete the query identified by the id parameter
Same as [viewing an existing, persisted Query](#queries-query-get) in its response, this resource returns an unpersisted query and by that allows to get the default query configuration. The client may also provide additional parameters which will modify the default query. Same as [viewing an existing, persisted Query](#queries-query-get) in its response, this resource returns an unpersisted query and by that allows to get the default query configuration. The client may also provide additional parameters which will modify the default query.
+ Parameters + Parameters
+ filters (optional, string, `[{ "assignee": { "operator": "=", "values": ["1", "5"] }" }]`) ... JSON specifying filter conditions. The filters provided as parameters are not applied to the query but are instead used to override the query's persisted filters. All filters also accepted by the work packages endpoint are accepted. + filters = `[{ "status_id": { "operator": "o", "values": null }}]` (optional, string, `[{ "assignee": { "operator": "=", "values": ["1", "5"] }" }]`) ... JSON specifying filter conditions.
The filters provided as parameters are not applied to the query but are instead used to override the query's persisted filters.
All filters also accepted by the work packages endpoint are accepted. If no filter is to be applied, the client should send an empty array (`[]`).
+ offset = `1` (optional, integer, `25`) ... Page number inside the queries' result collection of work packages. + offset = `1` (optional, integer, `25`) ... Page number inside the queries' result collection of work packages.
+ pageSize (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages. + pageSize = `DEPENDING ON CONFIGURATION` (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages.
+ sortBy (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. The sort criteria is applied to the querie's result collection of work packages overriding the query's persisted sort criteria. + sortBy = ["parent", "asc"] (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. The sort criteria is applied to the querie's result collection of work packages overriding the query's persisted sort criteria.
+ groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria. + groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria.
+ showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property. + showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property.
+ timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown. + timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown.
@ -704,10 +708,12 @@ Same as [viewing an existing, persisted Query](#queries-query-get) in its respon
+ Parameters + Parameters
+ id (required, integer, `1`) ... Id of the project the default query is requested for + id (required, integer, `1`) ... Id of the project the default query is requested for
+ filters (optional, string, `[{ "assignee": { "operator": "=", "values": ["1", "5"] }" }]`) ... JSON specifying filter conditions. The filters provided as parameters are not applied to the query but are instead used to override the query's persisted filters. All filters also accepted by the work packages endpoint are accepted. + filters = `[{ "status_id": { "operator": "o", "values": null }}]` (optional, string, `[{ "assignee": { "operator": "=", "values": ["1", "5"] }" }]`) ... JSON specifying filter conditions.
The filters provided as parameters are not applied to the query but are instead used to override the query's persisted filters.
All filters also accepted by the work packages endpoint are accepted. If no filter is to be applied, the client should send an empty array (`[]`).
+ offset = `1` (optional, integer, `25`) ... Page number inside the queries' result collection of work packages. + offset = `1` (optional, integer, `25`) ... Page number inside the queries' result collection of work packages.
+ pageSize (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages. + pageSize = `DEPENDING ON CONFIGURATION` (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages.
+ sortBy (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. The sort criteria is applied to the querie's result collection of work packages overriding the query's persisted sort criteria. + sortBy = ["parent", "asc"] (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. The sort criteria is applied to the querie's result collection of work packages overriding the query's persisted sort criteria.
+ groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria. + groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria.
+ showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property. + showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property.
+ timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown. + timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown.

@ -635,12 +635,12 @@ For more details and all possible responses see the general specification of [Fo
+ Parameters + Parameters
+ offset = `1` (optional, integer, `25`) ... Page number inside the requested collection. + offset = `1` (optional, integer, `25`) ... Page number inside the requested collection.
+ pageSize (optional, integer, `25`) ... Number of elements to display per page. + pageSize = DEPENDING ON CONFIGURATION (optional, integer, `25`) ... Number of elements to display per page.
+ filters (optional, string, `[{ "status_id": { "operator": "o", "values": null }" }]`) ... JSON specifying filter conditions. + filters = `[{ "status_id": { "operator": "o", "values": null }}]` (optional, string, `[{ "type_id": { "operator": "=", "values": ['1', '2'] }" }]`) ... JSON specifying filter conditions.
Accepts the same format as returned by the [queries](#queries) endpoint. Accepts the same format as returned by the [queries](#queries) endpoint. If no filter is to be applied, the client should send an empty array (`[]`).
+ sortBy (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. + sortBy = ["parent", "asc"] (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria.
Accepts the same format as returned by the [queries](#queries) endpoint. Accepts the same format as returned by the [queries](#queries) endpoint.
+ groupBy (optional, string, `status`) ... The column to group by. + groupBy (optional, string, `status`) ... The column to group by.
@ -821,12 +821,12 @@ A project link must be set when creating work packages through this route.
+ offset = `1` (optional, integer, `25`) ... Page number inside the requested collection. + offset = `1` (optional, integer, `25`) ... Page number inside the requested collection.
+ pageSize (optional, integer, `25`) ... Number of elements to display per page. + pageSize = DEPENDING ON CONFIGURATION (optional, integer, `25`) ... Number of elements to display per page.
+ filters (optional, string, `[{ "status_id": { "operator": "o", "values": null }" }]`) ... JSON specifying filter conditions. + filters = `[{ "status_id": { "operator": "o", "values": null }}]` (optional, string, `[{ "type_id": { "operator": "=", "values": ['1', '2'] }" }]`) ... JSON specifying filter conditions.
Accepts the same format as returned by the [queries](#queries) endpoint. Accepts the same format as returned by the [queries](#queries) endpoint. If no filter is to be applied, the client should send an empty array (`[]`).
+ sortBy (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria. + sortBy = ["parent", "asc"] (optional, string, `[["status", "asc"]]`) ... JSON specifying sort criteria.
Accepts the same format as returned by the [queries](#queries) endpoint. Accepts the same format as returned by the [queries](#queries) endpoint.
+ groupBy (optional, string, `status`) ... The column to group by. + groupBy (optional, string, `status`) ... The column to group by.

@ -74,6 +74,7 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy {
titleEditingEnabled:boolean; titleEditingEnabled:boolean;
currentQuery:QueryResource; currentQuery:QueryResource;
private removeTransitionSubscription:Function;
constructor(readonly states:States, constructor(readonly states:States,
@ -115,7 +116,7 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy {
this.setupRefreshObserver(); this.setupRefreshObserver();
// Listen for param changes // Listen for param changes
this.$transitions.onSuccess({}, (transition):any => { this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => {
let options = transition.options(); let options = transition.options();
// Avoid performing any changes when we're going to reload // Avoid performing any changes when we're going to reload
@ -135,6 +136,7 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy {
} }
ngOnDestroy():void { ngOnDestroy():void {
this.removeTransitionSubscription();
this.wpTableRefresh.clear('Table controller scope destroyed.'); this.wpTableRefresh.clear('Table controller scope destroyed.');
} }

@ -46,9 +46,29 @@ import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper
import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service'; import {NotificationsService} from 'core-app/modules/common/notifications/notifications.service';
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {input} from "reactivestates";
import {catchError, distinctUntilChanged, map, share, shareReplay, switchMap, take} from "rxjs/operators";
import {from, Observable} from "rxjs";
export interface QueryDefinition {
queryParams:{ query_id?:number, query_props?:string };
projectIdentifier?:string;
}
@Injectable() @Injectable()
export class WorkPackagesListService { export class WorkPackagesListService {
// We remember the query requests coming in so we can ensure only the latest request is being tended to
private queryRequests = input<QueryDefinition>();
// This mapped observable requests the latest query automatically.
private queryLoading = this.queryRequests
.values$()
.pipe(
switchMap((q:QueryDefinition) => this.handleQueryRequest(q.queryParams, q.projectIdentifier)),
shareReplay(1)
);
private queryChanges = new BehaviorSubject<string>(''); private queryChanges = new BehaviorSubject<string>('');
public queryChanges$ = this.queryChanges.asObservable(); public queryChanges$ = this.queryChanges.asObservable();
@ -68,24 +88,46 @@ export class WorkPackagesListService {
protected wpListInvalidQueryService:WorkPackagesListInvalidQueryService) { protected wpListInvalidQueryService:WorkPackagesListInvalidQueryService) {
} }
/** private handleQueryRequest(queryParams:{ query_id?:number, query_props?:string }, projectIdentifier ?:string):Observable<QueryResource> {
* Load a query.
* The query is either a persisted query, identified by the query_id parameter, or the default query. Both will be modified by the parameters in the query_props parameter.
*/
public fromQueryParams(queryParams:{ query_id?:number, query_props?:string }, projectIdentifier ?:string):Promise<QueryResource> {
const decodedProps = this.getCurrentQueryProps(queryParams); const decodedProps = this.getCurrentQueryProps(queryParams);
const queryData = this.UrlParamsHelper.buildV3GetQueryFromJsonParams(decodedProps); const queryData = this.UrlParamsHelper.buildV3GetQueryFromJsonParams(decodedProps);
const wpListPromise = this.QueryDm.find(queryData, queryParams.query_id, projectIdentifier); const stream = this.QueryDm.stream(queryData, queryParams.query_id, projectIdentifier);
const promise = this.updateStatesFromQueryOnPromise(wpListPromise);
promise return stream.pipe(
.catch((error) => { map((query:QueryResource) => {
const queryProps = this.UrlParamsHelper.buildV3GetQueryFromJsonParams(decodedProps);
return this.handleQueryLoadingError(error, queryProps, queryParams.query_id, projectIdentifier); // Project the loaded query into the table states and confirm the query is fully loaded
}); this.tableState.ready.doAndTransition('Query loaded', () => {
this.wpStatesInitialization.initialize(query, query.results);
return this.tableState.tableRendering.onQueryUpdated.valuesPromise();
});
return this.conditionallyLoadForm(promise); // load the form if needed
this.conditionallyLoadForm(query);
return query;
}),
catchError((error) => {
// Load a default query
const queryProps = this.UrlParamsHelper.buildV3GetQueryFromJsonParams(decodedProps);
return from(this.handleQueryLoadingError(error, queryProps, queryParams.query_id, projectIdentifier));
})
)
}
/**
* Load a query.
* The query is either a persisted query, identified by the query_id parameter, or the default query. Both will be modified by the parameters in the query_props parameter.
*/
public fromQueryParams(queryParams:{ query_id?:number, query_props?:string }, projectIdentifier ?:string):Observable<QueryResource> {
this.queryRequests.clear();
this.queryRequests.putValue({ queryParams: queryParams, projectIdentifier: projectIdentifier });
return this
.queryLoading
.pipe(
take(1)
);
} }
/** /**
@ -103,7 +145,7 @@ export class WorkPackagesListService {
* Load the default query. * Load the default query.
*/ */
public loadDefaultQuery(projectIdentifier ?:string):Promise<QueryResource> { public loadDefaultQuery(projectIdentifier ?:string):Promise<QueryResource> {
return this.fromQueryParams({}, projectIdentifier); return this.fromQueryParams({}, projectIdentifier).toPromise();
} }
/** /**
@ -115,16 +157,17 @@ export class WorkPackagesListService {
let wpListPromise = this.QueryDm.reload(query, pagination); let wpListPromise = this.QueryDm.reload(query, pagination);
let promise = this.updateStatesFromQueryOnPromise(wpListPromise); return this.updateStatesFromQueryOnPromise(wpListPromise)
.then((query:QueryResource) => {
promise this.conditionallyLoadForm(query);
return query;
})
.catch((error) => { .catch((error) => {
let projectIdentifier = query.project && query.project.id; let projectIdentifier = query.project && query.project.id;
return this.handleQueryLoadingError(error, {}, query.id, projectIdentifier); return this.handleQueryLoadingError(error, {}, query.id, projectIdentifier);
}); });
return this.conditionallyLoadForm(promise);
} }
/** /**
@ -164,8 +207,10 @@ export class WorkPackagesListService {
public loadCurrentQueryFromParams(projectIdentifier?:string) { public loadCurrentQueryFromParams(projectIdentifier?:string) {
this.wpListChecksumService.clear(); this.wpListChecksumService.clear();
this.loadingIndicator.table.promise = this.loadingIndicator.table.promise =
this.fromQueryParams(this.$state.params as any, projectIdentifier).then(() => { this.fromQueryParams(this.$state.params as any, projectIdentifier)
return this.tableState.rendered.valuesPromise(); .toPromise()
.then(() => {
return this.tableState.rendered.valuesPromise();
}); });
} }
@ -269,19 +314,12 @@ export class WorkPackagesListService {
return this.wpTablePagination.paginationObject; return this.wpTablePagination.paginationObject;
} }
private conditionallyLoadForm(promise:Promise<QueryResource>):Promise<QueryResource> { private conditionallyLoadForm(query:QueryResource):void {
promise.then(query => { let currentForm = this.states.query.form.value;
let currentForm = this.states.query.form.value;
if (!currentForm || query.$links.update.$href !== currentForm.$href) {
setTimeout(() => this.loadForm(query), 0);
}
return query; if (!currentForm || query.$links.update.$href !== currentForm.$href) {
}); setTimeout(() => this.loadForm(query), 0);
}
return promise;
} }
private updateStatesFromQueryOnPromise(promise:Promise<QueryResource>):Promise<QueryResource> { private updateStatesFromQueryOnPromise(promise:Promise<QueryResource>):Promise<QueryResource> {
@ -314,7 +352,7 @@ export class WorkPackagesListService {
return this.states.query.resource.value!; return this.states.query.resource.value!;
} }
private handleQueryLoadingError(error:ErrorResource, queryProps:any, queryId?:number, projectIdentifier?:string) { private handleQueryLoadingError(error:ErrorResource, queryProps:any, queryId?:number, projectIdentifier?:string):Promise<QueryResource> {
this.NotificationsService.addError(this.I18n.t('js.work_packages.faulty_query.description'), error.message); this.NotificationsService.addError(this.I18n.t('js.work_packages.faulty_query.description'), error.message);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

@ -100,12 +100,12 @@ export class WorkPackageStaticQueriesService {
{ {
identifier: 'created_by_me', identifier: 'created_by_me',
label: this.text.created_by_me, label: this.text.created_by_me,
query_props: "{%22c%22:[%22id%22,%22subject%22,%22type%22,%22status%22,%22assignee%22,%22updatedAt%22],%22tzl%22:%22days%22,%22hi%22:false,%22g%22:%22%22,%22t%22:%22updatedAt:desc,parent:asc%22,%22f%22:[{%22n%22:%22status%22,%22o%22:%22o%22,%22v%22:[]},{%22n%22:%22author%22,%22o%22:%22=%22,%22v%22:[%22me%22]}],%22pa%22:1,%22pp%22:20}" query_props: '{"c":["id","subject","type","status","assignee","updatedAt"],"tzl":"days","hi":false,"g":"","t":"updatedAt:desc,parent:asc","f":[{"n":"status","o":"o","v":[]},{"n":"author","o":"=","v":["me"]}],"pa":1,"pp":20}'
}, },
{ {
identifier: 'assigned_to_me', identifier: 'assigned_to_me',
label: this.text.assigned_to_me, label: this.text.assigned_to_me,
query_props: '{%22c%22:[%22id%22,%22subject%22,%22type%22,%22status%22,%20%22author%22,%20%22updatedAt%22],%22t%22:%22updatedAt:desc,parent:asc%22,%22f%22:[{%22n%22:%22status%22,%22o%22:%22o%22,%22v%22:[]},{%22n%22:%22assignee%22,%22o%22:%22=%22,%22v%22:[%22me%22]}]}' query_props: '{"c":["id","subject","type","status", "author", "updatedAt"],"t":"updatedAt:desc,parent:asc","f":[{"n":"status","o":"o","v":[]},{"n":"assignee","o":"=","v":["me"]}]}'
} }
]); ]);
} }

@ -49,6 +49,8 @@ export class LoadingIndicator {
} }
public start() { public start() {
// If we're currently having an active indicator, remove that one
this.stop();
this.indicator.prepend(this.template); this.indicator.prepend(this.template);
} }

@ -36,6 +36,7 @@ import {ApiV3FilterBuilder} from 'core-app/components/api/api-v3/api-v3-filter-b
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper'; import {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper';
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service'; import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service';
import {Observable} from "rxjs";
export interface PaginationObject { export interface PaginationObject {
pageSize:number; pageSize:number;
@ -50,7 +51,13 @@ export class QueryDmService {
protected PayloadDm:PayloadDmService) { protected PayloadDm:PayloadDmService) {
} }
public find(queryData:Object, queryId?:number, projectIdentifier?:string):Promise<QueryResource> { /**
* Stream the response for the given query request
* @param queryData
* @param queryId
* @param projectIdentifier
*/
public stream(queryData:Object, queryId?:number, projectIdentifier?:string):Observable<QueryResource> {
let path:string; let path:string;
if (queryId) { if (queryId) {
@ -60,8 +67,11 @@ export class QueryDmService {
} }
return this.halResourceService return this.halResourceService
.get<QueryResource>(path, queryData) .get<QueryResource>(path, queryData);
.toPromise(); }
public find(queryData:Object, queryId?:number, projectIdentifier?:string):Promise<QueryResource> {
return this.stream(queryData, queryId, projectIdentifier).toPromise();
} }
public findDefault(queryData:Object, projectIdentifier?:string):Promise<QueryResource> { public findDefault(queryData:Object, projectIdentifier?:string):Promise<QueryResource> {

@ -98,6 +98,7 @@ module OpenProject::TextFormatting
) )
|\.\z # Allow matching when string ends with . |\.\z # Allow matching when string ends with .
|, # or with , |, # or with ,
|\) # or with )
|[[:space:]] |[[:space:]]
|\] |\]
|< |<

@ -231,6 +231,12 @@ describe OpenProject::TextFormatting,
it { is_expected.to be_html_eql("<p>#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.</p>") } it { is_expected.to be_html_eql("<p>#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.</p>") }
end end
context 'Plain issue link with braces' do
subject { format_text("foo (bar ##{issue.id})") }
it { is_expected.to be_html_eql("<p>foo (bar #{issue_link})</p>") }
end
context 'Plain issue link to non-existing element' do context 'Plain issue link to non-existing element' do
subject { format_text('#0123456789') } subject { format_text('#0123456789') }

@ -156,51 +156,6 @@ describe ::Type, type: :model do
end end
end end
describe "#validate_attribute_groups" do
it 'raises an exception for invalid structure' do
# Exampel for invalid structure:
type.attribute_groups = ['foo']
expect { type.save }.to raise_exception(NoMethodError)
# Exampel for invalid structure:
expect { type.attribute_groups = [[]] }.to raise_exception(NoMethodError)
# Exampel for invalid group name:
type.attribute_groups = [['', ['date']]]
expect(type).not_to be_valid
end
it 'fails for duplicate group names' do
type.attribute_groups = [['foo', ['date']], ['foo', ['date']]]
expect(type).not_to be_valid
end
it 'passes validations for known attributes' do
type.attribute_groups = [['foo', ['date']]]
expect(type).to be_valid
end
it 'passes validation for defaults' do
expect(type).to be_valid
end
it 'passes validation for reset' do
# A reset is to save an empty Array
type.attribute_groups = []
expect(type).to be_valid
end
context 'with an invalid query' do
let(:query) { FactoryBot.build(:global_query, name: '') }
before do
type.attribute_groups = [['some name', [query]]]
end
it 'is invalid' do
expect(type).to be_invalid
end
end
end
describe 'custom fields' do describe 'custom fields' do
let!(:custom_field) do let!(:custom_field) do
FactoryBot.create( FactoryBot.create(

@ -32,10 +32,10 @@ require 'services/shared_type_service'
describe CreateTypeService do describe CreateTypeService do
let(:type) { instance.type } let(:type) { instance.type }
let(:user) { FactoryBot.build_stubbed(:user) } let(:user) { FactoryBot.build_stubbed(:admin) }
let(:instance) { described_class.new(user) } let(:instance) { described_class.new(user) }
let(:service_call) { instance.call(params, {}) } let(:service_call) { instance.call({ name: 'foo' }.merge(params), {}) }
it_behaves_like 'type service' it_behaves_like 'type service'
end end

@ -33,7 +33,7 @@ shared_examples_for 'type service' do
describe '#call' do describe '#call' do
before do before do
expect(type) allow(type)
.to receive(:save) .to receive(:save)
.and_return(success) .and_return(success)
end end
@ -127,7 +127,7 @@ shared_examples_for 'type service' do
['group1', query_params] ['group1', query_params]
end end
let(:params) { { attribute_groups: [query_group_params] } } let(:params) { { attribute_groups: [query_group_params] } }
let(:query) { FactoryBot.build_stubbed(:query) } let(:query) { FactoryBot.create(:query) }
let(:service_result) { ServiceResult.new(success: true, result: query) } let(:service_result) { ServiceResult.new(success: true, result: query) }
before do before do
@ -155,13 +155,15 @@ shared_examples_for 'type service' do
.to eql query .to eql query
expect(query.filters.length) expect(query.filters.length)
.to eql 1 .to eql 2
expect(query.filters[0].name) expect(query.filters[0].name)
.to eql :status_id
expect(query.filters[1].name)
.to eql :parent .to eql :parent
expect(query.filters[0].operator) expect(query.filters[1].operator)
.to eql '=' .to eql '='
expect(query.filters[0].values) expect(query.filters[1].values)
.to eql [::Queries::Filters::TemplatedValue::KEY] .to eql [::Queries::Filters::TemplatedValue::KEY]
end end
@ -180,6 +182,7 @@ shared_examples_for 'type service' do
context 'on failure' do context 'on failure' do
let(:success) { false } let(:success) { false }
let(:params) { { name: nil } }
subject { service_call } subject { service_call }
@ -188,12 +191,8 @@ shared_examples_for 'type service' do
end end
it 'returns the errors of the type' do it 'returns the errors of the type' do
type_errors = 'all the errors' type.name = nil
allow(type) expect(subject.errors.symbols_for(:name)).to include :blank
.to receive(:errors)
.and_return(type_errors)
expect(subject.errors).to eql type_errors
end end
end end
end end

@ -32,10 +32,58 @@ require 'services/shared_type_service'
describe UpdateTypeService do describe UpdateTypeService do
let(:type) { FactoryBot.build_stubbed(:type) } let(:type) { FactoryBot.build_stubbed(:type) }
let(:user) { FactoryBot.build_stubbed(:user) } let(:user) { FactoryBot.build_stubbed(:admin) }
let(:instance) { described_class.new(type, user) } let(:instance) { described_class.new(type, user) }
let(:service_call) { instance.call(params) } let(:service_call) { instance.call(params) }
it_behaves_like 'type service' it_behaves_like 'type service'
describe "#validate_attribute_groups" do
let(:params) { { name: 'blubs blubs' } }
it 'raises an exception for invalid structure' do
# Example for invalid structure:
result = instance.call(attribute_groups: ['foo'])
expect(result.success?).to be_falsey
# Example for invalid structure:
result = instance.call(attribute_groups: [[]])
expect(result.success?).to be_falsey
# Example for invalid group name:
result = instance.call(attribute_groups: [['', ['date']]])
expect(result.success?).to be_falsey
end
it 'fails for duplicate group names' do
result = instance.call(attribute_groups: [['foo', ['date']], ['foo', ['date']]])
expect(result.success?).to be_falsey
end
it 'passes validations for known attributes' do
expect(type).to receive(:save).and_return(true)
result = instance.call(attribute_groups: [['foo', ['date']]])
expect(result.success?).to be_truthy
end
it 'passes validation for defaults' do
expect(type).to be_valid
end
it 'passes validation for reset' do
# A reset is to save an empty Array
expect(type).to receive(:save).and_return(true)
result = instance.call(attribute_groups: [])
expect(result.success?).to be_truthy
expect(type).to be_valid
end
context 'with an invalid query' do
let(:query) { FactoryBot.build(:global_query, name: '') }
let(:params) { { attribute_groups: [['some name', [query]]] } }
it 'is invalid' do
expect(service_call.success?).to be_falsey
end
end
end
end end

Loading…
Cancel
Save