Merge branch dev into 45001-component-to-show-the-list-of-non-working-days-of-year

45001-component-to-show-the-list-of-non-working-days-of-year
bsatarnejad 2 years ago
commit 8aa7ec0250
  1. 7
      app/contracts/base_contract.rb
  2. 7
      app/controllers/enterprises_controller.rb
  3. 4
      app/models/activities/base_activity_provider.rb
  4. 8
      app/models/journal.rb
  5. 31
      app/models/journal/project_journal.rb
  6. 2
      app/models/project.rb
  7. 40
      app/models/projects/journalized.rb
  8. 4
      app/models/wiki_content.rb
  9. 5
      app/models/work_package.rb
  10. 5
      app/models/work_packages/scopes/relatable.rb
  11. 3
      app/services/journals/create_service.rb
  12. 2
      config/locales/en.yml
  13. 44
      db/migrate/20221129074635_remove_activity_type_from_journals_table.rb
  14. 15
      db/migrate/20221130150352_create_project_journals.rb
  15. 16
      db/migrate/20221201140825_add_non_null_constraint_on_projects_identifier.rb
  16. 97
      db/migrate/20221202130039_fill_project_journals_with_existing_data.rb
  17. 14
      docs/installation-and-operations/installation/packaged/README.md
  18. 1
      frontend/.gitignore
  19. 4
      frontend/angular.json
  20. 37010
      frontend/package-lock.json
  21. 53
      frontend/package.json
  22. 4
      frontend/src/app/core/augmenting/dynamic-scripts/backlogs/model.js
  23. 108
      frontend/src/app/core/augmenting/dynamic-scripts/members_form.js
  24. 6
      frontend/src/app/core/augmenting/dynamic-scripts/project.js
  25. 21
      frontend/src/app/core/augmenting/dynamic-scripts/two_factor_authentication.ts
  26. 4
      frontend/src/app/core/forms/forms.service.spec.ts
  27. 10
      frontend/src/app/core/forms/forms.service.ts
  28. 12
      frontend/src/app/core/setup/globals/components/admin/backup.component.html
  29. 5
      frontend/src/app/core/setup/globals/components/admin/backup.component.ts
  30. 11
      frontend/src/app/core/setup/globals/global-listeners/danger-zone-validation.ts
  31. 36
      frontend/src/app/core/setup/globals/onboarding/helpers.ts
  32. 9
      frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts
  33. 10
      frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts
  34. 4
      frontend/src/app/features/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.ts
  35. 14
      frontend/src/app/features/enterprise/enterprise-modal/enterprise-trial.modal.ts
  36. 4
      frontend/src/app/features/enterprise/enterprise-trial.service.ts
  37. 4
      frontend/src/app/features/invite-user-modal/principal/principal-search.component.ts
  38. 14
      frontend/src/app/features/invite-user-modal/principal/principal.component.ts
  39. 10
      frontend/src/app/features/invite-user-modal/project-selection/project-selection.component.ts
  40. 4
      frontend/src/app/features/invite-user-modal/role/role-search.component.ts
  41. 6
      frontend/src/app/features/overview/openproject-overview.module.ts
  42. 6
      frontend/src/app/features/projects/components/new-project/new-project.component.ts
  43. 8
      frontend/src/app/features/reporting/reporting-page/functionality/reporting_engine/controls.js
  44. 2
      frontend/src/app/features/reporting/reporting-page/functionality/reporting_engine/filters.js
  45. 4
      frontend/src/app/features/user-preferences/notifications-settings/inline-create/notification-setting-inline-create.component.ts
  46. 68
      frontend/src/app/features/user-preferences/notifications-settings/page/notifications-settings-page.component.ts
  47. 28
      frontend/src/app/features/user-preferences/notifications-settings/table/notification-settings-table.component.ts
  48. 6
      frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.ts
  49. 6
      frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.ts
  50. 10
      frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.ts
  51. 6
      frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.ts
  52. 20
      frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.ts
  53. 12
      frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.ts
  54. 1
      frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts
  55. 1
      frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service.ts
  56. 30
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component.spec.ts
  57. 19
      frontend/src/app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component.ts
  58. 4
      frontend/src/app/shared/components/dynamic-forms/services/dynamic-form/dynamic-form.service.spec.ts
  59. 8
      frontend/src/app/shared/components/dynamic-forms/services/dynamic-form/dynamic-form.service.ts
  60. 6
      frontend/src/app/shared/components/dynamic-forms/spec/helpers.ts
  61. 6
      frontend/src/app/shared/components/fields/changeset/resource-changeset.ts
  62. 1
      frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts
  63. 13
      frontend/src/app/shared/components/op-context-menu/op-context-menu.service.ts
  64. 1
      frontend/src/app/shared/directives/a11y/keyboard-shortcut.service.ts
  65. 7
      frontend/src/app/shared/helpers/set-click-position/set-click-position.ts
  66. 20
      frontend/src/app/spot/components/drop-modal/drop-modal.component.ts
  67. 6
      frontend/src/app/spot/components/form-field/form-binding.directive.ts
  68. 8
      frontend/src/app/spot/components/form-field/stories/FormFieldErrorSlot.example.ts
  69. 7
      frontend/src/app/spot/styles/sass/components/drop-modal.sass
  70. 10
      frontend/src/global_styles/openproject/_variables.scss
  71. 14
      frontend/src/polyfills.ts
  72. 6
      frontend/src/test.ts
  73. 5127
      frontend/src/vendor/enjoyhint.js
  74. 1
      frontend/src/vendor/qrcode-min.js
  75. 8
      lib/api/decorators/schema_representer.rb
  76. 17
      lib/api/v3/work_packages/schema/base_work_package_schema.rb
  77. 4
      lib/api/v3/work_packages/schema/work_package_sums_schema.rb
  78. 48
      lib/open_project/patches/delayed_job_adapter.rb
  79. 6
      lib_static/plugins/acts_as_journalized/lib/acts/journalized/creation.rb
  80. 6
      modules/backlogs/spec/api/work_packages/schema/specific_work_package_schema_spec.rb
  81. 2
      modules/backlogs/spec/api/work_packages/work_package_schema_representer_spec.rb
  82. 22
      modules/backlogs/spec/features/stories_in_backlog_spec.rb
  83. 29
      modules/boards/spec/features/boards_sorting_spec.rb
  84. 5
      modules/budgets/app/models/budget.rb
  85. 16
      modules/costs/spec/lib/api/v3/time_entries/schemas/time_entry_schema_representer_spec.rb
  86. 1
      modules/meeting/spec/factories/meeting_journal_factory.rb
  87. 10
      modules/reporting/spec/features/update_cost_report_spec.rb
  88. 2
      spec/controllers/work_packages/bulk_controller_spec.rb
  89. 4
      spec/factories/journal_factory.rb
  90. 23
      spec/lib/api/v3/memberships/schemas/membership_schema_representer_spec.rb
  91. 2
      spec/lib/api/v3/utilities/custom_field_injector_spec.rb
  92. 6
      spec/lib/api/v3/versions/schemas/version_schema_representer_spec.rb
  93. 44
      spec/lib/api/v3/work_packages/schema/specific_work_package_schema_spec.rb
  94. 4
      spec/lib/api/v3/work_packages/schema/typed_work_package_schema_spec.rb
  95. 26
      spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb
  96. 99
      spec/models/principals/scopes/visible_spec.rb
  97. 223
      spec/models/projects/project_acts_as_journalized_spec.rb
  98. 2
      spec/models/wiki_page_spec.rb
  99. 46
      spec/models/work_package/work_package_acts_as_journalized_spec.rb
  100. 13
      spec/models/work_packages/scopes/relatable_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -139,12 +139,7 @@ class BaseContract < Disposable::Twin
end
def writable?(attribute)
property_name = ::API::Utilities::PropertyNameConverter.to_ar_name(
attribute,
context: model,
collapse_cf_name: false
)
writable_attributes.include?(property_name)
writable_attributes.include?(attribute.to_s)
end
def valid?(*_args)

@ -36,13 +36,12 @@ class EnterprisesController < ApplicationController
before_action :require_admin
before_action :check_user_limit, only: [:show]
before_action :check_domain, only: [:show]
before_action :render_gon
def show
@current_token = EnterpriseToken.current
@token = @current_token || EnterpriseToken.new
helpers.write_augur_to_gon
if !@current_token.present?
helpers.write_trial_key_to_gon
end
@ -95,6 +94,10 @@ class EnterprisesController < ApplicationController
private
def render_gon
helpers.write_augur_to_gon
end
def default_breadcrumb
t(:label_enterprise_edition)
end

@ -122,9 +122,7 @@ class Activities::BaseActivityProvider
end
def activitied_type
activity_type = self.class.name
class_name = activity_type.demodulize
class_name = self.class.name.demodulize
class_name.gsub('ActivityProvider', '').constantize
end

@ -28,6 +28,7 @@
class Journal < ApplicationRecord
self.table_name = 'journals'
self.ignored_columns += ['activity_type']
include ::JournalChanges
include ::JournalFormatter
@ -55,6 +56,9 @@ class Journal < ApplicationRecord
# logs like the history on issue#show
scope :changing, -> { where(['version > 1']) }
scope :for_wiki_content, -> { where(journable_type: "WikiContent") }
scope :for_work_package, -> { where(journable_type: "WorkPackage") }
# In conjunction with the included Comparable module, allows comparison of journal records
# based on their corresponding version numbers, creation timestamps and IDs.
def <=>(other)
@ -91,11 +95,11 @@ class Journal < ApplicationRecord
end
def new_value_for(prop)
details[prop].last if details.keys.include? prop
details[prop].last if details.key? prop
end
def old_value_for(prop)
details[prop].first if details.keys.include? prop
details[prop].first if details.key? prop
end
def previous

@ -0,0 +1,31 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Journal::ProjectJournal < Journal::BaseJournal
self.table_name = 'project_journals'
end

@ -96,6 +96,8 @@ class Project < ApplicationRecord
project_key: 'id',
permission: nil
include Projects::Journalized
# Necessary for acts_as_searchable which depends on the event_datetime method for sorting
acts_as_event title: Proc.new { |o| "#{Project.model_name.human}: #{o.name}" },
url: Proc.new { |o| { controller: 'overviews/overviews', action: 'show', project_id: o } },

@ -0,0 +1,40 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Projects::Journalized
extend ActiveSupport::Concern
included do
acts_as_journalized
end
# override acts_as_journalized method
def activity_type
'project_attributes'
end
end

@ -48,10 +48,6 @@ class WikiContent < ApplicationRecord
}
}
def activity_type
'wiki_edits'
end
def visible?(user = User.current)
page.visible?(user)
end

@ -207,11 +207,6 @@ class WorkPackage < ApplicationRecord
(usr || User.current).allowed_to?(:view_work_packages, project)
end
# ACTS AS JOURNALIZED
def activity_type
'work_packages'
end
# RELATIONS
def blockers
# return work_packages that block me

