diff --git a/app/assets/stylesheets/layout/work_packages/_details_view.sass b/app/assets/stylesheets/layout/work_packages/_details_view.sass index a5ed3fb18d..e39dbee230 100644 --- a/app/assets/stylesheets/layout/work_packages/_details_view.sass +++ b/app/assets/stylesheets/layout/work_packages/_details_view.sass @@ -32,8 +32,7 @@ body.router--work-packages-partitioned-split-view-details, body.router--work-packages-partitioned-split-view-new .work-packages-partitioned-page--content-right - overflow-x: hidden - overflow-y: auto + overflow: auto position: relative border-left: 2px solid #eee border-top: 2px solid #eee diff --git a/app/assets/stylesheets/layout/work_packages/_mobile.sass b/app/assets/stylesheets/layout/work_packages/_mobile.sass index c54bcbe224..8feb032f80 100644 --- a/app/assets/stylesheets/layout/work_packages/_mobile.sass +++ b/app/assets/stylesheets/layout/work_packages/_mobile.sass @@ -72,9 +72,6 @@ @include breakpoint(1248px down) .router--work-packages-base // --------------- ALL WP VIEWS --------------- - .work-packages-tabletimeline--table-side - contain: none - .toolbar-container padding-right: 0 diff --git a/app/assets/stylesheets/layout/work_packages/_table.sass b/app/assets/stylesheets/layout/work_packages/_table.sass index 25ba9409f6..af82eb4019 100644 --- a/app/assets/stylesheets/layout/work_packages/_table.sass +++ b/app/assets/stylesheets/layout/work_packages/_table.sass @@ -168,6 +168,9 @@ #content height: 100% + .work-packages-partitioned-page--content-left + overflow: hidden + .icon-button, .sort-header, .action-icon cursor: pointer diff --git a/app/models/journal/aggregated_journal.rb b/app/models/journal/aggregated_journal.rb index 83f9bece40..2e70480116 100644 --- a/app/models/journal/aggregated_journal.rb +++ b/app/models/journal/aggregated_journal.rb @@ -346,7 +346,6 @@ class Journal::AggregatedJournal :project, :data, :data=, - :noop?, to: :journal # Initializes a new AggregatedJournal. Allows to explicitly set a predecessor, if it is already @@ -422,6 +421,12 @@ class Journal::AggregatedJournal predecessor.nil? end + # If we where to delegate here, the wrapped journal would be compared to its predecessor which is + # not necessarily the this aggreated journal's predecessor. + def noop? + (!notes || notes&.empty?) && get_changes.empty? + end + private attr_reader :journal diff --git a/app/uploaders/fog_file_uploader.rb b/app/uploaders/fog_file_uploader.rb index 0a6ff8487f..ecff0bca84 100644 --- a/app/uploaders/fog_file_uploader.rb +++ b/app/uploaders/fog_file_uploader.rb @@ -104,7 +104,9 @@ class FogFileUploader < CarrierWave::Uploader::Base def set_expires_at!(url_options, options:) if options[:expires_in].present? - url_options[:expire_at] = ::Fog::Time.now + options[:expires_in] + # AWS allows at max < 604800 expires time + expires = [options[:expires_in], 604799].min + url_options[:expire_at] = ::Fog::Time.now + expires end if options[:expires_at].present? diff --git a/config/environments/development.rb b/config/environments/development.rb index b2f8b11b55..d42a8a01a6 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -73,7 +73,7 @@ OpenProject::Application.configure do config.assets.digest = false # Suppress asset output - config.assets.quiet = true + config.assets.quiet = true unless config.log_level == :debug # Raises error for missing translations # config.action_view.raise_on_missing_translations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 4659950e42..c2d2445575 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -93,6 +93,8 @@ OpenProject::Application.configure do # Set to :debug to see everything in the log. config.log_level = OpenProject::Configuration['log_level'].to_sym + config.assets.quiet = true unless config.log_level == :debug + # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] diff --git a/config/initializers/suppress_routing_error_logs.rb b/config/initializers/suppress_routing_error_logs.rb new file mode 100644 index 0000000000..6c4dd072f1 --- /dev/null +++ b/config/initializers/suppress_routing_error_logs.rb @@ -0,0 +1,15 @@ +module NoRoutingErrorLogging + def log_error(env, wrapper) + super unless ignore_error?(env, wrapper) + end + + def ignore_error?(env, wrapper) + !Rails.logger.debug? && routing_error?(env, wrapper) + end + + def routing_error?(_env, wrapper) + wrapper.exception.is_a? ActionController::RoutingError + end +end + +ActionDispatch::DebugExceptions.prepend NoRoutingErrorLogging if Rails.env.production? diff --git a/docs/api/apiv2_1-doc.md b/docs/api/apiv2_1-doc.md new file mode 100644 index 0000000000..6f59cce8c5 --- /dev/null +++ b/docs/api/apiv2_1-doc.md @@ -0,0 +1,510 @@ +# BCF REST API in OpenProject +![](https://raw.githubusercontent.com/BuildingSMART/BCF/master/Icons/BCFicon128.png) + +The following describes the extensions and deviations of the BCF API v2.1 implementation in OpenProject. + +While the intend of the implementation is to follow the specification, the API builds on the existing OpenProject data +schema and by that requires to map between the concepts required in the much broader domain of project management and BCF. + +In other parts, the BCF API specification has not been completely implemented. It will be amended where requirements dictate. +OpenProject offers a second API (v3) which might be able to fill the gaps the BCF API implementation still has. + +This document should be read as an extension to the [standard specification](https://github.com/buildingSMART/BCF-API/blob/release_2_1/README.md). +The user should read the standard specification first, and then take a look at this document to be informed about OpenProject specificities. + +The document follows the structure of the standard specification to ease comparing the two documents. + +**Table of Contents** + + + +- [1. Introduction](#1-introduction) + * [1.1 Paging, Sorting and Filtering](#11-paging-sorting-and-filtering) + * [1.2 Caching](#12-caching) + * [1.3 Updating Resources via HTTP PUT](#13-updating-resources-via-http-put) + * [1.4 Cross Origin Resource Sharing (CORS)](#14-cross-origin-resource-sharing-cors) + * [1.5 Http Status Codes](#15-http-status-codes) + * [1.6 Error Response Body Format](#16-error-response-body-format) + * [1.7 DateTime Format](#17-datetime-format) + * [1.8 Authorization](#18-authorization) + + [1.8.1 Per-Entity Authorization](#181-per-entity-authorization) + + [1.8.2 Determining Authorized Entity Actions](#182-determining-authorized-entity-actions) + * [1.9 Additional Response and Request Object Properties](#19-additional-response-and-request-object-properties) + * [1.10 Binary File Uploads](#110-binary-file-uploads) +- [2. Topologies](#2-topologies) + * [2.1 Topology 1 - BCF-Server only](#21-topology-1---bcf-server-only) + * [2.2 Topology 2 - Colocated BCF-Server and Model Server](#22-topology-2---colocated-bcf-server-and-model-server) +- [3. Public Services](#3-public-services) + * [3.1 Versions Service](#31-versions-service) + * [3.2 Authentication Services](#32-authentication-services) + + [3.2.1 Obtaining Authentication Information](#321-obtaining-authentication-information) + + [3.2.2 OAuth2 Example](#322-oauth2-example) + + [3.2.3 OAuth2 Protocol Flow - Dynamic Client Registration](#323-oauth2-protocol-flow---dynamic-client-registration) + * [3.3 User Services](#33-user-services) + + [3.3.1 Get current user](#331-get-current-user) +- [4. BCF Services](#4-bcf-services) + * [4.1 Project Services](#41-project-services) + + [4.1.1 GET Projects Service](#411-get-projects-service) + + [4.1.2 GET Project Service](#412-get-project-service) + + [4.1.3 PUT Project Service](#413-put-project-service) + + [4.1.4 GET Project Extension Service](#414-get-project-extension-service) + + [4.1.5 Expressing User Authorization Through Project Extensions](#415-expressing-user-authorization-through-project-extensions) + - [4.1.5.1 Project](#4151-project) + - [4.1.5.2 Topic](#4152-topic) + - [4.1.5.3 Comment](#4153-comment) + * [4.2 Topic Services](#42-topic-services) + + [4.2.1 GET Topics Service](#421-get-topics-service) + + [4.2.2 POST Topic Service](#422-post-topic-service) + + [4.2.3 GET Topic Service](#423-get-topic-service) + + [4.2.4 PUT Topic Service](#424-put-topic-service) + + [4.2.5 DELETE Topic Service](#425-delete-topic-service) + + [4.2.6 GET Topic BIM Snippet Service](#426-get-topic-bim-snippet-service) + + [4.2.7 PUT Topic BIM Snippet Service](#427-put-topic-bim-snippet-service) + + [4.2.8 Determining Allowed Topic Modifications](#428-determining-allowed-topic-modifications) + * [4.3 File Services](#43-file-services) + + [4.3.1 GET Files (Header) Service](#431-get-files-header-service) + + [4.3.2 PUT Files (Header) Service](#432-put-files-header-service) + * [4.4 Comment Services](#44-comment-services) + + [4.4.1 GET Comments Service](#441-get-comments-service) + + [4.4.2 POST Comment Service](#442-post-comment-service) + + [4.4.3 GET Comment Service](#443-get-comment-service) + + [4.4.4 PUT Comment Service](#444-put-comment-service) + + [4.4.5 DELETE Comment Service](#445-delete-comment-service) + + [4.4.6 Determining allowed Comment modifications](#446-determining-allowed-comment-modifications) + * [4.5 Viewpoint Services](#45-viewpoint-services) + + [4.5.1 GET Viewpoints Service](#451-get-viewpoints-service) + + [4.5.2 POST Viewpoint Service](#452-post-viewpoint-service) + - [4.5.2.1 Point](#4521-point) + - [4.5.2.2 Direction](#4522-direction) + - [4.5.2.3 Orthogonal camera](#4523-orthogonal-camera) + - [4.5.2.4 Perspective camera](#4524-perspective-camera) + - [4.5.2.5 Line](#4525-line) + - [4.5.2.6 Clipping plane](#4526-clipping-plane) + - [4.5.2.7 Bitmap](#4527-bitmap) + - [4.5.2.8 Snapshot](#4528-snapshot) + - [4.5.2.9 Components](#4529-components) + - [4.5.2.10 Component](#45210-component) + * [Optimization rules](#optimization-rules) + - [4.5.2.11 Coloring](#45211-coloring) + * [Optimization rules](#optimization-rules-1) + - [4.5.2.12 Visibility](#45212-visibility) + * [Optimization rules](#optimization-rules-2) + - [4.5.2.13 View setup hints](#45213-view-setup-hints) + + [4.5.3 GET Viewpoint Service](#453-get-viewpoint-service) + + [4.5.4 GET Viewpoint Snapshot Service](#454-get-viewpoint-snapshot-service) + + [4.5.5 GET Viewpoint Bitmap Service](#455-get-viewpoint-bitmap-service) + + [4.5.6 GET selected Components Service](#456-get-selected-components-service) + + [4.5.7 GET colored Components Service](#457-get-colored-components-service) + + [4.5.8 GET visibility of Components Service](#458-get-visibility-of-components-service) + * [4.6 Related Topics Services](#46-related-topics-services) + + [4.6.1 GET Related Topics Service](#461-get-related-topics-service) + + [4.6.2 PUT Related Topics Service](#462-put-related-topics-service) + * [4.7 Document Reference Services](#47-document-reference-services) + + [4.7.1 GET Document References Service](#471-get-document-references-service) + + [4.7.2 POST Document Reference Service](#472-post-document-reference-service) + + [4.7.3 PUT Document Reference Service](#473-put-document-reference-service) + * [4.8 Document Services](#48-document-services) + + [4.8.1 GET Documents Service](#481-get-documents-service) + + [4.8.2 POST Document Service](#482-post-document-service) + + [4.8.3 GET Document Service](#483-get-document-service) + * [4.9 Topics Events Services](#49-topics-events-services) + + [4.9.1 GET Topics Events Service](#491-get-topics-events-service) + + [4.9.2 GET Topic Events Service](#492-get-topic-events-service) + * [4.10 Comments Events Services](#410-comments-events-services) + + [4.10.1 GET Comments Events Service](#4101-get-comments-events-service) + + [4.10.2 GET Comment Events Service](#4102-get-comment-events-service) + + + +# 1. Introduction + +All end points are nested within the `/api` path. So for a server listening on `https://foo.com/` the API root will be +`https://foo.com/api/bcf/2.1`. For a server listening on `https://foo.com/bar` the API root will be +`https://foo.com/bar/api/bcf/2.1`. + +## 1.1 Paging, Sorting and Filtering + +_Not implemented_ + +## 1.2 Caching + +_Implemented_ + +## 1.3 Updating Resources via HTTP PUT + +_Implemented_ + +## 1.4 Cross Origin Resource Sharing (CORS) + +_Not implemented_ + +## 1.5 Http Status Codes + +_Implemented_ + +## 1.6 Error Response Body Format + +_Implemented_ + +## 1.7 DateTime Format + +_Implemented_ + +## 1.8 Authorization + +_Implemented_ + +Authorization is granted based on the _view_linked_issues_ and the _manage_bcf_ permission. As BCFs share part of their +data structure with WorkPackages, which enables them to be worked on by the project team just like any other work package, +a user also needs to have the _view_work_packages_ permission to have _view_linked_issues_. For _manage_bcf_ the permissions +_view_work_packages_, _add_work_packages_, _edit_work_packages_ and _delete_work_packages_ are dependently required. + +### 1.8.1 Per-Entity Authorization + +_Implemented_ + +The `authorization` field is always returned, regardless of an `includeAuthorization` query parameter. + +### 1.8.2 Determining Authorized Entity Actions + +_Implemented_ + +## 1.9 Additional Response and Request Object Properties + +The implementation relies on a client to particularly adhere to this. + +## 1.10 Binary File Uploads + +_Implemented_ + +# 2. Topologies + +_Out of scope_ + +# 3. Public Services + +## 3.1 Versions Service + +_Not implemented_ + +## 3.2 Authentication Services + +### 3.2.1 Obtaining Authentication Information + +_Implemented_ + +The following OAuth2 flows are supported: +* `authorization_code_grant` - [4.1 - Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1) +* `client_credentials` - [4.4 - Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4) + +The `clients_credentials` grant explicitly ruled out by the standard specification as not being user specific can be supported by OpenProject as the grant is mapped to a user account +when configuring the OAuth access. + +Before a client is able to perform the flows, they need to be [configured in OpenProject](https://docs.openproject.org/system-admin-guide/authentication/oauth-applications/). `bcf_v2_1` needs +to be checked for the scope. That value also needs to be provided for the scope property in OAuth requests. + +The OAuth2 flows alternatively proposed by the specification +* `implicit_grant` - [4.2 - Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2) +* `resource_owner_password_credentials_grant` - [4.3 - Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) +are not implemented. + +### 3.2.2 OAuth2 Example + +_Out of scope_ + +### 3.2.3 OAuth2 Protocol Flow - Dynamic Client Registration + +_Not implemented_ + +## 3.3 User Services + +### 3.3.1 Get current user + +_Implemented_ + +# 4. BCF Services + +## 4.1 Project Services + +The `project_id` is an integer value. However, the API also understands requests where the project identifier, e.g. `bcf_project` +is used instead of the integer within a url. So the following urls might point to the same project resource: `/api/bcf/2.1/projects/3` and `/api/bcf/2.1/projects/bcf_project`. + +### 4.1.1 GET Projects Service + +_Partly implemented_ + +The end point is implemented but lacks the `authorization` property. However, the [Project Extension Service](#414-get-project-extension-service) is completely implemented and provides the same information. + +### 4.1.2 GET Project Service + +_Partly implemented_ + +The end point is implemented but lacks the `authorization` property. However, the [Project Extension Service](#414-get-project-extension-service) is completely implemented and provides the same information. + +### 4.1.3 PUT Project Service + +_Implemented_ + +### 4.1.4 GET Project Extension Service + +_Implemented and extended_ + +However, as some end points are not implemented, the actions indicating the ability to call those end points will also not be returned, e.g. `updateDocumentReferences` + +### 4.1.5 Expressing User Authorization Through Project Extensions + +_Out of scope_ + +#### 4.1.5.1 Project + +_Implemented and extended_ + +* *viewTopic* - The ability to see topics (see [4.2.3 GET Topic Service](#423-get-topic-service)) + +#### 4.1.5.2 Topic + +_Implemented_ + +#### 4.1.5.3 Comment + +_Implemented_ + +## 4.2 Topic Services + +BCF topics are tightly coupled to work packages in OpenProject. This coupling is denoted in the `reference_links` property +of a topic which will always have a link to the work package resource in the API v3. e.g.: + + <-- other properties --> + "reference_links": [ + "/api/v3/work_packages/92" + ], + <-- other properties --> + +### 4.2.1 GET Topics Service + +_Partly implemented_ + +The following properties are not supported: +* `labels` (the property exists but cannot be written and is always empty) +* `stage` (the property exists but cannot be written and is always null) +* `bim_snippet.snippet_type` +* `bim_snippet.is_external` +* `bim_snippet.reference` +* `bim_snippet.reference_schema` + +OData sort, filtering and pagination is not supported. + +### 4.2.2 POST Topic Service + +_Partly implemented_ + +See [4.2.3 GET Topic Service](#423-get-topic-service) for details. + +Either a new work package is created or, if a work package is referenced in the `reference_links` section, a the referenced +work package is associated to the newly created topic. A work package can only be associated to one topic and vice versa. + +### 4.2.3 GET Topic Service + +_Partly implemented_ + +See [4.2.3 GET Topic Service](#423-get-topic-service) for details. + +### 4.2.4 PUT Topic Service + +_Partly implemented_ + +The reference to the work package cannot be altered. + +See [4.2.3 GET Topic Service](#423-get-topic-service) for details. + +### 4.2.5 DELETE Topic Service + +_Implemented_ + +The associated work package will also be deleted. + +### 4.2.6 GET Topic BIM Snippet Service + +_Not implemented_ + +### 4.2.7 PUT Topic BIM Snippet Service + +_Not implemented_ + +## 4.3 File Services + +### 4.3.1 GET Files (Header) Service + +_Not implemented_ + +### 4.3.2 PUT Files (Header) Service + +_Not implemented_ + +## 4.4 Comment Services + +### 4.4.1 GET Comments Service + +_Not implemented_ + +### 4.4.2 POST Comment Service + +_Not implemented_ + +### 4.4.3 GET Comment Service + +_Not implemented_ + +### 4.4.4 PUT Comment Service + +_Not implemented_ + +### 4.4.5 DELETE Comment Service + +_Not implemented_ + +### 4.4.6 Determining allowed Comment modifications + +_Not implemented_ + +## 4.5 Viewpoint Services + +### 4.5.1 GET Viewpoints Service + +_Implemented_ + +### 4.5.2 POST Viewpoint Service + +_Implemented_ + +#### 4.5.2.1 Point + +_Implemented_ + +#### 4.5.2.2 Direction + +_Implemented_ + +#### 4.5.2.3 Orthogonal camera + +_Implemented_ + +#### 4.5.2.4 Perspective camera + +_Implemented_ + +#### 4.5.2.5 Line + +_Implemented_ + +#### 4.5.2.6 Clipping plane + +_Implemented_ + +#### 4.5.2.7 Bitmap + +_Implemented_ + +#### 4.5.2.8 Snapshot + +_Implemented_ + +#### 4.5.2.9 Components + +_Implemented_ + +#### 4.5.2.10 Component + +_Implemented_ + +#### 4.5.2.11 Coloring + +_Implemented_ + +#### 4.5.2.12 Visibility + +_Implemented_ + +#### 4.5.2.13 View setup hints + +_Implemented_ + +### 4.5.3 GET Viewpoint Service + +_Implemented_ + +### 4.5.4 GET Viewpoint Snapshot Service + +_Implemented_ + +### 4.5.6 GET selected Components Service + +_Implemented_ + +### 4.5.7 GET colored Components Service + +_Implemented_ + +### 4.5.8 GET visibility of Components Service + +_Implemented_ + +## 4.6 Related Topics Services + +### 4.6.1 GET Related Topics Service + +_Not implemented_ + +### 4.6.2 PUT Related Topics Service + +_Not implemented_ + +## 4.7 Document Reference Services + +### 4.7.1 GET Document References Service + +_Not implemented_ + +### 4.7.2 POST Document Reference Service + +_Not implemented_ + +### 4.7.3 PUT Document Reference Service + +_Not implemented_ + +## 4.8 Document Services + +### 4.8.1 GET Documents Service + +_Not implemented_ + +### 4.8.2 POST Document Service + +_Not implemented_ + +### 4.8.3 GET Document Service + +_Not implemented_ + +## 4.9 Topics Events Services + +### 4.9.1 GET Topics Events Service + +_Not implemented_ + +### 4.9.2 GET Topic Events Service + +_Not implemented_ + +## 4.10 Comments Events Services + +_Not implemented_ + +### 4.10.1 GET Comments Events Service + +_Not implemented_ + +### 4.10.2 GET Comment Events Service + +_Not implemented_ diff --git a/frontend/src/app/components/wp-card-view/styles/wp-card-view-horizontal.sass b/frontend/src/app/components/wp-card-view/styles/wp-card-view-horizontal.sass index 42890d0634..d1c66e415d 100644 --- a/frontend/src/app/components/wp-card-view/styles/wp-card-view-horizontal.sass +++ b/frontend/src/app/components/wp-card-view/styles/wp-card-view-horizontal.sass @@ -1,6 +1,6 @@ .wp-cards-container.-horizontal display: grid - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) grid-column-gap: 10px grid-row-gap: 10px margin-right: 5px diff --git a/frontend/src/app/modules/fields/edit/field-types/te-work-package-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field-types/te-work-package-edit-field.component.ts index 2de300afd5..67ea126b30 100644 --- a/frontend/src/app/modules/fields/edit/field-types/te-work-package-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field-types/te-work-package-edit-field.component.ts @@ -54,6 +54,7 @@ export class TimeEntryWorkPackageEditFieldComponent extends WorkPackageEditField // existing values. if (this.referenceOutputs) { this.referenceOutputs['modeSwitch'] = (mode:TimeEntryWorkPackageAutocompleterMode) => { + this.valuesLoaded = false; let lastValue = this.requests.lastRequestedValue!; // Hack to provide a new value to "reset" the input. diff --git a/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.html b/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.html index a21fbe18be..0e23f83553 100644 --- a/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.html +++ b/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.html @@ -4,7 +4,7 @@ model: selectedOption ? selectedOption : '', required: required, disabled: inFlight, - typeahead: requests.input$, + typeahead: typeahead, id: handler.htmlId, finishedLoading: requests.loading$, hideSelected: true, diff --git a/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.ts b/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.ts index d895bd2057..5bced89019 100644 --- a/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.ts +++ b/frontend/src/app/modules/fields/edit/field-types/work-package-edit-field.component.ts @@ -57,6 +57,14 @@ export class WorkPackageEditFieldComponent extends SelectEditFieldComponent { }); } + public get typeahead() { + if (this.valuesLoaded) { + return false; + } else { + return this.requests.input$; + } + } + protected allowedValuesFilter(query?:string):{} { let filterParams = super.allowedValuesFilter(query); diff --git a/frontend/src/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass b/frontend/src/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass index b479664828..9b03d66664 100644 --- a/frontend/src/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass +++ b/frontend/src/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass @@ -23,7 +23,6 @@ .work-packages-partitioned-page--content-left, .work-packages-partitioned-page--content-right position: relative - min-width: 580px .work-packages-partitioned-page--content-left flex: 1 diff --git a/modules/bim/app/assets/stylesheets/bim/ifc_viewer/generic.sass b/modules/bim/app/assets/stylesheets/bim/ifc_viewer/generic.sass index 5cd99f6ff7..2a9139c912 100644 --- a/modules/bim/app/assets/stylesheets/bim/ifc_viewer/generic.sass +++ b/modules/bim/app/assets/stylesheets/bim/ifc_viewer/generic.sass @@ -57,6 +57,7 @@ height: 100% padding-bottom: 10px overflow: hidden + min-width: 400px .ifc-model-viewer--model-canvas width: 100% diff --git a/modules/bim/config/locales/crowdin/js-tr.yml b/modules/bim/config/locales/crowdin/js-tr.yml index 909dcdc066..d8b2320426 100644 --- a/modules/bim/config/locales/crowdin/js-tr.yml +++ b/modules/bim/config/locales/crowdin/js-tr.yml @@ -10,8 +10,8 @@ tr: show_viewpoint: 'Bakış açısını göster' delete_viewpoint: 'Bakış açısını sil' ifc_models: - empty_warning: "This project does not yet have any IFC models." - use_this_link_to_manage: "Use this link to upload and manage your IFC models" + empty_warning: "Bu projenin henüz IFC modeli yok." + use_this_link_to_manage: "IFC modellerinizi yüklemek ve yönetmek için bu bağlantıyı kullanın" models: default: 'Varsayılan IFC modelleri' manage: 'Modelleri yönetin' diff --git a/modules/bim/spec/requests/api/bcf/v2_1/shared_responses.rb b/modules/bim/spec/requests/api/bcf/v2_1/shared_responses.rb index 8019a72421..4d0eddbac1 100644 --- a/modules/bim/spec/requests/api/bcf/v2_1/shared_responses.rb +++ b/modules/bim/spec/requests/api/bcf/v2_1/shared_responses.rb @@ -27,10 +27,37 @@ #++ shared_examples_for 'bcf api successful response' do + def expect_identical_without_time(subject, expected_body) + # Remove modified date + body = Array.wrap(JSON.parse(subject.body)) + Array.wrap(expected_body).each_with_index do |expected_item, index| + subject_body = body[index] + + expected_item.stringify_keys! + subject_modified_date = subject_body.delete('modified_date')&.to_time + expected_modified_date = expected_item.delete('modified_date')&.to_time + + if expected_modified_date + expect(subject_modified_date).to be_within(10.seconds).of(expected_modified_date) + else + expect(subject_modified_date).to eql(expected_modified_date) + end + + expect(subject_body.to_json).to be_json_eql(expected_item.to_json) + end + end + it 'responds correctly with the expected body', :aggregate_failures do expect(subject.status) .to eql(defined?(expected_status) ? expected_status : 200) - expect(subject.body).to be_json_eql(expected_body.to_json) + + if expected_body.nil? + expect("").to be_json_eql(expected_body.to_json) + else + expect_identical_without_time(subject, expected_body) + end + + expect(subject.headers['Content-Type']).to eql 'application/json; charset=utf-8' unless defined?(no_content) end end diff --git a/modules/costs/app/controllers/cost_objects_controller.rb b/modules/costs/app/controllers/cost_objects_controller.rb index 4c3c76f39e..9287b1478c 100644 --- a/modules/costs/app/controllers/cost_objects_controller.rb +++ b/modules/costs/app/controllers/cost_objects_controller.rb @@ -199,9 +199,13 @@ class CostObjectsController < ApplicationController @unit = cost_type.try(:unit_plural) || '' end - response = { "#{@element_id}_unit_name" => h(@unit) } + response = { + "#{@element_id}_unit_name" => h(@unit), + "#{@element_id}_currency" => Setting.plugin_openproject_costs['costs_currency'] + } if current_user.allowed_to?(:view_cost_rates, @project) response["#{@element_id}_costs"] = number_to_currency(@costs) + response["#{@element_id}_cost_value"] = @costs end respond_to do |format| @@ -222,9 +226,13 @@ class CostObjectsController < ApplicationController @costs = 0.0 end - response = { "#{@element_id}_unit_name" => h(@unit) } + response = { + "#{@element_id}_unit_name" => h(@unit), + "#{@element_id}_currency" => Setting.plugin_openproject_costs['costs_currency'] + } if current_user.allowed_to?(:view_hourly_rates, @project) response["#{@element_id}_costs"] = number_to_currency(@costs) + response["#{@element_id}_cost_value"] = @costs end respond_to do |format| diff --git a/modules/costs/app/views/cost_objects/items/_labor_budget_item.html.erb b/modules/costs/app/views/cost_objects/items/_labor_budget_item.html.erb index 49591ceb12..b690c0a5e2 100644 --- a/modules/costs/app/views/cost_objects/items/_labor_budget_item.html.erb +++ b/modules/costs/app/views/cost_objects/items/_labor_budget_item.html.erb @@ -80,9 +80,16 @@ See docs/COPYRIGHT.rdoc for more details. <% if templated == false && !labor_budget_item.new_record? && labor_budget_item.overridden_budget? %> <%= cost_form.hidden_field :budget, value: labor_budget_item.budget %> <% end %> - " obj-name="<%= "#{name_prefix}[budget]" %>"> + + <% cost_value = labor_budget_item.budget || labor_budget_item.calculated_costs(@cost_object.fixed_date, @cost_object.project_id) %> + <%= cost_form.hidden_field :currency, index: id_or_index, value: Setting.plugin_openproject_costs['costs_currency'] %> + "> " class="costs--edit-planned-costs-btn icon-context icon-edit" title="<%= t(:help_click_to_edit) %>"> - <%= number_to_currency(labor_budget_item.budget || labor_budget_item.calculated_costs(@cost_object.fixed_date, @cost_object.project_id)) if labor_budget_item.costs_visible_by?(User.current) %> + <% if labor_budget_item.costs_visible_by?(User.current) %> + <%= cost_form.hidden_field :cost_value, index: id_or_index, value: cost_value %> + <%= number_to_currency(cost_value) %> + <% end %> diff --git a/modules/costs/app/views/cost_objects/items/_material_budget_item.html.erb b/modules/costs/app/views/cost_objects/items/_material_budget_item.html.erb index 0566c6afb0..427e861f85 100644 --- a/modules/costs/app/views/cost_objects/items/_material_budget_item.html.erb +++ b/modules/costs/app/views/cost_objects/items/_material_budget_item.html.erb @@ -78,14 +78,17 @@ See docs/COPYRIGHT.rdoc for more details. <% if User.current.allowed_to? :view_cost_rates, @project %> - <% obj_id = "#{id_prefix}_costs" %> <%# Keep current budget as hidden field because otherwise they will be overridden %> <% if templated == false && !material_budget_item.new_record? && material_budget_item.overridden_budget? %> <%= cost_form.hidden_field :budget, value: material_budget_item.budget %> <% end %> - "> - - <%= number_to_currency(material_budget_item.budget || material_budget_item.calculated_costs(@cost_object.fixed_date)) %> + <% cost_value = material_budget_item.budget || material_budget_item.calculated_costs(@cost_object.fixed_date) %> + <%= cost_form.hidden_field :currency, index: id_or_index, value: Setting.plugin_openproject_costs['costs_currency'] %> + <%= cost_form.hidden_field :cost_value, index: id_or_index, value: cost_value %> + "> + + <%= number_to_currency(cost_value) %> diff --git a/modules/costs/app/views/costlog/edit.html.erb b/modules/costs/app/views/costlog/edit.html.erb index b7d5a449e0..95c305bc17 100644 --- a/modules/costs/app/views/costlog/edit.html.erb +++ b/modules/costs/app/views/costlog/edit.html.erb @@ -113,7 +113,8 @@ See docs/COPYRIGHT.rdoc for more details.
- + <% if User.current.allowed_to? :view_cost_rates, @cost_entry.project %> <%= number_to_currency(@cost_entry.real_costs) %> diff --git a/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts b/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts index 7446396731..895fc35a8a 100644 --- a/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts +++ b/modules/costs/frontend/module/augment/cost-budget-subform.augment.service.ts @@ -90,7 +90,12 @@ export class CostBudgetSubformAugmentService { .subscribe( (data:any) => { _.each(data, (val:string, selector:string) => { - jQuery('#' + selector).html(val); + let element = document.getElementById(selector) as HTMLElement|HTMLInputElement|undefined; + if (element instanceof HTMLInputElement) { + element.value = val; + } else if (element) { + element.textContent = val; + } }); }, (error:any) => this.halNotification.handleRawError(error) diff --git a/modules/costs/frontend/module/augment/planned-costs-form.ts b/modules/costs/frontend/module/augment/planned-costs-form.ts index 560c79852c..def2085cf9 100644 --- a/modules/costs/frontend/module/augment/planned-costs-form.ts +++ b/modules/costs/frontend/module/augment/planned-costs-form.ts @@ -42,35 +42,36 @@ export class PlannedCostsFormAugment { constructor(public $element:JQuery) { this.objId = this.$element.attr('obj-id')!; this.objName = this.$element.attr('obj-name')!; - this.obj = jQuery(`#${this.objId}`) as any; + this.obj = jQuery(`#${this.objId}_costs`) as any; - this.makeEditable('#' + this.objId, this.objName); + this.makeEditable(); } - private getCurrencyValue(str:string) { - var result = str.match(/^\s*(([0-9]+[.,])+[0-9]+) (.+)\s*/); - return result ? new Array(result[1], result[3]) : new Array(str, ""); - } - - public makeEditable(id:string, name:string) { + public makeEditable() { this.edit_and_focus(); } private edit_and_focus() { this.edit(); - jQuery('#' + this.objId + '_edit').trigger('focus'); - jQuery('#' + this.objId + '_edit').trigger('select'); + jQuery('#' + this.objId + '_costs_edit').trigger('focus'); + jQuery('#' + this.objId + '_costs_edit').trigger('select'); + } + + private getCurrency() { + return jQuery('#' + this.objId + '_currency').val(); + } + + private getValue() { + return jQuery('#' + this.objId + '_cost_value').val(); } private edit() { this.obj.hide(); - let obj_value = this.obj[0].innerHTML; let id = this.obj[0].id; - let parsed = this.getCurrencyValue(obj_value); - let value = parsed[0]; - let currency = parsed[1]; + let currency = this.getCurrency(); + let value = this.getValue(); let name = this.objName; let template = ` @@ -87,12 +88,11 @@ export class PlannedCostsFormAugment { `; - jQuery(template).insertAfter(this.obj); let that = this; - jQuery('#' + id + '_cancel').on('click', function () { - jQuery('#' + id + '_section').remove(); + jQuery('#' + id + '_costs_cancel').on('click', function () { + jQuery('#' + id + '_costs_section').remove(); that.obj.show(); return false; }); diff --git a/modules/costs/spec/features/budgets/update_budget_spec.rb b/modules/costs/spec/features/budgets/update_budget_spec.rb index b0f53b2b9d..030b9fd834 100644 --- a/modules/costs/spec/features/budgets/update_budget_spec.rb +++ b/modules/costs/spec/features/budgets/update_budget_spec.rb @@ -29,7 +29,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') describe 'updating a budget', type: :feature, js: true do - let(:project) { FactoryBot.create :project_with_types } + let(:project) { FactoryBot.create :project_with_types, enabled_module_names: %i[costs_module] } let(:user) { FactoryBot.create :admin } let(:budget) { FactoryBot.create :cost_object, author: user, project: project } @@ -79,14 +79,14 @@ describe 'updating a budget', type: :feature, js: true do let(:material_budget_item) do FactoryBot.create :material_budget_item, units: 3, - cost_type: cost_type, - cost_object: budget + cost_type: cost_type, + cost_object: budget end let(:labor_budget_item) do FactoryBot.create :labor_budget_item, hours: 5, - user: user, - cost_object: budget + user: user, + cost_object: budget end let(:budget_page) { Pages::EditBudget.new budget.id } @@ -110,10 +110,10 @@ describe 'updating a budget', type: :feature, js: true do budget_page.expect_planned_costs! type: :labor, row: 1, expected: '125.00 EUR' budget_page.edit_unit_costs! material_budget_item.id, units: 5, - comment: 'updated num stimpaks' + comment: 'updated num stimpaks' budget_page.edit_labor_costs! labor_budget_item.id, hours: 3, - user_name: user.name, - comment: 'updated treatment duration' + user_name: user.name, + comment: 'updated treatment duration' # Test for updated planned costs (Regression #31247) budget_page.expect_planned_costs! type: :material, row: 1, expected: '250.00 EUR' @@ -136,8 +136,8 @@ describe 'updating a budget', type: :feature, js: true do context 'with two material budget items' do let!(:material_budget_item_2) do FactoryBot.create :material_budget_item, units: 5, - cost_type: cost_type, - cost_object: budget + cost_type: cost_type, + cost_object: budget end it 'keeps previous planned material costs (Regression test #27692)' do @@ -169,13 +169,52 @@ describe 'updating a budget', type: :feature, js: true do expect(material_budget_item_2.overridden_budget?).to be_truthy expect(material_budget_item_2.costs).to eq(543.0) end + + context 'with a reversed currency format' do + + before do + allow(Setting) + .to receive(:plugin_openproject_costs) + .and_return({costs_currency_format: '%u %n', costs_currency: 'USD'}.with_indifferent_access) + end + + it 'can still update budgets (Regression test #32664)' do + budget_page.visit! + click_on 'Update' + + # Update first element + budget_page.edit_planned_costs! material_budget_item.id, type: :material, costs: 123 + expect(budget_page).to have_content('Successful update') + expect(page).to have_selector('tbody td.currency', text: 'USD 123.00') + + click_on 'Update' + + # Update second element + budget_page.edit_planned_costs! material_budget_item_2.id, type: :material, costs: 543 + expect(budget_page).to have_content('Successful update') + expect(page).to have_selector('tbody td.currency', text: 'USD 123.00') + expect(page).to have_selector('tbody td.currency', text: 'USD 543.00') + + # Expect overridden costs on both + material_budget_item.reload + material_budget_item_2.reload + + # Expect budget == costs + expect(material_budget_item.budget).to eq(123.0) + expect(material_budget_item.overridden_budget?).to be_truthy + expect(material_budget_item.costs).to eq(123.0) + expect(material_budget_item_2.budget).to eq(543.0) + expect(material_budget_item_2.overridden_budget?).to be_truthy + expect(material_budget_item_2.costs).to eq(543.0) + end + end end context 'with two labor budget items' do let!(:labor_budget_item_2) do FactoryBot.create :labor_budget_item, hours: 5, - user: user, - cost_object: budget + user: user, + cost_object: budget end it 'keeps previous planned labor costs (Regression test #27692)' do @@ -207,6 +246,45 @@ describe 'updating a budget', type: :feature, js: true do expect(labor_budget_item_2.overridden_budget?).to be_truthy expect(labor_budget_item_2.costs).to eq(987.0) end + + context 'with a reversed currency format' do + + before do + allow(Setting) + .to receive(:plugin_openproject_costs) + .and_return({costs_currency_format: '%u %n', costs_currency: 'USD'}.with_indifferent_access) + end + + it 'can still update budgets (Regression test #32664)' do + budget_page.visit! + click_on 'Update' + + # Update first element + budget_page.edit_planned_costs! labor_budget_item.id, type: :labor, costs: 456 + expect(budget_page).to have_content('Successful update') + expect(page).to have_selector('tbody td.currency', text: 'USD 456.00') + + click_on 'Update' + + # Update second element + budget_page.edit_planned_costs! labor_budget_item_2.id, type: :labor, costs: 987 + expect(budget_page).to have_content('Successful update') + expect(page).to have_selector('tbody td.currency', text: 'USD 456.00') + expect(page).to have_selector('tbody td.currency', text: 'USD 987.00') + + # Expect overridden costs on both + labor_budget_item.reload + labor_budget_item_2.reload + + # Expect budget == costs + expect(labor_budget_item.budget).to eq(456.0) + expect(labor_budget_item.overridden_budget?).to be_truthy + expect(labor_budget_item.costs).to eq(456.0) + expect(labor_budget_item_2.budget).to eq(987.0) + expect(labor_budget_item_2.overridden_budget?).to be_truthy + expect(labor_budget_item_2.costs).to eq(987.0) + end + end end it 'removes existing cost items' do diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index 8c3c039ec0..cc8f9a1c53 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -276,6 +276,14 @@ describe Attachment, type: :model do expect(query).to include "X-Amz-Expires=3600" end end + + context 'with expiry time exeeding maximum' do + let(:url_options) { { expires_in: 1.year } } + + it "uses the allowed max" do + expect(query).to include "X-Amz-Expires=604799" + end + end end describe "for an image file" do