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
after_save :unset_attribute_groups_objects
after_destroy :remove_attribute_groups_queries
validate :validate_attribute_group_names
validate :validate_attribute_groups
serialize :attribute_groups, Array
attr_accessor :attribute_groups_objects
# Mapping from AR attribute name to a default group
# May be extended by plugins
@ -135,9 +135,6 @@ module Type::AttributeGroups
self.attribute_groups_objects = nil
end
protected
attr_accessor :attribute_groups_objects
private
@ -167,44 +164,6 @@ module Type::AttributeGroups
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.

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

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

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

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

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

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

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

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

@ -256,10 +256,12 @@ Retreive an individual query as identified by the id parameter. Then end point a
+ Parameters
+ 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.
+ pageSize (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.
+ pageSize = `DEPENDING ON CONFIGURATION` (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages.
+ 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.
+ 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.
@ -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.
+ 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.
+ pageSize (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.
+ pageSize = `DEPENDING ON CONFIGURATION` (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages.
+ 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.
+ 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.
@ -704,10 +708,12 @@ Same as [viewing an existing, persisted Query](#queries-query-get) in its respon
+ Parameters
+ 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.
+ pageSize (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.
+ pageSize = `DEPENDING ON CONFIGURATION` (optional, integer, `25`) ... Number of elements to display per page for the queries' result collection of work packages.
+ 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.
+ 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.

@ -635,12 +635,12 @@ For more details and all possible responses see the general specification of [Fo
+ Parameters
+ 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.
Accepts the same format as returned by the [queries](#queries) endpoint.
+ 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. 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.
+ 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.
+ 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.
Accepts the same format as returned by the [queries](#queries) endpoint.
+ 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. 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.
+ groupBy (optional, string, `status`) ... The column to group by.

@ -74,6 +74,7 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy {
titleEditingEnabled:boolean;
currentQuery:QueryResource;
private removeTransitionSubscription:Function;
constructor(readonly states:States,
@ -115,7 +116,7 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy {
this.setupRefreshObserver();
// Listen for param changes
this.$transitions.onSuccess({}, (transition):any => {
this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => {
let options = transition.options();
// Avoid performing any changes when we're going to reload
@ -135,6 +136,7 @@ export class WorkPackagesListComponent implements OnInit, OnDestroy {
}
ngOnDestroy():void {
this.removeTransitionSubscription();
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 {I18nService} from "core-app/modules/common/i18n/i18n.service";
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()
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>('');
public queryChanges$ = this.queryChanges.asObservable();
@ -68,24 +88,46 @@ export class WorkPackagesListService {
protected wpListInvalidQueryService:WorkPackagesListInvalidQueryService) {
}
/**
* 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> {
private handleQueryRequest(queryParams:{ query_id?:number, query_props?:string }, projectIdentifier ?:string):Observable<QueryResource> {
const decodedProps = this.getCurrentQueryProps(queryParams);
const queryData = this.UrlParamsHelper.buildV3GetQueryFromJsonParams(decodedProps);
const wpListPromise = this.QueryDm.find(queryData, queryParams.query_id, projectIdentifier);
const promise = this.updateStatesFromQueryOnPromise(wpListPromise);
const stream = this.QueryDm.stream(queryData, queryParams.query_id, projectIdentifier);
promise
.catch((error) => {
const queryProps = this.UrlParamsHelper.buildV3GetQueryFromJsonParams(decodedProps);
return stream.pipe(
map((query:QueryResource) => {
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.
*/
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 promise = this.updateStatesFromQueryOnPromise(wpListPromise);
promise
return this.updateStatesFromQueryOnPromise(wpListPromise)
.then((query:QueryResource) => {
this.conditionallyLoadForm(query);
return query;
})
.catch((error) => {
let projectIdentifier = query.project && query.project.id;
return this.handleQueryLoadingError(error, {}, query.id, projectIdentifier);
});
return this.conditionallyLoadForm(promise);
}
/**
@ -164,8 +207,10 @@ export class WorkPackagesListService {
public loadCurrentQueryFromParams(projectIdentifier?:string) {
this.wpListChecksumService.clear();
this.loadingIndicator.table.promise =
this.fromQueryParams(this.$state.params as any, projectIdentifier).then(() => {
return this.tableState.rendered.valuesPromise();
this.fromQueryParams(this.$state.params as any, projectIdentifier)
.toPromise()
.then(() => {
return this.tableState.rendered.valuesPromise();
});
}
@ -269,19 +314,12 @@ export class WorkPackagesListService {
return this.wpTablePagination.paginationObject;
}
private conditionallyLoadForm(promise:Promise<QueryResource>):Promise<QueryResource> {
promise.then(query => {
let currentForm = this.states.query.form.value;
if (!currentForm || query.$links.update.$href !== currentForm.$href) {
setTimeout(() => this.loadForm(query), 0);
}
private conditionallyLoadForm(query:QueryResource):void {
let currentForm = this.states.query.form.value;
return query;
});
return promise;
if (!currentForm || query.$links.update.$href !== currentForm.$href) {
setTimeout(() => this.loadForm(query), 0);
}
}
private updateStatesFromQueryOnPromise(promise:Promise<QueryResource>):Promise<QueryResource> {
@ -314,7 +352,7 @@ export class WorkPackagesListService {
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);
return new Promise((resolve, reject) => {

@ -100,12 +100,12 @@ export class WorkPackageStaticQueriesService {
{
identifier: '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',
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() {
// If we're currently having an active indicator, remove that one
this.stop();
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 {UrlParamsHelperService} from 'core-components/wp-query/url-params-helper';
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service';
import {Observable} from "rxjs";
export interface PaginationObject {
pageSize:number;
@ -50,7 +51,13 @@ export class QueryDmService {
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;
if (queryId) {
@ -60,8 +67,11 @@ export class QueryDmService {
}
return this.halResourceService
.get<QueryResource>(path, queryData)
.toPromise();
.get<QueryResource>(path, queryData);
}
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> {

@ -98,6 +98,7 @@ module OpenProject::TextFormatting
)
|\.\z # Allow matching when string ends with .
|, # or with ,
|\) # or with )
|[[: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>") }
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
subject { format_text('#0123456789') }

@ -156,51 +156,6 @@ describe ::Type, type: :model do
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
let!(:custom_field) do
FactoryBot.create(

@ -32,10 +32,10 @@ require 'services/shared_type_service'
describe CreateTypeService do
let(:type) { instance.type }
let(:user) { FactoryBot.build_stubbed(:user) }
let(:user) { FactoryBot.build_stubbed(:admin) }
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'
end

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

@ -32,10 +32,58 @@ require 'services/shared_type_service'
describe UpdateTypeService do
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(:service_call) { instance.call(params) }
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

Loading…
Cancel
Save