mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Fix interaction between Filters::FilterForm and the WorkPackage Query
This commit is contained in:
@@ -106,11 +106,12 @@ class Filters::Inputs::BaseFilterForm < ApplicationForm
|
||||
"filter-name": @filter.name
|
||||
}
|
||||
) do |select|
|
||||
@filter.available_operators.each do |op|
|
||||
@filter.available_operators.each do |operator|
|
||||
select.option(
|
||||
label: op.human_name,
|
||||
value: op.symbol,
|
||||
selected: op.symbol == selected_operator
|
||||
label: operator.human_name,
|
||||
value: operator.symbol,
|
||||
selected: operator.symbol == selected_operator,
|
||||
**(operator.requires_value? ? {} : { "data-no-value": true })
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,6 +37,27 @@ module Queries::WorkPackages::Filter::FilterForWpMixin
|
||||
raise NotImplementedError, "There would be too many candidates"
|
||||
end
|
||||
|
||||
# Tell `Filters::FilterForm`'s dispatch to render these filters with a
|
||||
# server-side autocompleter (the candidate set is too large for an inline
|
||||
# `<select>`). Mirrors what the legacy Angular WP filter UI does — see
|
||||
# `filter-searchable-multiselect-value.component.html`, which renders an
|
||||
# `op-autocompleter` against the `work_packages` resource using the
|
||||
# `typeahead` filter for search.
|
||||
#
|
||||
# Subclasses that override `type` to something other than `:list` (e.g.
|
||||
# `SearchFilter` / `SubjectOrIdFilter` use `:search`, `RelatableFilter`
|
||||
# uses `:relation`) get an empty hash so the dispatch falls back to the
|
||||
# appropriate non-autocomplete input.
|
||||
def autocomplete_options
|
||||
return {} unless type == :list
|
||||
|
||||
{
|
||||
component: "opce-autocompleter",
|
||||
resource: "work_packages",
|
||||
searchKey: "typeahead"
|
||||
}
|
||||
end
|
||||
|
||||
def value_objects
|
||||
objects = visible_scope.find(no_templated_values)
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ class Query < ApplicationRecord
|
||||
validate :validate_timestamps
|
||||
|
||||
include Scopes::Scoped
|
||||
|
||||
scopes :visible,
|
||||
:having_views
|
||||
|
||||
@@ -217,6 +218,16 @@ class Query < ApplicationRecord
|
||||
filters.detect { |f| f.name == name }
|
||||
end
|
||||
|
||||
# The manual-sort filter is added programmatically when the user drags
|
||||
# work packages to reorder them — it has no operator/value UI of its own
|
||||
# (type `:empty_value`), so it doesn't belong in the picker that
|
||||
# `Filters::FilterForm` builds. Mirrors how
|
||||
# `Queries::Filters::AvailableFilters#available_advanced_filters` already
|
||||
# excludes the inline `name_and_identifier` quick-filter on projects.
|
||||
def available_advanced_filters
|
||||
super.grep_v(::Queries::WorkPackages::Filter::ManualSortFilter)
|
||||
end
|
||||
|
||||
def normalized_name
|
||||
name.parameterize.underscore
|
||||
end
|
||||
|
||||
@@ -69,10 +69,10 @@ en:
|
||||
not_available_in_semantic_mode: "Reserved identifiers are only available in classic identifier mode."
|
||||
filter_label: "Filter identifiers"
|
||||
btn_release: "Release"
|
||||
released_notice: "Identifier \"%{identifier}\" has been released."
|
||||
released_notice: 'Identifier "%{identifier}" has been released.'
|
||||
dialog:
|
||||
title: "Release identifier"
|
||||
heading: "Release \"%{identifier}\"?"
|
||||
heading: 'Release "%{identifier}"?'
|
||||
description: "Releasing this identifier cannot be undone. External links and integrations using it will stop resolving, and the name becomes available for any new project to claim."
|
||||
checkbox_label: "I understand that this cannot be undone."
|
||||
confirm_button: "Release identifier"
|
||||
@@ -1548,7 +1548,7 @@ en:
|
||||
one: "Successfully copied workflow to '%{type_name}' type."
|
||||
other: "Successfully copied workflow to %{count} types."
|
||||
new:
|
||||
title: "Copy workflow of \"%{source_type}\""
|
||||
title: 'Copy workflow of "%{source_type}"'
|
||||
form:
|
||||
matrix_caption: "Workflow matrix"
|
||||
matrix_caption_assignee: "Workflow matrix for assignee"
|
||||
@@ -1829,7 +1829,7 @@ en:
|
||||
changeset:
|
||||
repository: "Repository"
|
||||
comment:
|
||||
commented: "Commented" # an object that this comment belongs to
|
||||
commented: "Commented" # an object that this comment belongs to
|
||||
custom_action:
|
||||
actions: "Actions"
|
||||
custom_field:
|
||||
@@ -2124,7 +2124,7 @@ en:
|
||||
redirect_existing_links: "Redirect existing links"
|
||||
text: "Page content"
|
||||
work_package:
|
||||
ancestor: "Descendants of" # used for filtering of work packages that are descendants of a given work package
|
||||
ancestor: "Descendants of" # used for filtering of work packages that are descendants of a given work package
|
||||
begin_insertion: "Begin of the insertion"
|
||||
begin_deletion: "Begin of the deletion"
|
||||
children: "Subelements"
|
||||
@@ -2142,13 +2142,14 @@ en:
|
||||
false: "working days only"
|
||||
true: "include non-working days"
|
||||
journal_internal: Internal Journal
|
||||
notify: "Notify" # used in custom actions
|
||||
notify: "Notify" # used in custom actions
|
||||
parent: "Parent"
|
||||
parent_issue: "Parent"
|
||||
parent_work_package: "Parent"
|
||||
priority: "Priority"
|
||||
progress: "% Complete"
|
||||
readonly: "Read only"
|
||||
relatable: "Relatable to"
|
||||
remaining_hours: "Remaining work"
|
||||
remaining_time: "Remaining work"
|
||||
sequence_number: "Sequence number"
|
||||
@@ -4861,8 +4862,8 @@ en:
|
||||
A new user (%{email}) tried to create an account on an OpenProject environment that you manage (%{host}).
|
||||
The user cannot activate their account since the user limit has been reached.
|
||||
steps:
|
||||
a: "Upgrade your payment plan ([here](upgrade_url))" # here turned into a link
|
||||
b: "Lock or delete an existing user ([here](users_url))" # here turned into a link
|
||||
a: "Upgrade your payment plan ([here](upgrade_url))" # here turned into a link
|
||||
b: "Lock or delete an existing user ([here](users_url))" # here turned into a link
|
||||
label: "To allow the user to sign in you can either: "
|
||||
|
||||
more_actions: "More functions"
|
||||
|
||||
@@ -153,7 +153,7 @@ export default class FiltersFormController extends Controller {
|
||||
const filterName = target.getAttribute('data-filter-name');
|
||||
if (filterName) {
|
||||
const operator = this.findTargetByName(filterName, this.operatorTargets);
|
||||
if (operator && this.noValueOperators.includes(operator.value)) {
|
||||
if (operator && this.operatorRequiresNoValue(operator)) {
|
||||
target.setAttribute('hidden', '');
|
||||
}
|
||||
}
|
||||
@@ -354,16 +354,24 @@ export default class FiltersFormController extends Controller {
|
||||
inputElement.dispatchEvent(inputEvent);
|
||||
}
|
||||
|
||||
private readonly noValueOperators = ['*', '!*', 't', 'w'];
|
||||
private readonly daysOperators = ['>t-', '<t-', 't-', '<t+', '>t+', 't+'];
|
||||
private readonly onDateOperator = '=d';
|
||||
private readonly betweenDatesOperator = '<>d';
|
||||
|
||||
// Whether the operator currently selected in `operatorElement` declares
|
||||
// itself value-less. Driven by the `data-no-value` attribute that
|
||||
// `Filters::Inputs::BaseFilterForm#add_operator` emits on each `<option>`
|
||||
// whose `Operator.requires_value?` is false — keeps the symbol list in
|
||||
// one place (Ruby) instead of duplicating it here.
|
||||
private operatorRequiresNoValue(operatorElement:HTMLSelectElement):boolean {
|
||||
return operatorElement.selectedOptions[0]?.hasAttribute('data-no-value') ?? false;
|
||||
}
|
||||
|
||||
setValueVisibility({ target, params: { filterName } }:{ target:HTMLSelectElement, params:{ filterName:string } }) {
|
||||
const selectedOperator = target.value;
|
||||
const valueContainer = this.findTargetByName(filterName, this.filterValueContainerTargets);
|
||||
if (valueContainer) {
|
||||
if (this.noValueOperators.includes(selectedOperator)) {
|
||||
if (this.operatorRequiresNoValue(target)) {
|
||||
valueContainer.setAttribute('hidden', '');
|
||||
} else {
|
||||
valueContainer.removeAttribute('hidden');
|
||||
@@ -456,7 +464,9 @@ export default class FiltersFormController extends Controller {
|
||||
const type = filter.getAttribute('data-filter-type');
|
||||
const operator = filter.getAttribute('data-filter-operator');
|
||||
if (name && type && operator) {
|
||||
const value = this.parseFilterValue(filter, name, type, operator) as string[]|null;
|
||||
// Quick filters carry an operator that always requires a value
|
||||
// (the inline search input).
|
||||
const value = this.parseFilterValue(filter, name, type, operator, false) as string[]|null;
|
||||
|
||||
if (value) {
|
||||
filters.push({ name, operator, value });
|
||||
@@ -473,11 +483,13 @@ export default class FiltersFormController extends Controller {
|
||||
advancedFilters.forEach((filter) => {
|
||||
const filterName = filter.getAttribute('data-filter-name')!;
|
||||
const filterType = filter.getAttribute('data-filter-type');
|
||||
const parsedOperator = this.findTargetByName(filterName, this.operatorTargets)?.value;
|
||||
const operatorTarget = this.findTargetByName(filterName, this.operatorTargets);
|
||||
const parsedOperator = operatorTarget?.value;
|
||||
const valueContainer = this.findTargetByName(filterName, this.filterValueContainerTargets);
|
||||
|
||||
if (valueContainer && filterName && filterType && parsedOperator) {
|
||||
const parsedValue = this.parseFilterValue(valueContainer, filterName, filterType, parsedOperator) as string[]|null;
|
||||
if (valueContainer && filterName && filterType && parsedOperator && operatorTarget) {
|
||||
const requiresNoValue = this.operatorRequiresNoValue(operatorTarget);
|
||||
const parsedValue = this.parseFilterValue(valueContainer, filterName, filterType, parsedOperator, requiresNoValue) as string[]|null;
|
||||
|
||||
if (parsedValue) {
|
||||
filters.push({ name: filterName, operator: parsedOperator, value: parsedValue });
|
||||
@@ -509,10 +521,9 @@ export default class FiltersFormController extends Controller {
|
||||
return value && value.length > 0 ? value.replace(/"/g, '\\"') : '';
|
||||
}
|
||||
|
||||
private readonly operatorsWithoutValues = ['*', '!*', 't', 'w'];
|
||||
private readonly dateFilterTypes = ['datetime_past', 'date'];
|
||||
|
||||
private parseFilterValue(valueContainer:HTMLElement, filterName:string, filterType:string, operator:string) {
|
||||
private parseFilterValue(valueContainer:HTMLElement, filterName:string, filterType:string, operator:string, requiresNoValue:boolean) {
|
||||
const checkbox = valueContainer.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||
|
||||
if (checkbox) {
|
||||
@@ -523,7 +534,7 @@ export default class FiltersFormController extends Controller {
|
||||
return (valueContainer.querySelector<HTMLInputElement>('input[name="value"]'))?.value.split(',');
|
||||
}
|
||||
|
||||
if (this.operatorsWithoutValues.includes(operator)) {
|
||||
if (requiresNoValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -130,10 +130,7 @@ form forwards it as `appendTo` to every autocompleter it renders.
|
||||
| You want… | Use |
|
||||
|----------------------------------------------------------|---------------------------|
|
||||
| The standard OpenProject filter panel on an index page | `Filter::FilterComponent` |
|
||||
| Filters inside a dialog or a non-filter form | `Filters::FilterForm` |
|
||||
| Coordination with a quick filter input in a sub-header | `Filter::FilterComponent` (the sub-header attaches the controller) |
|
||||
| The serialized filter string in `params` on submit | `Filters::FilterForm` + `hidden_input_name:` |
|
||||
| Autocompleter dropdowns to escape a clipping container | `Filters::FilterForm` + `autocomplete_append_to:` |
|
||||
| Filters inside a dialog or a non-filter form | `Filters::FilterForm` + `hidden_input_name:` |
|
||||
|
||||
## Stimulus controller placement
|
||||
|
||||
@@ -164,6 +161,8 @@ default.
|
||||
with the same signature, so passing a `Query` (or any of its subclasses)
|
||||
to `FilterForm` works exactly like passing a `BaseQuery` subclass.
|
||||
|
||||
<%= embed OpenProject::Filter::FilterFormPreview, :for_a_work_package_query, panels: %i[preview source] %>
|
||||
|
||||
What `FilterForm` does *not* do for you on the legacy side: parsing the
|
||||
form submission back into a `Query#filters` collection. The work-package
|
||||
filter pipeline still uses its own serialization (URL `filters=[...]`
|
||||
|
||||
@@ -33,12 +33,14 @@ module OpenProject
|
||||
# @logical_path OpenProject/Filter
|
||||
class FilterFormPreview < Lookbook::Preview
|
||||
# @display min_height 600px
|
||||
# Using the `UserQuery` as an example.
|
||||
def default
|
||||
render_with_template(locals: { query: UserQuery.new })
|
||||
end
|
||||
|
||||
# @display min_height 600px
|
||||
# @label With an active filter
|
||||
# Using the `UserQuery` as an example.
|
||||
def with_active_filter
|
||||
query = UserQuery.new
|
||||
query.where(:login, "~", ["admin"])
|
||||
@@ -48,6 +50,7 @@ module OpenProject
|
||||
# @display min_height 600px
|
||||
# @label Hidden input mode
|
||||
# @param output_format [Symbol] select [json, params]
|
||||
# Using the `UserQuery` as an example.
|
||||
# This also renders a field that shows the value of the hidden input to show the different serialization formats.
|
||||
def with_hidden_input(output_format: :json)
|
||||
render_with_template(locals: { query: UserQuery.new, output_format: output_format.to_sym })
|
||||
@@ -55,9 +58,20 @@ module OpenProject
|
||||
|
||||
# @display min_height 600px
|
||||
# @label Combined with other inputs
|
||||
# Using the `UserQuery` as an example
|
||||
def combined_with_other_inputs
|
||||
render_with_template(locals: { query: UserQuery.new })
|
||||
end
|
||||
|
||||
# @display min_height 600px
|
||||
# @label Work package query (legacy `Query`)
|
||||
# Renders against the legacy work-package `Query` (the one that # predates `Queries::BaseQuery`).
|
||||
def for_a_work_package_query
|
||||
query = ::Query.new
|
||||
query.add_filter(:status_id, "o", [])
|
||||
query.add_filter(:due_date, "=d", [Date.current.end_of_year.iso8601])
|
||||
render_with_template(locals: { query: query })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<%# `Query` is the legacy work-package query model. `Filters::FilterForm` %>
|
||||
<%# works against it the same way it does against `Queries::BaseQuery` %>
|
||||
<%# subclasses — the only requirement is `available_advanced_filters`, %>
|
||||
<%# `filters`, and `find_active_filter(name)`, all of which `Query` %>
|
||||
<%# exposes (the last one was mirrored from `BaseQuery` to bring the %>
|
||||
<%# legacy query in line with the new form). %>
|
||||
<%= primer_form_with(url: "/foo", method: :post) do |f| %>
|
||||
<%=
|
||||
render(
|
||||
Filters::FilterForm.new(
|
||||
f,
|
||||
query: query,
|
||||
wrap_with_controller: true
|
||||
)
|
||||
)
|
||||
%>
|
||||
<% end %>
|
||||
@@ -31,10 +31,15 @@ en:
|
||||
authentication_method: Authentication Method
|
||||
creator: Creator
|
||||
drive: Drive ID
|
||||
file_link_origin: "File link origin ID"
|
||||
host: Host
|
||||
linkable_to_storage: "Linkable to storage"
|
||||
linkable_to_storage_url: "Linkable to storage URL"
|
||||
name: Name
|
||||
password: Application password
|
||||
provider_type: Provider type
|
||||
storage: "Storage"
|
||||
storage_url: "Storage URL"
|
||||
tenant: Directory (tenant) ID
|
||||
errors:
|
||||
messages:
|
||||
@@ -72,18 +77,18 @@ en:
|
||||
errors:
|
||||
too_many_elements_created_at_once: Too many elements created at once. Expected %{max} at most, got %{actual}.
|
||||
external_file_storages: External file storages
|
||||
permission_create_files: 'Automatically managed project folders: Create files'
|
||||
permission_create_files: "Automatically managed project folders: Create files"
|
||||
permission_create_files_explanation: This permission is only available for Nextcloud storages
|
||||
permission_delete_files: 'Automatically managed project folders: Delete files'
|
||||
permission_delete_files: "Automatically managed project folders: Delete files"
|
||||
permission_delete_files_explanation: This permission is only available for Nextcloud storages
|
||||
permission_header_for_project_module_storages: Automatically managed project folders
|
||||
permission_manage_file_links: Manage file links
|
||||
permission_manage_files_in_project: Manage files in project
|
||||
permission_read_files: 'Automatically managed project folders: Read files'
|
||||
permission_share_files: 'Automatically managed project folders: Share files'
|
||||
permission_read_files: "Automatically managed project folders: Read files"
|
||||
permission_share_files: "Automatically managed project folders: Share files"
|
||||
permission_share_files_explanation: This permission is only available for Nextcloud storages
|
||||
permission_view_file_links: View file links
|
||||
permission_write_files: 'Automatically managed project folders: Write files'
|
||||
permission_write_files: "Automatically managed project folders: Write files"
|
||||
project_module_storages: Files
|
||||
project_storages:
|
||||
edit_project_folder:
|
||||
@@ -101,25 +106,25 @@ en:
|
||||
services:
|
||||
attributes:
|
||||
nextcloud_sync_service:
|
||||
add_user_to_group: 'Add User to Group:'
|
||||
create_folder: 'Managed Project Folder Creation:'
|
||||
ensure_root_folder_permissions: 'Set Base Folder Permissions:'
|
||||
hide_inactive_folders: 'Hide Inactive Folders Step:'
|
||||
remote_folders: 'Read contents of the team folder:'
|
||||
remove_user_from_group: 'Remove User from Group:'
|
||||
rename_project_folder: 'Rename managed project Folder:'
|
||||
add_user_to_group: "Add User to Group:"
|
||||
create_folder: "Managed Project Folder Creation:"
|
||||
ensure_root_folder_permissions: "Set Base Folder Permissions:"
|
||||
hide_inactive_folders: "Hide Inactive Folders Step:"
|
||||
remote_folders: "Read contents of the team folder:"
|
||||
remove_user_from_group: "Remove User from Group:"
|
||||
rename_project_folder: "Rename managed project Folder:"
|
||||
one_drive_sync_service:
|
||||
create_folder: 'Managed Project Folder Creation:'
|
||||
ensure_root_folder_permissions: 'Set Base Folder Permissions:'
|
||||
hide_inactive_folders: 'Hide Inactive Folders Step:'
|
||||
remote_folders: 'Read contents of the drive root folder:'
|
||||
rename_project_folder: 'Rename managed project Folder:'
|
||||
create_folder: "Managed Project Folder Creation:"
|
||||
ensure_root_folder_permissions: "Set Base Folder Permissions:"
|
||||
hide_inactive_folders: "Hide Inactive Folders Step:"
|
||||
remote_folders: "Read contents of the drive root folder:"
|
||||
rename_project_folder: "Rename managed project Folder:"
|
||||
sharepoint_sync_service:
|
||||
create_folder: 'Managed Project Folder Creation:'
|
||||
ensure_root_folder_permissions: 'Set Base Folder Permissions:'
|
||||
hide_inactive_folders: 'Hide Inactive Folders Step:'
|
||||
remote_folders: 'Read contents of the drive root folder:'
|
||||
rename_project_folder: 'Rename managed project Folder:'
|
||||
create_folder: "Managed Project Folder Creation:"
|
||||
ensure_root_folder_permissions: "Set Base Folder Permissions:"
|
||||
hide_inactive_folders: "Hide Inactive Folders Step:"
|
||||
remote_folders: "Read contents of the drive root folder:"
|
||||
rename_project_folder: "Rename managed project Folder:"
|
||||
errors:
|
||||
messages:
|
||||
error: An unexpected error occurred. Please check OpenProject logs for more information or contact an administrator
|
||||
@@ -135,8 +140,8 @@ en:
|
||||
nextcloud_sync_service:
|
||||
attributes:
|
||||
add_user_to_group:
|
||||
conflict: 'The user %{user} could not be added to the %{group} group for the following reason: %{reason}'
|
||||
failed_to_add: 'The user %{user} could not be added to the %{group} group for the following reason: %{reason}'
|
||||
conflict: "The user %{user} could not be added to the %{group} group for the following reason: %{reason}"
|
||||
failed_to_add: "The user %{user} could not be added to the %{group} group for the following reason: %{reason}"
|
||||
create_folder:
|
||||
conflict: The folder %{folder_name} already exists on %{parent_location}.
|
||||
not_found: "%{parent_location} wasn't found."
|
||||
@@ -149,8 +154,8 @@ en:
|
||||
not_allowed: The %{username} doesn't have access to the %{group_folder} folder. Please check the folder permissions on Nextcloud.
|
||||
not_found: "%{group_folder} folder wasn't found. Please check your Nextcloud setup."
|
||||
remove_user_from_group:
|
||||
conflict: 'The user %{user} could not be removed from the %{group} group for the following reason: %{reason}'
|
||||
failed_to_remove: 'The user %{user} could not be removed from the %{group} group for the following reason: %{reason}'
|
||||
conflict: "The user %{user} could not be removed from the %{group} group for the following reason: %{reason}"
|
||||
failed_to_remove: "The user %{user} could not be removed from the %{group} group for the following reason: %{reason}"
|
||||
rename_project_folder:
|
||||
conflict: OpenProject could not rename the project folder to %{current_path} as a folder with the same name already exists
|
||||
forbidden: OpenProject user does not have access to %{current_path} folder.
|
||||
@@ -277,7 +282,7 @@ en:
|
||||
download_report: Download
|
||||
rerun_checks: Re-run all checks
|
||||
run_checks: Run checks now
|
||||
checked: 'Last check: %{datetime}'
|
||||
checked: "Last check: %{datetime}"
|
||||
checks:
|
||||
ampf_configuration:
|
||||
client_folder_creation: Automatic folder creation
|
||||
@@ -287,7 +292,7 @@ en:
|
||||
header: Automatically managed project folders
|
||||
project_folders_exist: Project folders exist
|
||||
project_folders_linked: Project folders linked
|
||||
team_folder_app: 'Dependency: Team Folders'
|
||||
team_folder_app: "Dependency: Team Folders"
|
||||
team_folder_contents: Team folder content
|
||||
team_folder_presence: Team folder exists
|
||||
userless_access: Server-side request authentication
|
||||
@@ -317,7 +322,7 @@ en:
|
||||
errors:
|
||||
client_id_invalid: The configured OAuth 2 client id is invalid. Please check the configuration.
|
||||
client_secret_invalid: The configured OAuth 2 client secret is invalid. Please check the configuration.
|
||||
nc_dependency_missing: 'A required dependency is missing on the file storage. Please add the following dependency: %{dependency}.'
|
||||
nc_dependency_missing: "A required dependency is missing on the file storage. Please add the following dependency: %{dependency}."
|
||||
nc_dependency_version_mismatch: The %{dependency} app version is not supported. Please update your Nextcloud server.
|
||||
nc_host_not_found: No Nextcloud server found at the configured host url. Please check the configuration.
|
||||
nc_oauth_request_not_found: The endpoint to fetch the currently connected user was not found. Please check the server logs for further information.
|
||||
@@ -325,7 +330,7 @@ en:
|
||||
nc_oauth_token_missing: OpenProject cannot test the user level communication with Nextcloud as the user did not yet link their Nextcloud account.
|
||||
nc_project_folder_missing: The previously created project folder for project "%{project}" could not be found.
|
||||
nc_team_folder_not_found: The team folder could not be found.
|
||||
nc_unexpected_files: 'Unexpected files found in the managed team folder. For example: %{sample}'
|
||||
nc_unexpected_files: "Unexpected files found in the managed team folder. For example: %{sample}"
|
||||
nc_unlinked_project_folders: Not all project folders have been created yet (%{actual} / %{expected}). This can indicate errors during the AMPF background synchronization.
|
||||
nc_userless_access_denied: The configured app password is invalid.
|
||||
not_configured: The connection could not be validated. Please finish configuration first.
|
||||
@@ -367,7 +372,7 @@ en:
|
||||
project_folders:
|
||||
subtitle: Automatically managed project folders
|
||||
since: since %{datetime}
|
||||
synced: 'Last sync: %{datetime}'
|
||||
synced: "Last sync: %{datetime}"
|
||||
title: Health status report
|
||||
health_email_notifications:
|
||||
description_disabled: Admins will not receive updates by email when there are important updates.
|
||||
@@ -384,7 +389,7 @@ en:
|
||||
empty_project_folder_validation: Selecting a folder is mandatory to proceed.
|
||||
existing_manual_folder: You can designate an existing folder as the root folder for this project. The permissions are however not automatically managed, the administrator needs to manually ensure relevant users have access. The selected folder can be used by multiple projects.
|
||||
host: Please add the host address of your storage including the https://. It should not be longer than 255 characters.
|
||||
managed_project_folders_application_password_caption: 'Enable automatic managed folders by copying this value from: %{provider_type_link}.'
|
||||
managed_project_folders_application_password_caption: "Enable automatic managed folders by copying this value from: %{provider_type_link}."
|
||||
name: Give your storage a name so that users can differentiate between multiple storages.
|
||||
new_storage_html: Read our documentation on [setting up a %{provider_name} file storage](docs_url) integration for more information.
|
||||
nextcloud:
|
||||
@@ -421,7 +426,7 @@ en:
|
||||
integration: SharePoint Administration / OpenProject
|
||||
oauth_configuration: Copy these values from the desired application in the %{application_link_text}.
|
||||
provider_configuration: Please make sure you have administration privileges in the %{application_link_text} or contact your Microsoft administrator before doing the setup. In the portal, you also need to register an Azure application or use an existing one for authentication.
|
||||
type: 'Please make sure you have administration privileges in your Nextcloud instance and have the following application installed before doing the setup:'
|
||||
type: "Please make sure you have administration privileges in your Nextcloud instance and have the following application installed before doing the setup:"
|
||||
type_link_text: "“Integration OpenProject”"
|
||||
label_active: Active
|
||||
label_add_new_storage: Add new storage
|
||||
|
||||
Reference in New Issue
Block a user