@ -365,9 +365,6 @@ module WorkPackages::Scopes
# rubocop:enable Metrics/PerceivedComplexity
def existing_hierarchy_lateral(with_descendants: true)
hierarchy_condition = ["work_package_hierarchies.descendant_id = related.id"]
hierarchy_condition << "work_package_hierarchies.ancestor_id = related.id" if with_descendants
<<~SQL.squish
SELECT
CASE
@ -385,7 +382,7 @@ module WorkPackages::Scopes
work_package_hierarchies
WHERE
related.from_hierarchy = false AND
(#{hierarchy_condition.join(' OR ')})
(work_package_hierarchies.descendant_id = related.id OR work_package_hierarchies.ancestor_id = related.id)
AND (work_package_hierarchies.generations != 0)
SQL
end

@ -251,7 +251,6 @@ module Journals
journable_id,
journable_type,
version,
activity_type,
user_id,
notes,
created_at,
@ -263,7 +262,6 @@ module Journals
:journable_id,
:journable_type,
COALESCE(max_journals.version, 0) + 1,
:activity_type,
:user_id,
:notes,
#{journal_timestamp_sql(notes, ':created_at')},
@ -277,7 +275,6 @@ module Journals
sanitize(journal_sql,
notes:,
journable_id: journable.id,
activity_type: journable.activity_type,
journable_type:,
user_id: user.id,
created_at: journable_timestamp,

@ -704,7 +704,7 @@ en:
auth_source:
attributes:
tls_certificate_string:
invalid_certificate: "The provides SSL certificate is invalid: %{additional_message}"
invalid_certificate: "The provided SSL certificate is invalid: %{additional_message}"
format: "%{message}"
attachment:
attributes:

@ -0,0 +1,44 @@
class RemoveActivityTypeFromJournalsTable < ActiveRecord::Migration[7.0]
def up
remove_column :journals, :activity_type
end
def down
add_column :journals, :activity_type, :string
execute <<-SQL.squish
UPDATE
journals
SET
activity_type =
CASE
WHEN journable_type = 'Attachment' THEN 'attachments'
WHEN journable_type = 'Budget' THEN 'budgets'
WHEN journable_type = 'Changeset' THEN 'changesets'
WHEN journable_type = 'Document' THEN 'documents'
WHEN journable_type = 'Meeting' THEN 'meetings'
WHEN journable_type = 'Message' THEN 'messages'
WHEN journable_type = 'News' THEN 'news'
WHEN journable_type = 'TimeEntry' THEN 'time_entries'
WHEN journable_type = 'WikiContent' THEN 'wiki_edits'
WHEN journable_type = 'WorkPackage' THEN 'work_packages'
END
WHERE journable_type != 'MeetingContent'
SQL
execute <<-SQL.squish
UPDATE
journals
SET
activity_type =
CASE
WHEN meeting_contents.type = 'MeetingMinutes' THEN 'meeting_minutes'
WHEN meeting_contents.type = 'MeetingAgenda' THEN 'meeting_agenda'
ELSE 'meetings'
END
FROM meeting_contents
WHERE journable_type = 'MeetingContent'
AND journable_id = meeting_contents.id
SQL
end
end

@ -0,0 +1,15 @@
class CreateProjectJournals < ActiveRecord::Migration[7.0]
# rubocop:disable Rails/CreateTableWithTimestamps(RuboCop)
def change
create_table :project_journals do |t|
t.string :name, null: false
t.text :description
t.boolean :public, null: false
t.bigint :parent_id
t.string :identifier, null: false
t.boolean :active, null: false
t.boolean :templated, null: false
end
end
# rubocop:enable Rails/CreateTableWithTimestamps(RuboCop)
end

@ -0,0 +1,16 @@
class AddNonNullConstraintOnProjectsIdentifier < ActiveRecord::Migration[7.0]
def change
reversible do |dir|
dir.up do
# should not be needed as all identifiers should be present
execute <<~SQL.squish
UPDATE projects
SET identifier = 'project-' || id
WHERE identifier IS NULL
SQL
end
end
change_column_null :projects, :identifier, false
end
end

@ -0,0 +1,97 @@
class FillProjectJournalsWithExistingData < ActiveRecord::Migration[7.0]
def up
create_journal_entries_for_projects
end
def down
delete_journal_entries_for_projects
end
private
def create_journal_entries_for_projects
sql = <<~SQL.squish
WITH project_journals_insertion AS (
INSERT INTO project_journals(
name,
description,
public,
parent_id,
identifier,
active,
templated
)
SELECT name,
description,
public,
parent_id,
identifier,
active,
templated
FROM projects
RETURNING id,
identifier
),
journals_insertion AS (
INSERT into journals (
journable_id,
journable_type,
user_id,
created_at,
updated_at,
version,
data_type,
data_id
)
SELECT projects.id,
'Project',
:user_id,
projects.created_at,
projects.updated_at,
1,
'Journal::ProjectJournal',
project_journals_insertion.id
FROM projects
FULL JOIN project_journals_insertion ON projects.identifier = project_journals_insertion.identifier
RETURNING id,
journable_id
),
customizable_journals_insertion AS (
INSERT into customizable_journals (
journal_id,
custom_field_id,
value
)
SELECT journals_insertion.id,
custom_field_id,
value
FROM custom_values
FULL JOIN journals_insertion ON custom_values.customized_id = journals_insertion.journable_id
WHERE custom_values.customized_type = 'Project'
)
SELECT COUNT(1)
FROM journals_insertion;
SQL
sql = ::ActiveRecord::Base.sanitize_sql_array([sql, { user_id: User.system.id }])
execute(sql)
end
def delete_journal_entries_for_projects
execute(<<~SQL.squish)
DELETE
FROM customizable_journals
WHERE journal_id IN (
SELECT id FROM journals WHERE journable_type = 'Project'
)
SQL
execute(<<~SQL.squish)
DELETE
FROM journals
WHERE journable_type = 'Project'
SQL
execute(<<~SQL.squish)
DELETE
FROM project_journals
SQL
end
end

@ -320,6 +320,20 @@ Then finish the installation by reading the [*Initial configuration*](#initial-c
> **Note:** On this distribution full-text extraction for attachments [*is not supported*](#full-text-extraction-not-supported) by default.
**Workaround for outdated PostgreSQL library package**
Depending on your version and installation variant, you might receive errors like `symbol lookup error: /opt/openproject/vendor/bundle/ruby/3.1.0/gems/pg-1.4.3/lib/pg_ext.so: undefined symbol: PQconninfo`.
This happens when your local postgresql-libs package is outdated. You'll have to install a newer version manually like so:
1. Add the package source for PostgreSQL (the exact URL might differ, double check https://www.postgresql.org/download/linux/redhat/)
`yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm`
2. Install postgresql13-libs
`yum install postgresql13-libs`
## SUSE Linux Enterprise Server (SLES) Installation
### SLES 12

@ -1,3 +1,4 @@
/.angular/cache
/.sass-cache
/bower_components
/coverage

@ -48,6 +48,8 @@
"node_modules/@fullcalendar/common/main.css",
"node_modules/@fullcalendar/daygrid/main.css",
"node_modules/@fullcalendar/timegrid/main.css",
"node_modules/@fullcalendar/timeline/main.css",
"node_modules/@fullcalendar/resource-timeline/main.css",
"node_modules/flatpickr/dist/flatpickr.min.css"
],
"stylePreprocessorOptions": {
@ -76,7 +78,6 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"fileReplacements": [
@ -136,7 +137,6 @@
}
}
},
"defaultProject": "OpenProject",
"schematics": {
"@schematics/angular:component": {
"prefix": "op",

File diff suppressed because it is too large Load Diff

@ -5,17 +5,17 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
"@angular-devkit/build-angular": "~12.2.6",
"@angular-eslint/builder": "^12.0.0",
"@angular-eslint/eslint-plugin": "^12.0.0",
"@angular-eslint/eslint-plugin-template": "^12.0.0",
"@angular-eslint/schematics": "12.5.0",
"@angular-eslint/template-parser": "^12.0.0",
"@angular/language-service": "12.2.6",
"@angular-devkit/build-angular": "^14.2.5",
"@angular-eslint/builder": "^13.2.1",
"@angular-eslint/eslint-plugin": "^13.2.1",
"@angular-eslint/eslint-plugin-template": "^13.2.1",
"@angular-eslint/schematics": "13.2.1",
"@angular-eslint/template-parser": "^13.2.1",
"@angular/language-service": "14.0.2",
"@babel/core": "^7.18.5",
"@compodoc/compodoc": "^1.1.19",
"@html-eslint/eslint-plugin": "^0.11.0",
"@html-eslint/parser": "^0.11.0",
"@html-eslint/eslint-plugin": "^0.13.1",
"@html-eslint/parser": "^0.13.1",
"@jsdevtools/coverage-istanbul-loader": "3.0.5",
"@storybook/addon-actions": "^6.5.10",
"@storybook/addon-essentials": "^6.5.10",
@ -29,7 +29,7 @@
"@storybook/preset-scss": "^1.0.3",
"@storybook/testing-library": "^0.0.13",
"@types/chart.js": "^2.9.20",
"@types/codemirror": "0.0.87",
"@types/codemirror": "5.60.5",
"@types/dragula": "^3.7.0",
"@types/hammerjs": "^2.0.36",
"@types/jasmine": "~3.6.0",
@ -79,21 +79,21 @@
"style-loader": "^3.3.1",
"theo": "^8.1.5",
"ts-node": "~8.3.0",
"typescript": "~4.2.4",
"typescript": "~4.7.4",
"webpack-bundle-analyzer": "^4.4.2"
},
"dependencies": {
"@angular/animations": "~12.2.6",
"@angular/cdk": "^12.2.6",
"@angular/cli": "~12.2.6",
"@angular/common": "~12.2.6",
"@angular/compiler": "~12.2.6",
"@angular/compiler-cli": "~12.2.6",
"@angular/core": "~12.2.6",
"@angular/forms": "~12.2.6",
"@angular/platform-browser": "~12.2.6",
"@angular/platform-browser-dynamic": "~12.2.6",
"@angular/router": "~12.2.6",
"@angular/animations": "^14.2.5",
"@angular/cdk": "^14.2.4",
"@angular/cli": "^14.2.5",
"@angular/common": "^14.2.5",
"@angular/compiler": "^14.2.5",
"@angular/compiler-cli": "^14.2.5",
"@angular/core": "^14.2.5",
"@angular/forms": "^14.2.5",
"@angular/platform-browser": "^14.2.5",
"@angular/platform-browser-dynamic": "^14.2.5",
"@angular/router": "^14.2.5",
"@appsignal/javascript": "^1.3.23",
"@appsignal/plugin-breadcrumbs-console": "^1.1.24",
"@appsignal/plugin-breadcrumbs-network": "^1.1.21",
@ -115,8 +115,8 @@
"@sentry/angular": "6.2.3",
"@sentry/tracing": "6.2.3",
"@sentry/types": "^6.2.3",
"@uirouter/angular": "^8.0.1",
"@uirouter/core": "^6.0.7",
"@uirouter/angular": "^9.1.0",
"@uirouter/core": "^6.0.8",
"@uirouter/rx": "^0.6.5",
"@w11k/ngx-componentdestroyed": "^5.0.2",
"@xeokit/xeokit-bim-viewer": "2.3.10",
@ -154,6 +154,7 @@
"ngx-cookie-service": "^14.0.0",
"observable-array": "0.0.4",
"pako": "^2.0.3",
"qr-creator": "^1.0.0",
"reactivestates": "2.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^6.6.6",
@ -172,8 +173,8 @@
},
"scripts": {
"analyze": "ng build --configuration production --stats-json && webpack-bundle-analyzer -h 0.0.0.0 -p 9999 ../public/assets/frontend/stats.json",
"build": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production --named-chunks --extract-css --source-map",
"build:watch": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --watch --named-chunks --extract-css",
"build": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production --named-chunks --source-map",
"build:watch": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --watch --named-chunks",
"tokens:generate": "theo src/app/spot/styles/tokens/tokens.yml --transform web --format sass,json --dest src/app/spot/styles/tokens/dist",
"icon-font:generate": "node ./src/app/spot/icon-font/generate.js ./src/app/spot/icon-font",
"serve": "NG_PERSISTENT_BUILD_CACHE=1 node --max_old_space_size=8096 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --public-host http://localhost:4200",

@ -183,7 +183,7 @@ RB.Model = (function ($) {
input.change(function () {
typeId = $(this).val();
statusId = $.trim(self.$.find('.status_id .v').html());
newInput = self.findFactory(typeId, statusId, 'status_id');
let newInput = self.findFactory(typeId, statusId, 'status_id');
newInput = self.prepareInputFromFactory(newInput, fieldId, 'status_id', fieldOrder, maxTabIndex);
newInput = self.replaceStatusForNewType(input, newInput, $(this).parent().find('.status_id').val(), editor);
});
@ -224,7 +224,7 @@ RB.Model = (function ($) {
findFactory: function (typeId, statusId, fieldName){
// Find a factory
newInput = $('#' + fieldName + '_options_' + typeId + '_' + statusId);
let newInput = $('#' + fieldName + '_options_' + typeId + '_' + statusId);
if (newInput.length === 0) {
// when no list found, only offer the default status
// no list = combination is not valid / user has no rights -> workflow

@ -25,74 +25,76 @@
//
// See COPYRIGHT and LICENSE files for more details.
//++
/* global jQuery */
function findFilter() {
var filter = jQuery('.simple-filters--container');
jQuery(document).ready(function($) {
function findFilter() {
var filter = jQuery('.simple-filters--container');
// Find the filter elements on the page
if(filter.length === 0)
filter = jQuery('.advanced-filters--container');
else if(filter.length === 0)
filter = nil;
// Find the filter elements on the page
if(filter.length === 0) {
filter = jQuery('.advanced-filters--container');
}
return filter;
}
return filter;
}
function hideFilter(filter) {
filter.addClass('collapsed');
}
const filter = findFilter();
function showFilter(filter) {
filter.removeClass('collapsed');
}
function hideFilter(filter) {
filter.addClass('collapsed');
}
function toggleMemberFilter() {
if (window.OpenProject.guardedLocalStorage("showFilter") === "true") {
window.OpenProject.guardedLocalStorage("showFilter", 'false');
function showFilter(filter) {
filter.removeClass('collapsed');
}
function toggleMemberFilter() {
if (window.OpenProject.guardedLocalStorage("showFilter") === "true") {
window.OpenProject.guardedLocalStorage("showFilter", 'false');
hideFilter(filter);
jQuery('#filter-member-button').removeClass('-active');
}
else {
window.OpenProject.guardedLocalStorage("showFilter", 'true');
showFilter(filter);
jQuery('#filter-member-button').addClass('-active');
hideAddMemberForm();
jQuery('.simple-filters--filter:first-of-type select').focus();
}
}
function showAddMemberForm() {
jQuery('#members_add_form').css('display', 'block');
jQuery('#members_add_form #principal_search').focus();
hideFilter(filter);
jQuery('#filter-member-button').removeClass('-active');
window.OpenProject.guardedLocalStorage("showFilter", 'false');
jQuery('#add-member-button').prop('disabled', true);
jQuery("input#member_user_ids").on("change", function() {
var values = jQuery("input#member_user_ids").val();
if (values.indexOf("@") != -1) {
jQuery("#member-user-limit-warning").css("display", "block");
} else {
jQuery("#member-user-limit-warning").css("display", "none");
}
});
}
else {
window.OpenProject.guardedLocalStorage("showFilter", 'true');
showFilter(filter);
jQuery('#filter-member-button').addClass('-active');
hideAddMemberForm();
jQuery('.simple-filters--filter:first-of-type select').focus();
}
}
function showAddMemberForm() {
jQuery('#members_add_form').css('display', 'block');
jQuery('#members_add_form #principal_search').focus();
hideFilter(filter = findFilter());
jQuery('#filter-member-button').removeClass('-active');
window.OpenProject.guardedLocalStorage("showFilter", 'false');
jQuery('#add-member-button').prop('disabled', true);
jQuery("input#member_user_ids").on("change", function() {
var values = jQuery("input#member_user_ids").val();
if (values.indexOf("@") != -1) {
jQuery("#member-user-limit-warning").css("display", "block");
} else {
jQuery("#member-user-limit-warning").css("display", "none");
}
});
}
function hideAddMemberForm() {
jQuery('#members_add_form').css('display', 'none');
jQuery('#add-member-button').focus();
jQuery('#add-member-button').prop('disabled', false);
}
function hideAddMemberForm() {
jQuery('#members_add_form').css('display', 'none');
jQuery('#add-member-button').focus();
jQuery('#add-member-button').prop('disabled', false);
}
jQuery(document).ready(function($) {
// Show/Hide content when page is loaded
if (window.OpenProject.guardedLocalStorage("showFilter") === "true") {
showFilter(filter = findFilter());
showFilter(filter);
}
else {
hideFilter(filter = findFilter());
hideFilter(filter);
// In case showFilter is not set yet
window.OpenProject.guardedLocalStorage("showFilter", 'false');
}

@ -197,9 +197,9 @@ jQuery(function ($) {
}
function setValueVisibility() {
selectedOperator = $(this).val();
$filter = $(this).parents('.advanced-filters--filter')
$filterValue = $('.advanced-filters--filter-value', $filter);
const selectedOperator = $(this).val();
const $filter = $(this).parents('.advanced-filters--filter')
const $filterValue = $('.advanced-filters--filter-value', $filter);
if (['*', '!*', 't', 'w'].includes(selectedOperator)) {
$filterValue.addClass('hidden');
} else {

@ -1,6 +1,4 @@
import 'core-vendor/qrcode-min';
declare let QRCode:any;
import QrCreator from 'qr-creator';
jQuery(($) => {
$('#submit_otp').submit(() => {
@ -13,15 +11,14 @@ jQuery(($) => {
});
$('.qr-code-element').each(function () {
const el = $(this);
new QRCode(
el[0],
{
text: el.data('value'),
width: 220,
height: 220,
},
);
QrCreator.render({
text: this.dataset.value as string,
radius: 0,
ecLevel: 'H',
fill: '#222222',
background: '#FFFFFF',
size: 250,
}, this);
});
$('.ajax_form').submit(function () {

@ -1,7 +1,7 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient } from '@angular/common/http';
import { FormBuilder } from '@angular/forms';
import { UntypedFormBuilder } from '@angular/forms';
import { FormsService } from './forms.service';
describe('FormsService', () => {
@ -26,7 +26,7 @@ describe('FormsService', () => {
],
},
};
const formBuilder = new FormBuilder();
const formBuilder = new UntypedFormBuilder();
beforeEach(() => {
TestBed.configureTestingModule({

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { catchError, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
@ -12,7 +12,7 @@ export class FormsService {
private _httpClient:HttpClient,
) { }
submit$(form:FormGroup, resourceEndpoint:string, resourceId?:string, formHttpMethod?:'post' | 'patch', formSchema?:IOPFormSchema):Observable<any> {
submit$(form:UntypedFormGroup, resourceEndpoint:string, resourceId?:string, formHttpMethod?:'post' | 'patch', formSchema?:IOPFormSchema):Observable<any> {
const modelToSubmit = this.formatModelToSubmit(form.getRawValue(), formSchema);
const httpMethod = resourceId ? 'patch' : (formHttpMethod || 'post');
const url = resourceId ? `${resourceEndpoint}/${resourceId}` : resourceEndpoint;
@ -37,7 +37,7 @@ export class FormsService {
);
}
validateForm$(form:FormGroup, resourceEndpoint:string, formSchema?:IOPFormSchema):Observable<any> {
validateForm$(form:UntypedFormGroup, resourceEndpoint:string, formSchema?:IOPFormSchema):Observable<any> {
const modelToSubmit = this.formatModelToSubmit(form.value, formSchema);
return this._httpClient
@ -116,7 +116,7 @@ export class FormsService {
return model;
}
private handleBackendFormValidationErrors(error:HttpErrorResponse, form:FormGroup):void {
private handleBackendFormValidationErrors(error:HttpErrorResponse, form:UntypedFormGroup):void {
const errors:IOPFormError[] = error?.error?._embedded?.errors
? error?.error?._embedded?.errors : [error.error];
const formErrors = this.getFormattedErrors(errors);
@ -124,7 +124,7 @@ export class FormsService {
this.setFormValidationErrors(formErrors, form);
}
private setFormValidationErrors(errors:IFormattedValidationError[], form:FormGroup) {
private setFormValidationErrors(errors:IFormattedValidationError[], form:UntypedFormGroup) {
errors.forEach((err:any) => {
const formControl = form.get(err.key) || form.get('_links')?.get(err.key);

@ -25,11 +25,11 @@
<h3 class="form--section-title">
{{ text.title }}
</h3>
<p>
{{ text.info }}
</p>
<p class="danger-zone--warning">
<span class="icon icon-error"></span>
<span>{{ text.note }}</span>
@ -63,7 +63,13 @@
(input)="backupToken = $event.target.value"
#backupTokenInput
/>
<button name="button" type="submit" class="-highlight button" (click)="triggerBackup($event)" [disabled]="backupToken.length == 0">
<button
name="button"
type="submit"
class="-highlight button"
[disabled]="backupToken.length === 0"
(click)="triggerBackup($event)"
>
<i class="button--icon icon-export"></i>
<span class="button--text">{{ text.requestBackup }}</span>
</button>

@ -26,9 +26,9 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { HttpErrorResponse } from '@angular/common/http';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
Injector,
@ -49,6 +49,7 @@ export const backupSelector = 'backup';
@Component({
selector: backupSelector,
templateUrl: './backup.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BackupComponent implements AfterViewInit {
public text = {
@ -93,7 +94,7 @@ export class BackupComponent implements AfterViewInit {
this.includeAttachments = this.mayIncludeAttachments;
}
ngAfterViewInit() {
ngAfterViewInit():void {
this.backupTokenInput.nativeElement.focus();
}

@ -28,14 +28,21 @@
// Moved from app/assets/javascript/danger_zone_validation.js
// Make the whole danger zone a component the next time this needs changes!
export function dangerZoneValidation() {
// This will only work iff there is a single danger zone on the page
const dangerZoneVerification = jQuery('.danger-zone--verification');
const expectedValue = jQuery('.danger-zone--expected-value').text();
const expectedValue = jQuery('.danger-zone--expected-value');
// When no expected value is set up, do not disable button
if (!expectedValue[0]) {
return;
}
const expectedText = expectedValue.text();
dangerZoneVerification.find('input').on('input', () => {
const actualValue = dangerZoneVerification.find('input').val() as string;
if (expectedValue.toLowerCase() === actualValue.toLowerCase()) {
if (expectedText.toLowerCase() === actualValue.toLowerCase()) {
dangerZoneVerification.find('button').prop('disabled', false);
} else {
dangerZoneVerification.find('button').prop('disabled', true);

@ -2,20 +2,46 @@ export const demoProjectName = 'Demo project';
export const scrumDemoProjectName = 'Scrum project';
export const onboardingTourStorageKey = 'openProject-onboardingTour';
export type OnboardingTourNames = 'prepareBacklogs'|'backlogs'|'taskboard'|'homescreen'|'main';
export enum ProjectName {
demo = 'demo',
scrum = 'scrum',
}
export function waitForElement(element:string, container:string, execFunction:Function) {
function matchingFilter(list:NodeListOf<HTMLElement>, filterFunction:(match:HTMLElement) => boolean):HTMLElement|null {
for (let i = 0; i < list.length; i++) {
if (filterFunction(list[i])) {
return list[i];
}
}
return null;
}
export function waitForElement(
selector:string,
containerSelector:string,
execFunction:(match:HTMLElement) => void,
filterFunction:(match:HTMLElement) => boolean = () => true,
):void {
const container = document.querySelector(containerSelector) as HTMLElement;
// If the element is ready immediately
const initial = matchingFilter(container.querySelectorAll<HTMLElement>(selector), filterFunction);
if (initial) {
execFunction(initial);
return;
}
// Wait for the element to be ready
const observer = new MutationObserver((mutations, observerInstance) => {
if (jQuery(element).length) {
observerInstance.disconnect(); // stop observing
execFunction();
const matches = matchingFilter(container.querySelectorAll<HTMLElement>(selector), filterFunction);
if (matches) {
execFunction(matches);
observerInstance.disconnect();
}
});
observer.observe(jQuery(container)[0], {
observer.observe(container, {
childList: true,
subtree: true,
});

@ -21,9 +21,12 @@ export function boardTourSteps(edition:'basic'|'enterprise', project:ProjectName
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
onNext() {
jQuery('.board-view-menu-item ~ .toggler')[0].click();
waitForElement('.op-sidemenu--items', '#main-menu', () => {
jQuery(`.op-sidemenu--item-action:contains(${boardName})`)[0].click();
});
waitForElement(
'.op-sidemenu--item-action',
'#main-menu',
(match) => match.click(),
(match) => !!match.textContent?.includes(boardName),
);
},
},
{

@ -9,9 +9,13 @@ export function teamPlannerTourSteps():OnboardingStep[] {
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
onNext() {
jQuery('.team-planner-view-menu-item ~ .toggler')[0].click();
waitForElement('.op-sidemenu--items', '#main-menu', () => {
jQuery(".op-sidemenu--item-action:contains('Team planner')")[0].click();
});
waitForElement(
'.op-sidemenu--item-action',
'#main-menu',
(match) => match.click(),
(match) => !!match.textContent?.includes('Team planner'),
);
},
},
{

@ -32,7 +32,7 @@ import {
ElementRef,
} from '@angular/core';
import {
FormBuilder,
UntypedFormBuilder,
Validators,
} from '@angular/forms';
import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -97,7 +97,7 @@ export class EETrialFormComponent {
constructor(
readonly elementRef:ElementRef,
readonly I18n:I18nService,
readonly formBuilder:FormBuilder,
readonly formBuilder:UntypedFormBuilder,
readonly currentUserService:CurrentUserService,
readonly configurationService:ConfigurationService,
readonly eeTrialService:EnterpriseTrialService,

@ -36,14 +36,8 @@ import {
Input,
ViewChild,
} from '@angular/core';
import {
DomSanitizer,
SafeResourceUrl,
} from '@angular/platform-browser';
import {
FormControl,
FormGroup,
} from '@angular/forms';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
@ -66,7 +60,7 @@ export class EnterpriseTrialModalComponent extends OpModalComponent implements A
@Input() public opReferrer:string;
public trialForm:FormGroup;
public trialForm:UntypedFormGroup;
public trustedEEVideoURL:SafeResourceUrl;
@ -131,7 +125,7 @@ export class EnterpriseTrialModalComponent extends OpModalComponent implements A
// checks if form is valid and submits it
public onSubmit():void {
if (this.trialForm.valid) {
this.trialForm.addControl('_type', new FormControl('enterprise-trial'));
this.trialForm.addControl('_type', new UntypedFormControl('enterprise-trial'));
void this.eeTrialService.sendForm(this.trialForm);
}
}

@ -27,7 +27,7 @@
//++
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import {
HttpClient,
HttpErrorResponse,
@ -128,7 +128,7 @@ export class EnterpriseTrialService {
// send POST request with form object
// receive an enterprise trial link to access a token
public sendForm(form:FormGroup):Promise<unknown> {
public sendForm(form:UntypedFormGroup):Promise<unknown> {
const request:unknown = { ...form.value, token_version: this.tokenVersion };
return this.http
.post(

@ -6,7 +6,7 @@ import {
Output,
ElementRef,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { UntypedFormControl } from '@angular/forms';
import {
Observable,
BehaviorSubject,
@ -42,7 +42,7 @@ interface NgSelectPrincipalOption {
templateUrl: './principal-search.component.html',
})
export class PrincipalSearchComponent extends UntilDestroyedMixin implements OnInit {
@Input() spotFormBinding:FormControl;
@Input() spotFormBinding:UntypedFormControl;
@Input() type:PrincipalType;

@ -4,8 +4,8 @@ import {
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
FormGroup,
FormControl,
UntypedFormGroup,
UntypedFormControl,
Validators,
AbstractControl,
} from '@angular/forms';
@ -99,13 +99,13 @@ export class PrincipalComponent implements OnInit {
},
};
public principalForm = new FormGroup({
public principalForm = new UntypedFormGroup({
// eslint-disable-next-line @typescript-eslint/unbound-method
principal: new FormControl(null, [Validators.required]),
userDynamicFields: new FormGroup({}),
principal: new UntypedFormControl(null, [Validators.required]),
userDynamicFields: new UntypedFormGroup({}),
// eslint-disable-next-line @typescript-eslint/unbound-method
role: new FormControl(null, [Validators.required]),
message: new FormControl(''),
role: new UntypedFormControl(null, [Validators.required]),
message: new UntypedFormControl(''),
});
public userDynamicFieldConfig:{

@ -10,8 +10,8 @@ import {
} from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -73,11 +73,11 @@ export class ProjectSelectionComponent implements OnInit {
},
];
projectAndTypeForm = new FormGroup({
projectAndTypeForm = new UntypedFormGroup({
// eslint-disable-next-line @typescript-eslint/unbound-method
type: new FormControl(PrincipalType.User, [Validators.required]),
type: new UntypedFormControl(PrincipalType.User, [Validators.required]),
// eslint-disable-next-line @typescript-eslint/unbound-method
project: new FormControl(null, [Validators.required], ProjectAllowedValidator(this.currentUserService)),
project: new UntypedFormControl(null, [Validators.required], ProjectAllowedValidator(this.currentUserService)),
});
get typeControl():AbstractControl {

@ -4,7 +4,7 @@ import {
Input,
OnInit,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { UntypedFormControl } from '@angular/forms';
import {
combineLatest,
Observable,
@ -27,7 +27,7 @@ import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter
templateUrl: './role-search.component.html',
})
export class RoleSearchComponent extends UntilDestroyedMixin implements OnInit {
@Input() spotFormBinding:FormControl;
@Input() spotFormBinding:UntypedFormControl;
public input$ = new Subject<string|null>();

@ -48,13 +48,13 @@ export const OVERVIEW_ROUTES:Ng2StateDeclaration[] = [
},
];
export function uiRouterOverviewConfiguration(uiRouter:UIRouter) {
export function uiRouterOverviewConfiguration(uiRouter:UIRouter):void {
// Ensure projects/:project_id/ are being redirected correctly
// cf., https://community.openproject.com/wp/29754
uiRouter.urlService.rules
.when(
new RegExp('^/projects(?!/new$)/([^/]+)$'),
(match) => `/projects/${match[1]}/`,
new RegExp('^/projects(?!/new$)/([^/?]+)$'),
(match:string[]) => `/projects/${match[1]}/${window.location.search}`,
);
}

@ -3,7 +3,7 @@ import { StateService, UIRouterGlobals } from '@uirouter/core';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { IDynamicFieldGroupConfig, IOPFormlyFieldSettings } from 'core-app/shared/components/dynamic-forms/typings';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { FormControl, FormGroup } from '@angular/forms';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
@ -64,8 +64,8 @@ export class NewProjectComponent extends UntilDestroyedMixin implements OnInit {
map((response) => response.elements.map((el:HalResource) => ({ href: el.href, name: el.name }))),
);
templateForm = new FormGroup({
template: new FormControl(),
templateForm = new UntypedFormGroup({
template: new UntypedFormControl(),
});
get templateControl() {

@ -54,9 +54,7 @@ Reporting.Controls = function($){
};
var send_settings_data = function (targetUrl, callback, failureCallback) {
if (!failureCallback) {
failureCallback = default_failure_callback;
}
var errorCallback = failureCallback || default_failure_callback()
Reporting.clearFlash();
$.ajax({
@ -66,7 +64,7 @@ Reporting.Controls = function($){
beforeSend: function () {
$('#ajax-indicator').show();
},
error: failureCallback,
error: errorCallback,
success: callback
});
};
@ -94,7 +92,7 @@ Reporting.Controls = function($){
if (element === null) {
return;
}
failureCallback = function (response) {
var failureCallback = function (response) {
$('#result-table').html("");
default_failure_callback(response);

@ -338,7 +338,7 @@ Reporting.Filters = function($){
$(".advanced-filters--filter-value .filter-value").each(function () {
var select = $(this);
select_value = select.val();
var select_value = select.val();
select.attr('multiple', select_value && select_value.length > 1);

@ -5,7 +5,7 @@ import {
Input,
Output,
} from '@angular/core';
import { FormArray } from '@angular/forms';
import { UntypedFormArray } from '@angular/forms';
import { ApiV3ListFilter } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { HalSourceLink } from 'core-app/features/hal/resources/hal-resource';
@ -25,7 +25,7 @@ export interface NotificationSettingProjectOption {
export class NotificationSettingInlineCreateComponent {
@Input() userId:string;
@Input() settings:FormArray;
@Input() settings:UntypedFormArray;
@Output() selected = new EventEmitter<HalSourceLink>();

@ -6,9 +6,9 @@ import {
OnInit,
} from '@angular/core';
import {
FormArray,
FormControl,
FormGroup,
UntypedFormGroup,
UntypedFormArray,
UntypedFormControl,
} from '@angular/forms';
import { take } from 'rxjs/internal/operators/take';
import { UIRouterGlobals } from '@uirouter/core';
@ -65,27 +65,27 @@ export class NotificationsSettingsPageComponent extends UntilDestroyedMixin impl
public eeShowBanners = false;
public form = new FormGroup({
assignee: new FormControl(false),
responsible: new FormControl(false),
workPackageCreated: new FormControl(false),
workPackageProcessed: new FormControl(false),
workPackageScheduled: new FormControl(false),
workPackagePrioritized: new FormControl(false),
workPackageCommented: new FormControl(false),
startDate: new FormGroup({
active: new FormControl(false),
time: new FormControl(this.availableTimes[1]),
public form = new UntypedFormGroup({
assignee: new UntypedFormControl(false),
responsible: new UntypedFormControl(false),
workPackageCreated: new UntypedFormControl(false),
workPackageProcessed: new UntypedFormControl(false),
workPackageScheduled: new UntypedFormControl(false),
workPackagePrioritized: new UntypedFormControl(false),
workPackageCommented: new UntypedFormControl(false),
startDate: new UntypedFormGroup({
active: new UntypedFormControl(false),
time: new UntypedFormControl(this.availableTimes[1]),
}),
dueDate: new FormGroup({
active: new FormControl(false),
time: new FormControl(this.availableTimes[1]),
dueDate: new UntypedFormGroup({
active: new UntypedFormControl(false),
time: new UntypedFormControl(this.availableTimes[1]),
}),
overdue: new FormGroup({
active: new FormControl(false),
time: new FormControl(this.availableTimesOverdue[0]),
overdue: new UntypedFormGroup({
active: new UntypedFormControl(false),
time: new UntypedFormControl(this.availableTimesOverdue[0]),
}),
projectSettings: new FormArray([]),
projectSettings: new UntypedFormArray([]),
});
text = {
@ -201,24 +201,24 @@ export class NotificationsSettingsPageComponent extends UntilDestroyedMixin impl
return;
}
const projectSettings = new FormArray([]);
const projectSettings = new UntypedFormArray([]);
projectSettings.clear();
settings
.sort(
(a, b):number => a._links.project.title!.localeCompare(b._links.project.title!),
)
.forEach((setting) => projectSettings.push(new FormGroup({
project: new FormControl(setting._links.project),
assignee: new FormControl(setting.assignee),
responsible: new FormControl(setting.responsible),
workPackageCreated: new FormControl(setting.workPackageCreated),
workPackageProcessed: new FormControl(setting.workPackageProcessed),
workPackageScheduled: new FormControl(setting.workPackageScheduled),
workPackagePrioritized: new FormControl(setting.workPackagePrioritized),
workPackageCommented: new FormControl(setting.workPackageCommented),
startDate: new FormControl(setting.startDate),
dueDate: new FormControl(setting.dueDate),
overdue: new FormControl(setting.overdue),
.forEach((setting) => projectSettings.push(new UntypedFormGroup({
project: new UntypedFormControl(setting._links.project),
assignee: new UntypedFormControl(setting.assignee),
responsible: new UntypedFormControl(setting.responsible),
workPackageCreated: new UntypedFormControl(setting.workPackageCreated),
workPackageProcessed: new UntypedFormControl(setting.workPackageProcessed),
workPackageScheduled: new UntypedFormControl(setting.workPackageScheduled),
workPackagePrioritized: new UntypedFormControl(setting.workPackagePrioritized),
workPackageCommented: new UntypedFormControl(setting.workPackageCommented),
startDate: new UntypedFormControl(setting.startDate),
dueDate: new UntypedFormControl(setting.dueDate),
overdue: new UntypedFormControl(setting.overdue),
})));
this.form.setControl('projectSettings', projectSettings);

@ -6,7 +6,7 @@ import {
Input,
OnInit,
} from '@angular/core';
import { FormArray, FormGroup, FormControl } from '@angular/forms';
import { UntypedFormArray, UntypedFormGroup, UntypedFormControl } from '@angular/forms';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
@ -23,7 +23,7 @@ import { OVERDUE_REMINDER_AVAILABLE_TIMEFRAMES, REMINDER_AVAILABLE_TIMEFRAMES }
export class NotificationSettingsTableComponent implements OnInit {
@Input() userId:string;
@Input() settings:FormArray;
@Input() settings:UntypedFormArray;
public eeShowBanners = false;
@ -91,18 +91,18 @@ export class NotificationSettingsTableComponent implements OnInit {
}
addProjectSettings(project:HalSourceLink):void {
this.settings.push(new FormGroup({
project: new FormControl(project),
assignee: new FormControl(false),
responsible: new FormControl(false),
workPackageCreated: new FormControl(false),
workPackageProcessed: new FormControl(false),
workPackageScheduled: new FormControl(false),
workPackagePrioritized: new FormControl(false),
workPackageCommented: new FormControl(false),
startDate: new FormControl(this.availableTimes[2].value),
dueDate: new FormControl(this.availableTimes[2].value),
overdue: new FormControl(this.availableTimesOverdue[0].value),
this.settings.push(new UntypedFormGroup({
project: new UntypedFormControl(project),
assignee: new UntypedFormControl(false),
responsible: new UntypedFormControl(false),
workPackageCreated: new UntypedFormControl(false),
workPackageProcessed: new UntypedFormControl(false),
workPackageScheduled: new UntypedFormControl(false),
workPackagePrioritized: new UntypedFormControl(false),
workPackageCommented: new UntypedFormControl(false),
startDate: new UntypedFormControl(this.availableTimes[2].value),
dueDate: new UntypedFormControl(this.availableTimes[2].value),
overdue: new UntypedFormControl(this.availableTimesOverdue[0].value),
}));
}

@ -6,7 +6,7 @@ import {
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service';
import {
FormGroup,
UntypedFormGroup,
FormGroupDirective,
} from '@angular/forms';
@ -31,7 +31,7 @@ export const emailAlerts:EmailAlertType[] = [
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmailAlertsSettingsComponent implements OnInit {
form:FormGroup;
form:UntypedFormGroup;
alerts:EmailAlertType[] = emailAlerts;
@ -56,6 +56,6 @@ export class EmailAlertsSettingsComponent implements OnInit {
}
ngOnInit():void {
this.form = this.rootFormGroup.control.get('emailAlerts') as FormGroup;
this.form = this.rootFormGroup.control.get('emailAlerts') as UntypedFormGroup;
}
}

@ -6,7 +6,7 @@ import {
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service';
import {
FormGroup,
UntypedFormGroup,
FormGroupDirective,
} from '@angular/forms';
@ -16,7 +16,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ImmediateReminderSettingsComponent implements OnInit {
form:FormGroup;
form:UntypedFormGroup;
text = {
title: this.I18n.t('js.reminders.settings.immediate.title'),
@ -32,6 +32,6 @@ export class ImmediateReminderSettingsComponent implements OnInit {
}
ngOnInit():void {
this.form = this.rootFormGroup.control.get('immediateReminders') as FormGroup;
this.form = this.rootFormGroup.control.get('immediateReminders') as UntypedFormGroup;
}
}

@ -11,8 +11,8 @@ import { take } from 'rxjs/internal/operators/take';
import { UIRouterGlobals } from '@uirouter/core';
import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service';
import {
FormArray,
FormBuilder,
UntypedFormArray,
UntypedFormBuilder,
} from '@angular/forms';
import {
DailyRemindersSettings,
@ -97,7 +97,7 @@ export class ReminderSettingsPageComponent extends UntilDestroyedMixin implement
private storeService:UserPreferencesService,
private currentUserService:CurrentUserService,
private uiRouterGlobals:UIRouterGlobals,
private fb:FormBuilder,
private fb:UntypedFormBuilder,
private cdRef:ChangeDetectorRef,
) {
super();
@ -132,7 +132,7 @@ export class ReminderSettingsPageComponent extends UntilDestroyedMixin implement
this.form.get('pauseReminders')?.patchValue(settings.pauseReminders);
const dailyReminderTimes = this.form.get('dailyReminders.times') as FormArray;
const dailyReminderTimes = this.form.get('dailyReminders.times') as UntypedFormArray;
dailyReminderTimes.clear({ emitEvent: false });
[...settings.dailyReminders.times].sort().forEach((time) => {
dailyReminderTimes.push(this.fb.control(time), { emitEvent: false });
@ -140,7 +140,7 @@ export class ReminderSettingsPageComponent extends UntilDestroyedMixin implement
dailyReminderTimes.enable({ emitEvent: true });
const workdays = this.form.get('workdays') as FormArray;
const workdays = this.form.get('workdays') as UntypedFormArray;
for (let i = 0; i <= 6; i++) {
const control = workdays.at(i);
control.setValue(settings.workdays.includes(i + 1));

@ -4,7 +4,7 @@ import {
OnInit,
} from '@angular/core';
import {
FormGroup,
UntypedFormGroup,
FormGroupDirective,
} from '@angular/forms';
import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -21,7 +21,7 @@ import { Observable } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PauseRemindersComponent implements OnInit {
form:FormGroup;
form:UntypedFormGroup;
selectedDates$:Observable<[string, string]>;
@ -41,7 +41,7 @@ export class PauseRemindersComponent implements OnInit {
}
ngOnInit():void {
this.form = this.rootFormGroup.control.get('pauseReminders') as FormGroup;
this.form = this.rootFormGroup.control.get('pauseReminders') as UntypedFormGroup;
this.selectedDates$ = this
.form
.valueChanges

@ -16,9 +16,9 @@ import {
} from 'rxjs';
import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service';
import {
FormArray,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
} from '@angular/forms';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
@ -42,7 +42,7 @@ export class ReminderSettingsDailyTimeComponent implements OnInit {
// Upon a reload of the page, it is accepted to loose this information.
public inactiveTimes:Array<{ position:number, time:string }> = [];
public form:FormGroup;
public form:UntypedFormGroup;
// Hours suggested if a new time is added by a user.
public suggestedTimes = ['08:00', '12:00', '15:00', '18:00'];
@ -83,7 +83,7 @@ export class ReminderSettingsDailyTimeComponent implements OnInit {
}
ngOnInit():void {
this.form = this.rootFormGroup.control.get('dailyReminders') as FormGroup;
this.form = this.rootFormGroup.control.get('dailyReminders') as UntypedFormGroup;
this.enabled$ = this
.form
@ -96,12 +96,12 @@ export class ReminderSettingsDailyTimeComponent implements OnInit {
this.selectedTimes$ = (this
.form
.get('times') as FormArray)
.get('times') as UntypedFormArray)
.valueChanges
.pipe(
startWith(() => this.form.get('times')?.value as FormArray),
startWith(() => this.form.get('times')?.value as UntypedFormArray),
map(() => {
const timesArray = this.form.get('times') as FormArray;
const timesArray = this.form.get('times') as UntypedFormArray;
const activeTimes = timesArray.controls.map((c) => c.value as string);
this
@ -211,10 +211,10 @@ export class ReminderSettingsDailyTimeComponent implements OnInit {
.includes(selected),
);
const timesForm = this.form.get('times') as FormArray;
const timesForm = this.form.get('times') as UntypedFormArray;
timesForm.clear({ emitEvent: false });
times.forEach((time) => {
timesForm.push(new FormControl(time), { emitEvent: false });
timesForm.push(new UntypedFormControl(time), { emitEvent: false });
});
timesForm.enable({ emitEvent: true });

@ -4,8 +4,8 @@ import {
OnInit,
} from '@angular/core';
import {
FormArray,
FormControl,
UntypedFormArray,
UntypedFormControl,
FormGroupDirective,
} from '@angular/forms';
import * as moment from 'moment';
@ -18,7 +18,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkdaysSettingsComponent implements OnInit {
control:FormArray;
control:UntypedFormArray;
/**
* The locale might render workdays in a different order, which is what moment return with localeSorted
@ -46,16 +46,16 @@ export class WorkdaysSettingsComponent implements OnInit {
}
ngOnInit():void {
this.control = this.formGroup.control.get('workdays') as FormArray;
this.control = this.formGroup.control.get('workdays') as UntypedFormArray;
}
indexOfLocalWorkday(day:string):number {
return this.isoWorkdays.indexOf(day);
}
controlForLocalWorkday(day:string):FormControl {
controlForLocalWorkday(day:string):UntypedFormControl {
const index = this.indexOfLocalWorkday(day);
return this.control.at(index) as FormControl;
return this.control.at(index) as UntypedFormControl;
}
/** Workdays from moment.js are in non-ISO order, that means Sunday=0, Saturday=6 */

@ -67,6 +67,7 @@ import {
zoomLevelOrder,
} from '../wp-timeline';
import { WeekdayService } from 'core-app/core/days/weekday.service';
import * as Mousetrap from 'mousetrap';
@Component({
selector: 'wp-timeline-container',

@ -6,6 +6,7 @@ import { OPContextMenuService } from 'core-app/shared/components/op-context-menu
import { WorkPackageViewBaseService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-base.service';
import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource';
import * as Mousetrap from 'mousetrap';
export interface WorkPackageViewSelectionState {
// Map of selected rows

@ -1,13 +1,29 @@
import { NgSelectModule } from '@ng-select/ng-select';
import { NgOptionHighlightModule } from '@ng-select/ng-option-highlight';
import { Component, forwardRef, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
import {
Component,
forwardRef,
ViewChild,
} from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flush,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { defer, of } from 'rxjs';
import { FormControl, FormGroup, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import {
defer,
of,
} from 'rxjs';
import {
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
UntypedFormControl,
UntypedFormGroup,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
import { FormlyModule } from '@ngx-formly/core';
import { DynamicFormComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component';
import { DynamicFormService } from 'core-app/shared/components/dynamic-forms/services/dynamic-form/dynamic-form.service';
import { DynamicFieldsService } from 'core-app/shared/components/dynamic-forms/services/dynamic-fields/dynamic-fields.service';
@ -32,7 +48,7 @@ import { ConfirmDialogService } from "core-app/shared/components/modals/confirm-
providers: [],
})
class DynamicFormsTestingComponent {
control = new FormControl('');
control = new UntypedFormControl('');
@ViewChild(DynamicFormComponent) dynamicFormControl:DynamicFormComponent;
}
@ -260,7 +276,7 @@ describe('DynamicFormComponent', () => {
},
},
},
form: new FormGroup({}),
form: new UntypedFormGroup({}),
};
const I18nServiceStub = {
t(key:string) {

@ -11,15 +11,22 @@ import {
import { FormlyForm } from '@ngx-formly/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { catchError, finalize } from 'rxjs/operators';
import {
catchError,
finalize,
} from 'rxjs/operators';
import { HalSource } from 'core-app/features/hal/resources/hal-resource';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
import { DynamicFieldsService } from 'core-app/shared/components/dynamic-forms/services/dynamic-fields/dynamic-fields.service';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { ConfirmDialogService } from 'core-app/shared/components/modals/confirm-dialog/confirm-dialog.service';
import { IDynamicFieldGroupConfig, IOPDynamicFormSettings, IOPFormlyFieldSettings } from '../../typings';
import {
IDynamicFieldGroupConfig,
IOPDynamicFormSettings,
IOPFormlyFieldSettings,
} from '../../typings';
import { DynamicFormService } from '../../services/dynamic-form/dynamic-form.service';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
@ -143,7 +150,7 @@ export class DynamicFormComponent extends UntilDestroyedMixin implements OnChang
@Input() settings?:IOPFormSettings;
@Input() dynamicFormGroup?:FormGroup;
@Input() dynamicFormGroup?:UntypedFormGroup;
/** Initial payload to POST to the form */
@Input() initialPayload:Object = {};
@ -178,7 +185,7 @@ export class DynamicFormComponent extends UntilDestroyedMixin implements OnChang
@Output() errored = new EventEmitter<IOPFormErrorResponse>();
form:FormGroup;
form:UntypedFormGroup;
fields:IOPFormlyFieldSettings[];
@ -254,7 +261,7 @@ export class DynamicFormComponent extends UntilDestroyedMixin implements OnChang
this.modelChange.emit(changes);
}
submitForm(form:FormGroup) {
submitForm(form:UntypedFormGroup) {
if (!this.handleSubmit) {
return;
}

@ -3,7 +3,7 @@ import { DynamicFormService } from 'core-app/shared/components/dynamic-forms/ser
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient } from '@angular/common/http';
import { DynamicFieldsService } from 'core-app/shared/components/dynamic-forms/services/dynamic-fields/dynamic-fields.service';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { of } from 'rxjs';
import { FormsService } from 'core-app/core/forms/forms.service';
@ -112,7 +112,7 @@ describe('DynamicFormService', () => {
},
_meta: undefined,
},
form: new FormGroup({}),
form: new UntypedFormGroup({}),
};
beforeEach(() => {

@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { UntypedFormGroup } from '@angular/forms';
import { FormlyForm } from '@ngx-formly/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@ -47,7 +47,7 @@ export class DynamicFormService {
this.formSchema = formConfig._embedded?.schema;
const formPayload = formConfig._embedded?.payload;
const dynamicForm = {
form: new FormGroup({}),
form: new UntypedFormGroup({}),
fields: this._dynamicFieldsService.getConfig(this.formSchema, formPayload),
model: this._dynamicFieldsService.getModel(formPayload),
};
@ -59,11 +59,11 @@ export class DynamicFormService {
return this._formsService.formatModelToEdit(formModel);
}
validateForm$(form:FormGroup, resourceEndpoint:string) {
validateForm$(form:UntypedFormGroup, resourceEndpoint:string) {
return this._formsService.validateForm$(form, resourceEndpoint, this.formSchema);
}
submit$(form:FormGroup, resourceEndpoint:string, resourceId?:string, formHttpMethod?:'post' | 'patch') {
submit$(form:UntypedFormGroup, resourceEndpoint:string, resourceId?:string, formHttpMethod?:'post' | 'patch') {
return this._formsService.submit$(form, resourceEndpoint, resourceId, formHttpMethod, this.formSchema);
}
}

@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, forwardRef, ViewChild } from '@angular/core';
import { FormGroup, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { UntypedFormGroup, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { TextInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/text-input/text-input.component';
import { IntegerInputComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-inputs/integer-input/integer-input.component';
@ -36,7 +36,7 @@ export function createDynamicInputFixture(fields:IOPFormlyFieldSettings[], model
providers,
})
class DynamicInputsTestingComponent {
form = new FormGroup({});
form = new UntypedFormGroup({});
model = model;
@ -117,7 +117,7 @@ export function createDynamicInputFixture(fields:IOPFormlyFieldSettings[], model
}
export function testDynamicInputControValueAccessor(fixture:ComponentFixture<any>, model:any, selector:string) {
const dynamicForm:FormGroup = fixture.componentInstance.dynamicForm.form;
const dynamicForm:UntypedFormGroup = fixture.componentInstance.dynamicForm.form;
const dynamicInput = fixture.debugElement.query(By.css(selector)).nativeElement;
// Test ControlValueAccessor

@ -319,9 +319,9 @@ export class ResourceChangeset<T extends HalResource = HalResource> {
* Access some promised value
* that should be cached for the lifetime duration of the form.
*/
public cacheValue<T>(key:string, request:() => Promise<T>):Promise<T> {
if (this.cache[key]) {
return this.cache[key] as Promise<T>;
public cacheValue<V>(key:string, request:() => Promise<V>):Promise<V> {
if (this.cache[key] !== undefined) {
return this.cache[key] as Promise<V>;
}
return this.cache[key] = request();

@ -2,6 +2,7 @@ import { AfterViewInit, Directive, ElementRef } from '@angular/core';
import { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service';
import { OpContextMenuHandler } from 'core-app/shared/components/op-context-menu/op-context-menu-handler';
import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op-context-menu.types';
import * as Mousetrap from 'mousetrap';
@Directive({
selector: '[opContextMenuTrigger]',

@ -1,7 +1,15 @@
import {
ApplicationRef, ComponentFactoryResolver, Injectable, Injector,
ApplicationRef,
ComponentFactoryResolver,
Injectable,
Injector,
} from '@angular/core';
import { ComponentPortal, DomPortalOutlet, PortalInjector } from '@angular/cdk/portal';
import {
ComponentPortal,
ComponentType,
DomPortalOutlet,
PortalInjector,
} from '@angular/cdk/portal';
import { TransitionService } from '@uirouter/core';
import { OpContextMenuHandler } from 'core-app/shared/components/op-context-menu/op-context-menu-handler';
import {
@ -11,7 +19,6 @@ import {
import { OPContextMenuComponent } from 'core-app/shared/components/op-context-menu/op-context-menu.component';
import { KeyCodes } from 'core-app/shared/helpers/keyCodes.enum';
import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper';
import { ComponentType } from '@angular/cdk/portal/portal';
@Injectable({ providedIn: 'root' })
export class OPContextMenuService {

@ -30,6 +30,7 @@ import { Injectable } from '@angular/core';
import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helper';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import * as Mousetrap from 'mousetrap';
const accessKeys = {
preview: 1,

@ -30,9 +30,10 @@ export function getPosition(evt:any):number {
return range.startOffset;
}
if (document.caretRangeFromPoint) {
return document
.caretRangeFromPoint(evt.clientX!, evt.clientY!)
const legacyDocument = document as { caretRangeFromPoint?:(x:number, y:number) => { startOffset:number } };
if (legacyDocument.caretRangeFromPoint) {
return legacyDocument
.caretRangeFromPoint((evt as MouseEvent).clientX, (evt as MouseEvent).clientY)
.startOffset;
}

@ -1,5 +1,5 @@
import {
ChangeDetectionStrategy,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
@ -36,6 +36,7 @@ export class SpotDropModalComponent implements OnDestroy {
/**
* Boolean indicating whether the modal should be opened
*/
/* eslint-disable-next-line @angular-eslint/no-input-rename */
@Input('open')
@HostBinding('class.spot-drop-modal_opened')
set open(value:boolean) {
@ -49,6 +50,9 @@ export class SpotDropModalComponent implements OnDestroy {
setTimeout(() => {
document.body.addEventListener('click', this.closeEventListener);
document.body.addEventListener('keydown', this.escapeListener);
window.addEventListener('resize', this.appHeightListener);
window.addEventListener('orientationchange', this.appHeightListener);
this.appHeightListener();
const focusCatcherContainer = document.querySelectorAll("[data-modal-focus-catcher-container='true']")[0];
if (focusCatcherContainer) {
@ -60,7 +64,10 @@ export class SpotDropModalComponent implements OnDestroy {
});
} else {
document.body.removeEventListener('click', this.closeEventListener);
document.body.removeEventListener('click', this.escapeListener);
document.body.removeEventListener('keydown', this.escapeListener);
window.removeEventListener('resize', this.appHeightListener);
window.removeEventListener('orientationchange', this.appHeightListener);
this.closed.emit();
}
}
@ -110,7 +117,9 @@ export class SpotDropModalComponent implements OnDestroy {
ngOnDestroy():void {
document.body.removeEventListener('click', this.closeEventListener);
document.body.removeEventListener('click', this.escapeListener);
document.body.removeEventListener('keydown', this.escapeListener);
window.removeEventListener('resize', this.appHeightListener);
window.removeEventListener('orientationchange', this.appHeightListener);
}
private closeEventListener = this.close.bind(this) as () => void;
@ -122,4 +131,9 @@ export class SpotDropModalComponent implements OnDestroy {
};
private escapeListener = this.onEscape.bind(this) as () => void;
private appHeightListener = () => {
const doc = document.documentElement;
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
};
}

@ -1,6 +1,6 @@
import { Directive, forwardRef, Input } from '@angular/core';
import {
FormArray, FormControl, FormGroup, NgControl,
UntypedFormArray, UntypedFormControl, UntypedFormGroup, NgControl,
} from '@angular/forms';
export const formControlBinding = {
@ -15,9 +15,9 @@ export const formControlBinding = {
exportAs: 'ngForm',
})
export class SpotFormBindingDirective extends NgControl {
@Input('spotFormBinding') form!:FormControl|FormGroup|FormArray;
@Input('spotFormBinding') form!:UntypedFormControl|UntypedFormGroup|UntypedFormArray;
get control():FormControl|FormGroup|FormArray {
get control():UntypedFormControl|UntypedFormGroup|UntypedFormArray {
return this.form;
}

@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import {
FormGroup,
FormControl,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
@ -9,8 +9,8 @@ import {
templateUrl: './FormFieldErrorSlot.example.html',
})
export class SbFormFieldErrorSlotExample {
public myForm = new FormGroup({
myInput: new FormControl(null, [Validators.required, Validators.minLength(8)]),
public myForm = new UntypedFormGroup({
myInput: new UntypedFormControl(null, [Validators.required, Validators.minLength(8)]),
});
get myInputControl() {

@ -15,8 +15,7 @@
left: $spot-spacing-1
right: $spot-spacing-1
width: calc(100vw - (2 * #{$spot-spacing-1}))
max-height: calc(100vh - (#{$spot-spacing-3_5} + #{$spot-spacing-1}))
max-height: calc(#{var(--app-height)} - (#{$spot-spacing-3_5} + #{$spot-spacing-1}))
box-shadow: $spot-shadow-light-mid
background: $spot-color-basic-white
border-radius: $border-radius
@ -27,7 +26,7 @@
@media #{$spot-mq-landscape}
bottom: $spot-spacing-1
right: $spot-spacing-3_5
max-height: calc(100vh - (2 * #{$spot-spacing-1}))
max-height: calc(#{var(--app-height)} - (2 * #{$spot-spacing-1}))
width: calc(100vw - (#{$spot-spacing-3_5} + #{$spot-spacing-1}))
@media #{$spot-mq-drop-modal-in-context}
@ -38,7 +37,7 @@
width: auto
height: auto
max-width: calc(100vw - (2 * #{$spot-spacing-1}))
max-height: calc(100vh - (2 * #{$spot-spacing-1}))
max-height: calc(#{var(--app-height)} - (2 * #{$spot-spacing-1}))
&_left-top
right: 100%

@ -32,6 +32,16 @@
--primary-color: #1A67A3;
--primary-color-dark: #175A8E;
--alternative-color: #35C53F;
/**
* The 100vh bug on iOS is a well known issue that will not be addressed.
* Using 100vh will not work correctly whenever the bottom toolbar of ios safari is open.
* To address the issue, we need to watch the screen height change events and rely on the
* window.innerHeight, instead of using 100vh.
* See more: https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
*/
--app-height: 100vh;
--body-font-family: 'Lato', 'Lucida Grande', Helvetica, Arial, sans-serif;
--gray: #EAEAEA;
--gray-dark: #878787;

@ -19,6 +19,9 @@
*/
// Ensure global is set for ng2-dragula and others
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access no-explicit-any
(window as any).global = window;
/*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
@ -30,11 +33,6 @@
*/
import 'zone.js';
(window as any).global = window;
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';
@ -42,12 +40,6 @@ import 'zone.js';
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
// import 'core-js/es7/reflect';
/**
* Required to support Web Animations `@angular/platform-browser/animations`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
* */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags

@ -13,6 +13,9 @@ import {
import { GlobalI18n } from 'core-app/core/i18n/i18n.service';
import { I18nShim } from './test/i18n-shim';
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access no-explicit-any
(window as any).global = window;
require('expose-loader?_!lodash');
declare const require:any;
@ -29,6 +32,9 @@ window.I18n = new I18nShim();
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
{
teardown: { destroyAfterEach: false },
},
);
// Then we find all the tests.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -230,7 +230,13 @@ module API
def default_writable_property(property)
-> do
if represented.respond_to?(:writable?)
represented.writable?(property)
property_name = ::API::Utilities::PropertyNameConverter.to_ar_name(
property,
context: represented.model,
collapse_cf_name: false
)
represented.writable?(property_name)
else
false
end

@ -52,19 +52,13 @@ module API
end
def writable?(property)
property = property.to_sym
property = property.to_s
# Special case for milestones + date property
property = :start_date if property == :date && milestone?
property = 'start_date' if property == 'date' && milestone?
@writable_attributes ||= contract.writable_attributes
property_name = ::API::Utilities::PropertyNameConverter.to_ar_name(
property,
context: work_package,
collapse_cf_name: false
)
@writable_attributes.include?(property_name)
@writable_attributes.include?(property)
end
def milestone?
@ -75,6 +69,11 @@ module API
work_package.readonly_status?
end
# Alias method will not work since work_package is only defined in subclasses.
def model
work_package
end
private
def contract

@ -38,6 +38,10 @@ module API
def writable?(_property)
false
end
def work_package
nil
end
end
end
end

@ -0,0 +1,48 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# This patch adds our job status extension to background jobs carried out when mailing with
# perform_later.
module OpenProject
module Patches
module DelayedJobAdapter
module AllowNonExistingJobClass
def log_arguments?
super
rescue NameError
false
end
end
end
end
end
ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.prepend(
OpenProject::Patches::DelayedJobAdapter::AllowNonExistingJobClass
)

@ -82,12 +82,6 @@ module Acts::Journalized
def journaled_columns_names
self.class.journal_class.journaled_attributes
end
# Returns the activity type. Should be overridden in the journalized class to offer
# multiple types
def activity_type
self.class.name.underscore.pluralize
end
end
end
end

@ -74,7 +74,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
login_as(current_user)
end
describe '#remaining_time_writable?' do
describe '#writable? for remaining_hours' do
subject { described_class.new(work_package:) }
context 'work_package is a leaf' do
@ -83,7 +83,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
end
it 'is writable' do
expect(subject.writable?(:remaining_time)).to be(true)
expect(subject.writable?(:remaining_hours)).to be(true)
end
end
@ -93,7 +93,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
end
it 'is not writable' do
expect(subject.writable?(:remaining_time)).to be(false)
expect(subject.writable?(:remaining_hours)).to be(false)
end
end
end

@ -127,7 +127,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
context 'remainingTime not writable' do
before do
allow(schema).to receive(:writable?).and_call_original
allow(schema).to receive(:writable?).with(:remaining_time).and_return(false)
allow(schema).to receive(:writable?).with('remaining_hours').and_return(false)
end
it_behaves_like 'has basic schema properties' do

@ -188,7 +188,11 @@ describe 'Stories in backlog',
.edit_new_story(subject: 'New story',
story_points: 10)
new_story = WorkPackage.find_by(subject: 'New story')
new_story = nil
retry_block do
new_story = WorkPackage.find_by(subject: 'New story')
raise "Expected story" unless new_story
end
backlogs_page
.expect_story_in_sprint(new_story, sprint)
@ -212,10 +216,10 @@ describe 'Stories in backlog',
subject: 'Altered story1',
story_points: 15)
sprint_story1.reload
expect(sprint_story1.subject)
.to eql 'Altered story1'
retry_block do
sprint_story1.reload
raise "Expected story to be renamed" unless sprint_story1.subject == 'Altered story1'
end
backlogs_page
.expect_for_story(sprint_story1, subject: 'Altered story1')
@ -280,10 +284,10 @@ describe 'Stories in backlog',
backlogs_page
.save_story_from_edit_mode(backlog_story1)
backlog_story1.reload
expect(backlog_story1.subject)
.to eql 'Altered backlog story1'
retry_block do
backlog_story1.reload
raise "Expected story to be renamed" unless backlog_story1.subject == 'Altered backlog story1'
end
expect(backlog_story1.status)
.to eql other_status

@ -49,24 +49,37 @@ describe 'Work Package boards sorting spec', type: :feature, js: true do
# The currently added board should be at the top
it 'sorts the boards grid and menu based on their names' do
board_page = board_index.create_board action: nil
board_page.back_to_index
expect(page.first('[data-qa-selector="boards-table-column--name"]'))
.to have_text('Unnamed board')
retry_block do
board_page.back_to_index
find('[data-qa-selector="boards-table-column--name"]', text: 'Unnamed board')
end
expect(page.all('[data-qa-selector="boards-table-column--name"]').map(&:text))
.to eq ['Unnamed board']
query_menu.expect_menu_entry 'Unnamed board'
board_page = board_index.create_board action: :Version, expect_empty: true
board_page.back_to_index
retry_block do
board_page.back_to_index
find('[data-qa-selector="boards-table-column--name"]', text: 'Action board (version)')
end
expect(page.first('[data-qa-selector="boards-table-column--name"]'))
.to have_text('Action board (version)')
expect(page.all('[data-qa-selector="boards-table-column--name"]').map(&:text))
.to eq ['Action board (version)', 'Unnamed board']
query_menu.expect_menu_entry 'Action board (version)'
board_page = board_index.create_board action: :Status
board_page.back_to_index
expect(page.first('[data-qa-selector="boards-table-column--name"]'))
.to have_text('Action board (status)')
retry_block do
board_page.back_to_index
find('[data-qa-selector="boards-table-column--name"]', text: 'Action board (status)')
end
expect(page.all('[data-qa-selector="boards-table-column--name"]').map(&:text))
.to eq ['Action board (status)', 'Action board (version)', 'Unnamed board']
query_menu.expect_menu_entry 'Action board (status)'
end
end

@ -128,11 +128,6 @@ class Budget < ApplicationRecord
subject
end
# override acts_as_journalized method
def activity_type
self.class.plural_name
end
def material_budget
@material_budget ||= material_budget_items.visible_costs.inject(BigDecimal('0.0000')) { |sum, i| sum += i.costs }
end

@ -40,14 +40,16 @@ describe ::API::V3::TimeEntries::Schemas::TimeEntrySchemaRepresenter do
let(:user) { build_stubbed(:user) }
let(:assigned_project) { nil }
let(:activity) { build_stubbed(:time_entry_activity) }
let(:time_entry) { build_stubbed(:time_entry) }
let(:writable_attributes) { %w(spent_on hours project work_package activity comment user) }
let(:contract) do
contract = double('contract',
new_record?: new_record,
id: new_record ? nil : 5,
project: assigned_project,
project_id: project.id)
contract = instance_double(new_record ? TimeEntries::CreateContract : TimeEntries::UpdateContract,
new_record?: new_record,
id: new_record ? nil : 5,
project: assigned_project,
project_id: project.id,
model: time_entry)
allow(contract)
.to receive(:writable?) do |attribute|
@ -268,10 +270,10 @@ describe ::API::V3::TimeEntries::Schemas::TimeEntrySchemaRepresenter do
end
end
context 'custom value' do
context 'for a custom value' do
let(:custom_field) { build_stubbed(:text_time_entry_custom_field) }
let(:path) { "customField#{custom_field.id}" }
let(:writable_attributes) { ["customField#{custom_field.id}"] }
let(:writable_attributes) { ["custom_field_#{custom_field.id}"] }
before do
allow(contract)

@ -33,7 +33,6 @@ FactoryBot.define do
factory :meeting_content_journal, class: 'Journal' do
journable_type { 'MeetingContent' }
activity_type { 'meetings' }
end
end
end

@ -52,8 +52,16 @@ describe "updating a cost report's cost type", type: :feature, js: true do
it 'works' do
report_page.visit!
report_page.save(as: 'My Query', public: true)
SeleniumHubWaiter.wait
retry_block do
cost_query = CostQuery.find_by!(name: 'My Query')
raise "Expected path change" unless page.has_current_path?("/projects/#{project.identifier}/cost_reports/#{cost_query.id}")
expect(page).to have_field('Labor', checked: true)
end
report_page.switch_to_type cost_type.name
expect(page).to have_field(cost_type.name, checked: true, wait: 10)
click_on "Save"
click_on "My Query"

@ -324,7 +324,7 @@ describe WorkPackages::BulkController, type: :controller, with_settings: { journ
it { expect(response.response_code).to eq(403) }
describe '#journal' do
subject { Journal.count }
subject { Journal.for_work_package.count }
it { is_expected.to eq(work_package_ids.count) }
end

@ -34,7 +34,6 @@ FactoryBot.define do
factory :work_package_journal, class: 'Journal' do
journable_type { 'WorkPackage' }
activity_type { 'work_packages' }
data { build(:journal_work_package_journal) }
callback(:after_stub) do |journal, options|
@ -44,19 +43,16 @@ FactoryBot.define do
factory :wiki_content_journal, class: 'Journal' do
journable_type { 'WikiContent' }
activity_type { 'wiki_edits' }
data { build(:journal_wiki_content_journal) }
end
factory :message_journal, class: 'Journal' do
journable_type { 'Message' }
activity_type { 'messages' }
data { build(:journal_message_journal) }
end
factory :news_journal, class: 'Journal' do
journable_type { 'News' }
activity_type { 'news' }
data { build(:journal_message_journal) }
end
end

@ -40,35 +40,26 @@ describe ::API::V3::Memberships::Schemas::MembershipSchemaRepresenter do
let(:principal) { build_stubbed(:group) }
let(:assigned_project) { nil }
let(:assigned_principal) { nil }
let(:allowed_roles) do
if new_record
[build_stubbed(:role),
build_stubbed(:role)]
end
end
let(:member) { build_stubbed(:member) }
let(:contract) do
contract = double('contract',
new_record?: new_record,
project: assigned_project,
principal: assigned_principal)
contract = instance_double(new_record ? Members::CreateContract : Members::UpdateContract,
model: member,
new_record?: new_record,
project: assigned_project,
principal: assigned_principal)
allow(contract)
.to receive(:writable?) do |attribute|
writable = %w(roles)
if new_record
writable = writable.concat(%w(project principal))
writable.concat(%w(project principal))
end
writable.include?(attribute.to_s)
end
allow(contract)
.to receive(:assignable_values)
.with(:roles, current_user)
.and_return(allowed_roles)
contract
end
let(:representer) do

@ -52,9 +52,11 @@ describe ::API::V3::Utilities::CustomFieldInjector do
let(:base_class) { Class.new(::API::Decorators::SchemaRepresenter) }
let(:modified_class) { described_class.create_schema_representer([custom_field], base_class) }
let(:schema_writable) { true }
let(:model) { build_stubbed(:work_package) }
let(:schema) do
double('WorkPackageSchema',
project_id: 42,
model:,
defines_assignable_values?: true,
available_custom_fields: [custom_field],
writable?: schema_writable)

@ -41,10 +41,12 @@ describe ::API::V3::Versions::Schemas::VersionSchemaRepresenter do
let(:custom_field) do
build_stubbed(:int_version_custom_field)
end
let(:version) { build_stubbed(:version) }
let(:contract) do
contract = double('contract',
new_record?: new_record)
contract = instance_double(new_record ? Versions::CreateContract : Versions::UpdateContract,
new_record?: new_record,
model: version)
allow(contract)
.to receive(:writable?) do |attribute|

@ -85,8 +85,8 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
.and_return(true)
expect(subject).to be_readonly
expect(subject.writable?(:status)).to be_truthy
expect(subject.writable?(:subject)).to be_falsey
expect(subject).to be_writable('status')
expect(subject).not_to be_writable('subject')
allow(work_package)
.to receive(:readonly_status?)
@ -95,8 +95,8 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
# As the writability is memoized we need to have a new schema
new_schema = described_class.new(work_package:)
expect(new_schema).not_to be_readonly
expect(new_schema.writable?(:status)).to be_truthy
expect(new_schema.writable?(:subject)).to be_truthy
expect(new_schema).to be_writable('status')
expect(new_schema).to be_writable('subject')
end
end
@ -176,46 +176,46 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
context 'percentage done' do
it 'is not writable when inferred by status' do
allow(Setting).to receive(:work_package_done_ratio).and_return('status')
expect(subject.writable?(:percentage_done)).to be false
expect(subject.writable?('done_ratio')).to be false
end
it 'is not writable when disabled' do
allow(Setting).to receive(:work_package_done_ratio).and_return('disabled')
expect(subject.writable?(:percentage_done)).to be false
expect(subject.writable?('done_ratio')).to be false
end
it 'is not writable when the work package is a parent' do
allow(work_package).to receive(:leaf?).and_return(false)
expect(subject.writable?(:percentage_done)).to be false
expect(subject.writable?('done_ratio')).to be false
end
it 'is writable when the work package is a leaf' do
allow(work_package).to receive(:leaf?).and_return(true)
expect(subject.writable?(:percentage_done)).to be true
expect(subject.writable?('done_ratio')).to be true
end
end
context 'estimated time' do
it 'is writable when the work package is a parent' do
allow(work_package).to receive(:leaf?).and_return(false)
expect(subject.writable?(:estimated_time)).to be true
expect(subject.writable?('estimated_hours')).to be true
end
it 'is writable when the work package is a leaf' do
allow(work_package).to receive(:leaf?).and_return(true)
expect(subject.writable?(:estimated_time)).to be true
expect(subject.writable?('estimated_hours')).to be true
end
end
context 'derived estimated time' do
it 'is not writable when the work package is a parent' do
allow(work_package).to receive(:leaf?).and_return(false)
expect(subject.writable?(:derived_estimated_time)).to be false
expect(subject.writable?('derived_estimated_time')).to be false
end
it 'is not writable when the work package is a leaf' do
allow(work_package).to receive(:leaf?).and_return(true)
expect(subject.writable?(:derived_estimated_time)).to be false
expect(subject.writable?('derived_estimated_time')).to be false
end
end
@ -229,7 +229,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
context 'scheduled automatically' do
it 'is not writable' do
expect(subject.writable?(:start_date)).to be false
expect(subject.writable?('start_date')).to be false
end
end
@ -239,7 +239,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
end
it 'is writable' do
expect(subject.writable?(:start_date)).to be true
expect(subject.writable?('start_date')).to be true
end
end
end
@ -247,7 +247,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
context 'work package is a leaf' do
it 'is writable' do
allow(work_package).to receive(:leaf?).and_return(true)
expect(subject.writable?(:start_date)).to be true
expect(subject.writable?('start_date')).to be true
end
end
end
@ -262,7 +262,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
context 'scheduled automatically' do
it 'is not writable' do
expect(subject.writable?(:due_date)).to be false
expect(subject.writable?('due_date')).to be false
end
end
@ -272,7 +272,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
end
it 'is writable' do
expect(subject.writable?(:due_date)).to be true
expect(subject.writable?('due_date')).to be true
end
end
end
@ -280,7 +280,7 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
context 'work package is a leaf' do
it 'is writable' do
allow(work_package).to receive(:leaf?).and_return(true)
expect(subject.writable?(:due_date)).to be true
expect(subject.writable?('due_date')).to be true
end
end
end
@ -294,24 +294,24 @@ describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
it 'is not writable when the work package is a parent' do
allow(work_package).to receive(:leaf?).and_return(false)
expect(subject.writable?(:date)).to be false
expect(subject.writable?('date')).to be false
end
it 'is writable when the work package is a leaf' do
allow(work_package).to receive(:leaf?).and_return(true)
expect(subject.writable?(:date)).to be true
expect(subject.writable?('date')).to be true
end
end
context 'priority' do
it 'is writable when the work package is a parent' do
allow(work_package).to receive(:leaf?).and_return(false)
expect(subject.writable?(:priority)).to be true
expect(subject.writable?('priority')).to be true
end
it 'is writable when the work package is a leaf' do
allow(work_package).to receive(:leaf?).and_return(true)
expect(subject.writable?(:priority)).to be true
expect(subject.writable?('priority')).to be true
end
end
end

@ -64,11 +64,11 @@ describe ::API::V3::WorkPackages::Schema::TypedWorkPackageSchema do
describe '#writable?' do
it 'percentage done is writable' do
expect(subject.writable?(:percentage_done)).to be true
expect(subject.writable?(:done_ratio)).to be true
end
it 'estimated time is writable' do
expect(subject.writable?(:estimated_time)).to be true
expect(subject.writable?(:estimated_hours)).to be true
end
it 'start date is writable' do

@ -334,7 +334,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:ignore_non_working_days)
.with('ignore_non_working_days')
.and_return writable
end
@ -367,7 +367,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:date)
.with('date')
.and_return true
allow(schema)
@ -387,7 +387,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:date)
.with('date')
.and_return false
end
@ -417,7 +417,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:start_date)
.with('start_date')
.and_return true
allow(schema)
@ -437,7 +437,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:start_date)
.with('start_date')
.and_return false
end
@ -467,7 +467,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:due_date)
.with('due_date')
.and_return true
allow(schema)
@ -485,7 +485,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
context 'when not writable' do
before do
allow(schema).to receive(:writable?).with(:due_date).and_return false
allow(schema).to receive(:writable?).with('due_date').and_return false
end
it_behaves_like 'has basic schema properties' do
@ -566,7 +566,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:estimated_time)
.with('estimated_hours')
.and_return true
end
@ -582,7 +582,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
before do
allow(schema)
.to receive(:writable?)
.with(:estimated_time)
.with('estimated_hours')
.and_return false
end
@ -640,7 +640,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
describe 'percentageDone' do
before do
allow(schema).to receive(:writable?).with(:percentage_done).and_return true
allow(schema).to receive(:writable?).with('done_ratio').and_return true
end
it_behaves_like 'has basic schema properties' do
@ -653,7 +653,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
context 'when not writable' do
before do
allow(schema).to receive(:writable?).with(:percentage_done).and_return false
allow(schema).to receive(:writable?).with('done_ratio').and_return false
end
it_behaves_like 'has basic schema properties' do
@ -917,7 +917,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
describe 'priorities' do
before do
allow(schema).to receive(:writable?).with(:priority).and_return true
allow(schema).to receive(:writable?).with('priority').and_return true
end
it_behaves_like 'has basic schema properties' do
@ -938,7 +938,7 @@ describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
context 'when not writable' do
before do
allow(schema).to receive(:writable?).with(:priority).and_return false
allow(schema).to receive(:writable?).with('priority').and_return false
end
it_behaves_like 'has basic schema properties' do

@ -28,45 +28,102 @@
require 'spec_helper'
describe Principals::Scopes::Visible, type: :model do
describe Principals::Scopes::Visible do
describe '.visible' do
shared_let(:project) { create :project }
shared_let(:other_project) { create :project }
shared_let(:role) { create :role, permissions: %i[manage_members] }
shared_let(:role) { create(:role, permissions: %i[manage_members]) }
shared_let(:other_project_user) { create :user, member_in_project: other_project, member_through_role: role }
shared_let(:global_user) { create :user }
shared_let(:anonymous_user) { User.anonymous }
shared_let(:system_user) { User.system }
shared_let(:project) { create(:project) }
shared_let(:project_user) do
create(:user, firstname: 'project user',
member_in_project: project,
member_through_role: role)
end
shared_let(:project_group) do
create(:group, firstname: 'project group',
member_in_project: project,
member_through_role: role)
end
shared_let(:project_placeholder_user) do
create(:placeholder_user, firstname: 'project placeholder user',
member_in_project: project,
member_through_role: role)
end
# The 'other project' is here to ensure their members are not visible from
# the outside for people lacking manage_members or manage_user permissions
shared_let(:other_project) { create(:project) }
shared_let(:other_project_user) do
create(:user, firstname: 'other project user',
member_in_project: other_project,
member_through_role: role)
end
shared_let(:other_project_group) do
create(:group, firstname: 'other project group',
member_in_project: other_project,
member_through_role: role)
end
shared_let(:other_placeholder_user) do
create(:placeholder_user, firstname: 'other project placeholder user',
member_in_project: other_project,
member_through_role: role)
end
shared_let(:global_user) { create(:user, firstname: 'global user') }
shared_let(:global_group) { create(:group, firstname: 'global group') }
shared_let(:global_placeholder_user) { create(:placeholder_user, firstname: 'global placeholder') }
subject { ::Principal.visible.to_a }
context 'when user has manage_members permission' do
current_user { create :user, member_in_project: project, member_through_role: role }
shared_examples 'sees all principals' do
it 'sees all users, groups, and placeholder users' do
expect(subject).to match_array [
# system and anonymous users
anonymous_user, system_user,
# regular users
current_user, project_user, other_project_user, global_user,
# groups,
project_group, other_project_group, global_group,
# placeholder users
project_placeholder_user, other_placeholder_user, global_placeholder_user
]
end
end
it 'sees all users' do
expect(subject).to match_array [current_user, other_project_user, global_user]
context 'when user has manage_members project permission' do
current_user do
create(:user, firstname: 'current user',
member_in_project: project,
member_through_role: role)
end
include_examples 'sees all principals'
end
context 'when user has no manage_members permission, but it is in other project' do
current_user { create :user, member_in_project: other_project, member_with_permissions: %i[view_work_packages] }
context 'when user has no manage_members project permission, and is member of a project' do
current_user do
create(:user, firstname: 'current user',
member_in_project: project,
member_with_permissions: %i[view_work_packages])
end
it 'sees the other user in the same project' do
expect(subject).to match_array [current_user, other_project_user]
it 'sees only the users, groups, and placeholder users in the same project' do
expect(subject).to match_array [current_user, project_user, project_group, project_placeholder_user]
end
end
context 'when user has no manage_members permission, but has manage_user global permission' do
current_user { create :user, global_permissions: %i[manage_user] }
context 'when user has manage_user global permission' do
current_user { create(:user, firstname: 'current user', global_permissions: %i[manage_user]) }
it 'sees all users' do
expect(subject).to match_array [current_user, other_project_user, global_user]
end
include_examples 'sees all principals'
end
context 'when user has no permission' do
current_user { create :user }
current_user { create(:user, firstname: 'current user') }
it 'sees only herself' do
it 'sees only themself' do
expect(subject).to match_array [current_user]
end
end

@ -0,0 +1,223 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
describe Project, 'acts_as_journalized' do
shared_let(:user) { create(:user) }
let!(:project) do
User.execute_as user do
create(:project,
description: 'project description')
end
end
context 'on project creation' do
it 'has one journal entry' do
expect(Journal.all.count).to eq(1)
expect(Journal.first.journable).to eq(project)
end
it 'notes the changes to name' do
expect(Journal.first.details[:name])
.to match_array [nil, project.name]
end
it 'notes the changes to description' do
expect(Journal.first.details[:description])
.to match_array [nil, project.description]
end
it 'notes the changes to public flag' do
expect(Journal.first.details[:public])
.to match_array [nil, project.public]
end
it 'notes the changes to identifier' do
expect(Journal.first.details[:identifier])
.to match_array [nil, project.identifier]
end
it 'notes the changes to active flag' do
expect(Journal.first.details[:active])
.to match_array [nil, project.active]
end
it 'notes the changes to template flag' do
expect(Journal.first.details[:templated])
.to match_array [nil, project.templated]
end
it 'has the timestamp of the project update time for created_at' do
expect(Journal.first.created_at)
.to eql(project.reload.updated_at)
end
end
context 'when nothing is changed' do
it { expect { project.save! }.not_to change(Journal, :count) }
end
describe 'on project update', with_settings: { journal_aggregation_time_minutes: 0 } do
shared_let(:parent_project) { create(:project) }
before do
project.name = 'changed project name'
project.description = 'changed project description'
project.public = !project.public
project.parent = parent_project
project.identifier = 'changed-identifier'
project.active = !project.active
project.templated = !project.templated
project.save!
end
context 'for last created journal' do
it 'has the timestamp of the project update time for created_at' do
expect(project.last_journal.created_at)
.to eql(project.reload.updated_at)
end
it 'contains last changes' do
%i[name description public parent_id identifier active templated].each do |prop|
expect(project.last_journal.details).to have_key(prop.to_s), "Missing change for #{prop}"
end
end
end
end
describe 'custom values', with_settings: { journal_aggregation_time_minutes: 0 } do
shared_let(:custom_field) { create(:string_project_custom_field) }
let(:custom_value) do
build(:custom_value,
value: 'some string value for project custom field',
custom_field:)
end
let(:custom_field_id) { "custom_fields_#{custom_value.custom_field_id}" }
shared_context 'for project with new custom value' do
before do
project.update(custom_values: [custom_value])
end
end
context 'for new custom value' do
include_context 'for project with new custom value'
it 'contains the new custom value change' do
expect(project.last_journal.details)
.to include(custom_field_id => [nil, custom_value.value])
end
end
context 'for updated custom value' do
include_context 'for project with new custom value'
let(:modified_custom_value) do
build(:custom_value,
value: 'some modified value for project custom field',
custom_field:)
end
before do
project.update(custom_values: [modified_custom_value])
end
it 'contains the change from previous value to updated value' do
expect(project.last_journal.details)
.to include(custom_field_id => [custom_value.value, modified_custom_value.value])
end
end
context 'when project saved without any changes' do
include_context 'for project with new custom value'
let(:unmodified_custom_value) do
build(:custom_value,
value: custom_value.value,
custom_field:)
end
before do
project.custom_values = [unmodified_custom_value]
end
it { expect { project.save! }.not_to change(Journal, :count) }
end
context 'when custom value removed' do
include_context 'for project with new custom value'
before do
project.update(custom_values: [])
end
it 'contains the change from previous value to nil' do
expect(project.last_journal.details)
.to include(custom_field_id => [custom_value.value, nil])
end
end
end
describe 'on project deletion' do
shared_let(:custom_field) { create(:string_project_custom_field) }
let(:custom_value) do
build(:custom_value,
value: 'some string value for project custom field',
custom_field:)
end
let!(:project) do
User.execute_as user do
create(:project, custom_values: [custom_value])
end
end
let!(:journal) { project.last_journal }
let!(:customizable_journals) { journal.customizable_journals }
before do
project.destroy
end
it 'removes the journal' do
expect(Journal.find_by(id: journal.id))
.to be_nil
end
it 'removes the journal data' do
expect(Journal::ProjectJournal.find_by(id: journal.data_id))
.to be_nil
end
it 'removes the customizable journals' do
expect(Journal::CustomizableJournal.find_by(id: customizable_journals.map(&:id)))
.to be_nil
end
end
end

@ -133,7 +133,7 @@ describe WikiPage, type: :model do
end
it 'destroys the wiki content\'s journals as well' do
expect { wiki_page.destroy }.to change(Journal, :count).from(1).to(0)
expect { wiki_page.destroy }.to change(Journal.for_wiki_content, :count).from(1).to(0)
end
end

@ -52,29 +52,29 @@ describe WorkPackage, type: :model do
current_user { create(:user) }
context 'for work package creation' do
it { expect(Journal.all.count).to eq(1) }
it { expect(Journal.for_work_package.count).to eq(1) }
it 'has a journal entry' do
expect(Journal.first.journable).to eq(work_package)
expect(Journal.for_work_package.first.journable).to eq(work_package)
end
it 'notes the changes to subject' do
expect(Journal.first.details[:subject])
expect(work_package.last_journal.details[:subject])
.to match_array [nil, work_package.subject]
end
it 'notes the changes to project' do
expect(Journal.first.details[:project_id])
expect(work_package.last_journal.details[:project_id])
.to match_array [nil, work_package.project_id]
end
it 'notes the description' do
expect(Journal.first.details[:description])
expect(work_package.last_journal.details[:description])
.to match_array [nil, work_package.description]
end
it 'notes the scheduling mode' do
expect(Journal.first.details[:schedule_manually])
expect(work_package.last_journal.details[:schedule_manually])
.to match_array [nil, false]
end
@ -82,7 +82,7 @@ describe WorkPackage, type: :model do
# This seemingly unnecessary reload leads to the updated_at having the same
# precision as the created_at of the Journal. It is database dependent, so it would work without
# reload on PG 12 but does not work on PG 9.
expect(Journal.first.created_at)
expect(work_package.last_journal.created_at)
.to eql(work_package.reload.updated_at)
end
end
@ -113,13 +113,13 @@ describe WorkPackage, type: :model do
end
describe 'does not track the changed newline characters' do
subject { work_package1.journals.last.data.description }
subject { work_package1.last_journal.data.description }
it { is_expected.to eq(description) }
end
describe 'tracks only the other change' do
subject { work_package1.journals.last.details }
subject { work_package1.last_journal.details }
it { is_expected.to have_key :subject }
it { is_expected.not_to have_key :description }
@ -142,7 +142,7 @@ describe WorkPackage, type: :model do
description: changed_description))
end
subject { work_package1.journals.reload.last.details }
subject { work_package1.last_journal.details }
it { is_expected.not_to have_key :description }
end
@ -180,7 +180,7 @@ describe WorkPackage, type: :model do
end
context 'for last created journal' do
subject { work_package.journals.reload.last.details }
subject { work_package.last_journal.details }
it 'contains all changes' do
%i(subject description type_id status_id priority_id
@ -267,7 +267,7 @@ describe WorkPackage, type: :model do
end
it 'creates a journal for the last change' do
last_journal = work_package.journals.order(:id).last
last_journal = work_package.last_journal
expect(last_journal.data.description).to eql('description v4')
end
@ -277,7 +277,7 @@ describe WorkPackage, type: :model do
# This seemingly unnecessary reload leads to the updated_at having the same
# precision as the created_at of the Journal. It is database dependent, so it would work without
# reload on PG 12 but does not work on PG 9.
expect(work_package.journals.order(:id).last.created_at)
expect(work_package.last_journal.created_at)
.to eql(work_package.reload.updated_at)
end
end
@ -292,7 +292,7 @@ describe WorkPackage, type: :model do
end
context 'for new attachment' do
subject { work_package.journals.reload.last.details }
subject { work_package.last_journal.details }
it { is_expected.to have_key attachment_id }
@ -327,7 +327,7 @@ describe WorkPackage, type: :model do
context 'for new custom value' do
include_context 'for work package with custom value'
subject { work_package.journals.reload.last.details }
subject { work_package.last_journal.details }
it { is_expected.to have_key custom_field_id }
@ -348,7 +348,7 @@ describe WorkPackage, type: :model do
work_package.save!
end
subject { work_package.journals.reload.last.details }
subject { work_package.last_journal.details }
it { is_expected.to have_key custom_field_id }
@ -379,7 +379,7 @@ describe WorkPackage, type: :model do
work_package.save!
end
subject { work_package.journals.last.details }
subject { work_package.last_journal.details }
it { is_expected.to have_key custom_field_id }
@ -403,13 +403,13 @@ describe WorkPackage, type: :model do
describe 'empty values are recognized as unchanged' do
include_context 'for work package with custom value'
it { expect(work_package.journals.reload.last.customizable_journals).to be_empty }
it { expect(work_package.last_journal.customizable_journals).to be_empty }
end
describe 'empty values handled as non existing' do
include_context 'for work package with custom value'
it { expect(work_package.journals.reload.last.customizable_journals.count).to eq(0) }
it { expect(work_package.last_journal.customizable_journals.count).to eq(0) }
end
end
end
@ -421,7 +421,7 @@ describe WorkPackage, type: :model do
end
it 'has the timestamp of the work package update time for created_at' do
expect(work_package.journals.last.updated_at)
expect(work_package.last_journal.updated_at)
.to eql(work_package.updated_at)
end
end
@ -434,7 +434,7 @@ describe WorkPackage, type: :model do
end
it 'has the timestamp of the work package update time for created_at' do
expect(work_package.journals.last.updated_at)
expect(work_package.last_journal.updated_at)
.to eql(work_package.updated_at)
end
end
@ -592,8 +592,8 @@ describe WorkPackage, type: :model do
subject(:journals) { work_package.journals }
before do
work_package.journals.last.update_columns(created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
work_package.last_journal.update_columns(created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
work_package.status = build(:status)
work_package.save!

@ -1431,7 +1431,7 @@ describe WorkPackages::Scopes::Relatable, '.relatable scope' do
end
end
context 'with a predecessor chain where the first has parent and child' do
context 'with a predecessor chain where the first has parent and child and that child has a predecessor' do
let(:direct_predecessor) do
create(:work_package).tap do |pre|
create(:follows_relation, from: origin, to: pre)
@ -1448,8 +1448,17 @@ describe WorkPackages::Scopes::Relatable, '.relatable scope' do
let(:transitive_predecessor_child) do
create(:work_package, parent: transitive_predecessor)
end
let(:transitive_predecessor_child_predecessor) do
create(:work_package).tap do |pre|
create(:follows_relation, from: transitive_predecessor_child, to: pre)
end
end
let!(:existing_work_packages) do
[direct_predecessor, transitive_predecessor, transitive_predecessor_parent, transitive_predecessor_child]
[direct_predecessor,
transitive_predecessor,
transitive_predecessor_parent,
transitive_predecessor_child,
transitive_predecessor_child_predecessor]
end
context "for a 'parent' relation" do

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save