Merge branch 'dev' into feature/75749-rename-ckeditor-macro-to--insert

This commit is contained in:
Eric Schubert
2026-06-12 10:38:31 +02:00
140 changed files with 4868 additions and 273 deletions
@@ -59,15 +59,23 @@ module WorkPackages
def initialize(work_package,
focused_field: nil,
touched_field_map: {})
touched_field_map: {},
submit_path: nil)
super()
@work_package = work_package
@focused_field = map_field(focused_field)
@touched_field_map = touched_field_map
@submit_path = submit_path
end
# Defaults to the core progress controller, but callers reusing the modal
# from another context (e.g. the resource planner) can point the form at
# their own endpoint. The live preview derives its URL from the form
# action too (`<action>/preview`), so a single override covers both.
def submit_path
return @submit_path if @submit_path
if work_package.new_record?
url_for(controller: "work_packages/progress",
action: "create")
@@ -34,7 +34,8 @@ module WorkPackages
class ModalBodyComponent < BaseModalComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
def initialize(work_package,
focused_field: nil,
touched_field_map: {})
touched_field_map: {},
submit_path: nil)
super
@mode = :status_based
@@ -35,7 +35,8 @@ module WorkPackages
class ModalBodyComponent < BaseModalComponent
def initialize(work_package,
focused_field: nil,
touched_field_map: {})
touched_field_map: {},
submit_path: nil)
super
@mode = :work_based
@@ -158,6 +158,15 @@ module OpTurbo
turbo_streams << OpTurbo::StreamComponent.new(action: :reloadPage, target: nil).render_in(view_context)
end
# Dispatches a `CustomEvent` on `document` from a turbo stream, letting the
# server signal a client-side change without knowing which listeners (if
# any) react to it. `detail` is serialized and exposed as the event's detail.
def dispatch_event_via_turbo_stream(name, detail: {})
turbo_streams << OpTurbo::StreamComponent
.new(action: :dispatchEvent, target: nil, "event-name": name, detail: detail.to_json)
.render_in(view_context)
end
def turbo_streams
@turbo_streams ||= []
end
@@ -0,0 +1,125 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 WorkPackages
module Progress
# Form glue shared by every controller that renders the progress modal:
# parses the hidden `*_touched` inputs so only fields the user actually
# edited are written, picks the mode-appropriate attributes, and builds the
# modal body component.
module ModalParams
extend ActiveSupport::Concern
ERROR_PRONE_ATTRIBUTES = %i[status_id
estimated_hours
remaining_hours
done_ratio].freeze
private
def progress_modal_component(submit_path: nil)
modal_class.new(@work_package,
focused_field:,
touched_field_map:,
submit_path:)
end
def modal_class
if WorkPackage.status_based_mode?
WorkPackages::Progress::StatusBased::ModalBodyComponent
else
WorkPackages::Progress::WorkBased::ModalBodyComponent
end
end
def focused_field
params[:field]
end
def set_progress_attributes_to_work_package
WorkPackages::SetAttributesService
.new(user: current_user,
model: @work_package,
contract_class:)
.call(work_package_progress_params)
end
def contract_class
if @work_package.new_record?
WorkPackages::CreateContract
else
WorkPackages::UpdateContract
end
end
def work_package_progress_params
params.require(:work_package)
.slice(*allowed_touched_params)
.permit!
end
def allowed_touched_params
allowed_params.filter { touched?(it) }
end
def allowed_params
if WorkPackage.status_based_mode?
%i[estimated_hours status_id]
else
%i[estimated_hours remaining_hours done_ratio]
end
end
def touched?(field)
touched_field_map[:"#{field}_touched"]
end
# Tolerates a missing `work_package` param so the modal can be opened
# without carrying the form values along (e.g. from a context menu).
def touched_field_map
(params[:work_package] || ActionController::Parameters.new)
.slice("estimated_hours_touched",
"remaining_hours_touched",
"done_ratio_touched",
"status_id_touched")
.transform_values { it == "true" }
.permit!
end
def extra_error_messages(service_call)
errors_not_handled_by_progress_modal = service_call.errors.reject do |error|
ERROR_PRONE_ATTRIBUTES.include?(error.attribute)
end
join_flash_messages(errors_not_handled_by_progress_modal.map(&:full_message))
end
end
end
end
@@ -31,11 +31,7 @@
class WorkPackages::ProgressController < ApplicationController
include OpTurbo::ComponentStream
include FlashMessagesHelper
ERROR_PRONE_ATTRIBUTES = %i[status_id
estimated_hours
remaining_hours
done_ratio].freeze
include WorkPackages::Progress::ModalParams
layout false
authorization_checked! :new, :edit, :preview, :create, :update
@@ -132,22 +128,6 @@ class WorkPackages::ProgressController < ApplicationController
}
end
def progress_modal_component
modal_class.new(@work_package, focused_field:, touched_field_map:)
end
def modal_class
if WorkPackage.status_based_mode?
WorkPackages::Progress::StatusBased::ModalBodyComponent
else
WorkPackages::Progress::WorkBased::ModalBodyComponent
end
end
def focused_field
params[:field]
end
def find_work_package
@work_package = WorkPackage.visible.find(params[:work_package_id])
end
@@ -160,63 +140,7 @@ class WorkPackages::ProgressController < ApplicationController
@work_package.clear_changes_information
end
def touched_field_map
params.require(:work_package)
.slice("estimated_hours_touched",
"remaining_hours_touched",
"done_ratio_touched",
"status_id_touched")
.transform_values { it == "true" }
.permit!
end
def work_package_progress_params
params.require(:work_package)
.slice(*allowed_touched_params)
.permit!
end
def allowed_touched_params
allowed_params.filter { touched?(it) }
end
def allowed_params
if WorkPackage.status_based_mode?
%i[estimated_hours status_id]
else
%i[estimated_hours remaining_hours done_ratio]
end
end
def touched?(field)
touched_field_map[:"#{field}_touched"]
end
def set_progress_attributes_to_work_package
WorkPackages::SetAttributesService
.new(user: current_user,
model: @work_package,
contract_class:)
.call(work_package_progress_params)
end
def contract_class
if @work_package.new_record?
WorkPackages::CreateContract
else
WorkPackages::UpdateContract
end
end
def formatted_duration(hours)
API::V3::Utilities::DateTimeFormatter.format_duration_from_hours(hours, allow_nil: true)
end
def extra_error_messages(service_call)
errors_not_handled_by_progress_modal = service_call.errors.reject do |error|
ERROR_PRONE_ATTRIBUTES.include?(error.attribute)
end
join_flash_messages(errors_not_handled_by_progress_modal.map(&:full_message))
end
end
+2 -2
View File
@@ -51,8 +51,8 @@ module Import
def try_to_find_existing_op_users
op_attributes = to_op_attributes
User.where(login: op_attributes[:login]).or(
User.where(mail: op_attributes[:mail])
User.by_login(op_attributes[:login]).or(
User.where("LOWER(mail) = ?", op_attributes[:mail]&.downcase)
)
end
@@ -113,10 +113,10 @@ module ProjectIdentifiers
@historical_identifiers ||= Project.identifier_slugs.historically_reserved.upcased_values.to_set
end
def exceeds_max_length = Project.where("length(identifier) > ?", self.class.max_identifier_length)
def contains_non_alphanumeric = Project.where("identifier ~ ?", "[^a-zA-Z0-9_]")
def exceeds_max_length = Project.where("length(identifier) > ?", self.class.max_identifier_length)
def contains_non_alphanumeric = Project.where("identifier ~ ?", "[^a-zA-Z0-9_]")
def does_not_start_with_letter = Project.where("identifier ~ ?", "^[^A-Za-z]") # rubocop:disable Naming/PredicatePrefix
def not_fully_uppercased = Project.where("identifier != UPPER(identifier)")
def not_fully_uppercased = Project.where("identifier != UPPER(identifier)")
def collision_error_reason(identifier)
if self.class.model_reserved_identifiers.include?(identifier)
@@ -384,10 +384,16 @@ module Import
jira_user = Import::JiraUser.find_by(jira_user_key:, jira_import: @jira_import)
if jira_user
JiraOpenProjectReference.find_by!(
jira_entity_class: "Import::JiraUser",
reference = JiraOpenProjectReference.find_by(
jira_entity_class: jira_user.class.to_s,
jira_entity_id: jira_user.id
).op_leg
)
if reference
reference.op_leg
else
raise "Import::JiraOpenProjectReference with jira_entity_class #{jira_user.class} " \
"and jira_entity_id #{jira_user.id} not found!"
end
else
raise "Import::JiraUser with jira_user_key #{jira_user_key} not found!"
end
@@ -42,6 +42,7 @@ Rails.application.configure do
GoodJob::Job
.discarded
.where.not(error_event: GoodJob::ErrorEvents::RETRIED) # reject discarded jobs that were already retried
.where("error LIKE ?", "NameError: uninitialized constant %")
.filter do |job|
# Only retry jobs with NameError related to the job class name.
+3 -3
View File
@@ -3996,7 +3996,7 @@ fr:
label_enumerations: Énumérations
label_enterprise: Entreprise
label_enterprise_active_users: "%{current}/%{limit} utilisateurs actifs inscrits"
label_enterprise_edition: édition Enterprise
label_enterprise_edition: Édition Enterprise
label_enterprise_support: Support Enterprise
label_environment: Environnement
label_estimates_and_progress: Estimations et progression
@@ -4526,8 +4526,8 @@ fr:
one: 'Temps libre : 1 jour ouvrable'
other: 'Temps libre : %{count} jours ouvrables'
label_x_items_selected:
one: One item selected
other: "%{count} items selected"
one: Un élément sélectionné
other: "%{count} éléments sélectionnés"
label_yesterday: hier
label_zen_mode: Mode zen
label_role_type: Type
+5 -5
View File
@@ -3416,10 +3416,10 @@ zh-CN:
no_results_text: 无结果
header:
project_select_component:
all_projects: All projects
favorites: Favorites
leave_project: Leave project
title: Projects
all_projects: 所有项目
favorites: 收藏夹
leave_project: 退出项目
title: 项目
toggle_switch:
label_on: 开启
label_off: 关闭
@@ -4466,7 +4466,7 @@ zh-CN:
label_x_working_days_time_off:
other: 休息时间:%{count} 个工作日
label_x_items_selected:
other: "%{count} items selected"
other: 已选%{count}
label_yesterday: 昨天
label_zen_mode: 极简模式
label_role_type: 类型
+2 -2
View File
@@ -49,9 +49,9 @@ Existing integrations such as GitHub and GitLab already support the new identifi
[See our system admin guide for detailed information on how to manage work package identifiers](../../system-admin-guide/manage-work-packages/work-package-identifiers/).
#### Releasing unused numerical identifiers
#### Releasing unused project identifiers
When switching from the default numerical sequence to project-based work package identifiers, previously reserved numerical identifiers can be released again if they are no longer needed. This helps administrators avoid unnecessary gaps and keep numerical identifiers available if they later revert to the default sequence.
OpenProject allows administrators to release reserved project identifiers that are no longer needed. Please note that this option is currently only available when numerical work package identifiers are enabled.
> [!NOTE]
> Releasing an identifier cannot be undone. External links and integrations using it will stop resolving, and the name becomes available for any new project to claim.
@@ -27,6 +27,7 @@
@import "../../../modules/grids/app/components/_index.sass"
@import "../../../modules/meeting/app/components/_index.sass"
@import "../../../modules/overviews/app/components/_index.sass"
@import "../../../modules/resource_management/app/components/_index.sass"
@import "../../../modules/storages/app/components/_index.sass"
@import "../../../modules/wikis/app/components/_index.sass"
@@ -0,0 +1,97 @@
/*
* -- copyright
* OpenProject is an open source project management software.
* Copyright (C) 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.
* ++
*/
import ReloadFrameOnEventController from './reload-frame-on-event.controller';
import { setupStimulusTest, type StimulusTestContext } from 'core-stimulus/test-helpers';
interface ReloadableFrame extends HTMLElement {
src:string;
reload:() => void;
}
describe('ReloadFrameOnEventController', () => {
let ctx:StimulusTestContext;
// Each frame gets its own reload counter. The controller listens on
// `document`, so a counter per frame keeps a test from observing reloads
// triggered on another test's (or another frame's) controller.
const mountFrame = async ():Promise<{ frame:ReloadableFrame; calls:{ count:number } }> => {
await ctx.mount(`
<turbo-frame id="frame"
data-controller="reload-frame-on-event"
data-reload-frame-on-event-event-name-value="resource-allocations:changed"
data-reload-frame-on-event-url-value="/planner/view">
content
</turbo-frame>
`);
const frame = ctx.container.querySelector('#frame') as unknown as ReloadableFrame;
const calls = { count: 0 };
frame.reload = () => { calls.count += 1; };
return { frame, calls };
};
beforeEach(async () => {
ctx = await setupStimulusTest({
controllers: { 'reload-frame-on-event': ReloadFrameOnEventController },
});
});
afterEach(() => ctx.dispose());
it('points the frame at its url on the first event, then reloads on later events', async () => {
const { frame, calls } = await mountFrame();
document.dispatchEvent(new CustomEvent('resource-allocations:changed'));
expect(frame.src).toContain('/planner/view');
expect(calls.count).toBe(0);
document.dispatchEvent(new CustomEvent('resource-allocations:changed'));
expect(calls.count).toBe(1);
});
it('ignores unrelated events', async () => {
const { frame, calls } = await mountFrame();
document.dispatchEvent(new CustomEvent('some-other-event'));
expect(frame.src || '').not.toContain('/planner/view');
expect(calls.count).toBe(0);
});
it('stops reacting once disconnected', async () => {
const { frame, calls } = await mountFrame();
ctx.getController<ReloadFrameOnEventController>('reload-frame-on-event', frame).disconnect();
document.dispatchEvent(new CustomEvent('resource-allocations:changed'));
expect(calls.count).toBe(0);
});
});
@@ -0,0 +1,59 @@
/*
* -- copyright
* OpenProject is an open source project management software.
* Copyright (C) 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.
* ++
*/
import { Controller } from '@hotwired/stimulus';
import { FrameElement } from '@hotwired/turbo';
// Reloads the turbo-frame this controller is attached to whenever the
// configured document event fires. The frame starts without a `src` (so it is
// not fetched on load); the first event points it at `urlValue`, later events
// reload it. Pair the frame with `refresh="morph"` to reload without flicker.
export default class ReloadFrameOnEventController extends Controller<FrameElement> {
static values = { eventName: String, url: String };
declare eventNameValue:string;
declare urlValue:string;
private readonly listener = ():void => {
if (this.element.src) {
void this.element.reload();
} else {
this.element.src = this.urlValue;
}
};
connect():void {
document.addEventListener(this.eventNameValue, this.listener);
}
disconnect():void {
document.removeEventListener(this.eventNameValue, this.listener);
}
}
+2
View File
@@ -19,6 +19,7 @@ import KeepScrollPositionController from './controllers/keep-scroll-position.con
import PatternInputController from './controllers/pattern-input.controller';
import HoverCardTriggerController from './controllers/hover-card-trigger.controller';
import ScrollIntoViewController from './controllers/scroll-into-view.controller';
import ReloadFrameOnEventController from './controllers/reload-frame-on-event.controller';
import CkeditorFocusController from './controllers/ckeditor-focus.controller';
import IndexController from './controllers/dynamic/work-packages/activities-tab/index.controller';
import AutoScrollingController from './controllers/dynamic/work-packages/activities-tab/auto-scrolling.controller';
@@ -77,6 +78,7 @@ OpenProjectStimulusApplication.preregister('work-packages--date-picker--preview'
OpenProjectStimulusApplication.preregister('keep-scroll-position', KeepScrollPositionController);
OpenProjectStimulusApplication.preregister('pattern-input', PatternInputController);
OpenProjectStimulusApplication.preregister('scroll-into-view', ScrollIntoViewController);
OpenProjectStimulusApplication.preregister('reload-frame-on-event', ReloadFrameOnEventController);
OpenProjectStimulusApplication.preregister('ckeditor-focus', CkeditorFocusController);
OpenProjectStimulusApplication.preregister('auto-submit', AutoSubmit);
OpenProjectStimulusApplication.preregister('reveal', RevealController);
@@ -0,0 +1,11 @@
import { StreamActions, StreamElement } from '@hotwired/turbo';
export function registerDispatchEventStreamAction() {
StreamActions.dispatchEvent = function dispatchEventStreamAction(this:StreamElement) {
const name = this.getAttribute('event-name');
if (!name) { return; }
const detail = JSON.parse(this.getAttribute('detail') ?? '{}') as unknown;
document.dispatchEvent(new CustomEvent(name, { detail }));
};
}
+2
View File
@@ -5,6 +5,7 @@ import { addTurboEventListeners } from './turbo-event-listeners';
import { registerFlashStreamAction } from './flash-stream-action';
import { registerLiveRegionStreamAction } from './live-region-stream-action';
import { registerInputCaptionStreamAction } from './input-caption-stream-action';
import { registerDispatchEventStreamAction } from './dispatch-event-stream-action';
import { addTurboGlobalListeners } from './turbo-global-listeners';
import { applyTurboNavigationPatch } from './turbo-navigation-patch';
import { debugLog, whenDebugging } from 'core-app/shared/helpers/debug_output';
@@ -43,6 +44,7 @@ registerDialogStreamAction();
registerFlashStreamAction();
registerLiveRegionStreamAction();
registerInputCaptionStreamAction();
registerDispatchEventStreamAction();
addTurboAngularWrapper();
StreamActions.reloadPage = function reloadPage() {
@@ -1 +1,2 @@
@import "advanced_form_group"
@import "filterable_tree_view"
@@ -0,0 +1,70 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 Primer
module OpenProject
module Forms
module Dsl
# :nodoc:
class FilterableTreeViewInput < Primer::Forms::Dsl::Input
attr_reader :name, :label, :block
def initialize(name:, label: nil, **system_arguments, &block)
@name = name
@label = label
@block = block
super(**system_arguments)
end
def to_component
FilterableTreeView.new(input: self)
end
# :nocov:
def type
:filterable_tree_view
end
# :nocov:
# :nocov:
def focusable?
true
end
# :nocov:
def supports_validation?
false
end
end
end
end
end
end
@@ -84,6 +84,10 @@ module Primer
add_input SelectPanelInput.new(builder:, form:, **decorate_options(**), &)
end
def filterable_tree_view(**, &)
add_input FilterableTreeViewInput.new(builder:, form:, **decorate_options(**), &)
end
def decorate_options(include_help_text: true, help_text_options: {}, **options)
if include_help_text && supports_help_texts?(form.model)
attribute_name = help_text_options[:attribute_name] || options[:name]
@@ -0,0 +1,19 @@
<div class="FormControl-filterable-tree-view-wrap">
<%= content_tag(:fieldset, **@fieldset_arguments) do %>
<% if @input.label %>
<%= content_tag(:legend, **@input.label_arguments) do %>
<%= @input.label %>
<% end %>
<% end %>
<div class="mb-2">
<%= render(Caption.new(input: @input)) %>
</div>
<%= render(SpacingWrapper.new) do %>
<%=
render(Primer::OpenProject::FilterableTreeView.new(**@tree_view_arguments)) do |tree_view|
@input.block&.call(tree_view)
end
%>
<% end %>
<% end %>
</div>
@@ -0,0 +1,59 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 Primer
module OpenProject
module Forms
# :nodoc:
class FilterableTreeView < Primer::Forms::BaseComponent
delegate :builder, :form, to: :@input
def initialize(input:)
super()
@input = input
@input.add_label_classes("FormControl-label")
@fieldset_arguments = @input.input_arguments.extract!(:hidden, :class, :classes)
Primer::Forms::Utils.classify(@fieldset_arguments)
@fieldset_arguments.delete(:class) if @fieldset_arguments[:class].blank?
@tree_view_arguments = {
**@input.input_arguments,
form_arguments: {
name: @input.name,
builder: builder
}
}
end
end
end
end
end
@@ -0,0 +1,5 @@
.FormControl-filterable-tree-view-wrap
& fieldset
padding: 0
margin: 0
border: 0
@@ -31,8 +31,8 @@ cs:
label: Git snippets
description: Kopírovat úryvky gitu do schránky
git_actions:
branch_name: Název pobočky
commit_message: odevzdat zprávu
branch_name: Název větve
commit_message: Zpráva commitu
cmd: Vytvořit větev s prázdným commitem
title: Rychlé snippety pro Git
copy_success: "✅ zkopírováno!"
+1 -1
View File
@@ -78,7 +78,7 @@ module Meetings
end
def all_meetings_item
all_filter = [{ invited_user_id: { operator: "*", values: [] } }].to_json
all_filter = [].to_json
my_meetings_href = polymorphic_path([project, :meetings])
query_params = { filters: all_filter }
@@ -402,38 +402,6 @@ cs:
confirm_label:
enable: Povolit aktualizací e-mailem
disable: Zakázat aktualizací e-mailem
banner:
participants:
enabled: 'Všichni účastníci obdrží e-mailem aktualizované pozvánky do kalendáře, když přidáte nebo odeberete účastníky.
'
disabled: 'E-mailové aktualizace kalendářů jsou zakázány. Účastníci nedostanou e-mail s informacemi o přidání nebo odebrání účastníků.
'
draft_disabled: 'Participants will not receive an email informing them when you add or remove participants.
'
onetime:
enabled: 'Všichni účastníci obdrží e-mailem aktualizované pozvánky do kalendáře, když přidáte nebo odeberete účastníky.
'
disabled: 'Participants will not receive an email informing them of changes to meeting date, time or participants.
'
occurrence:
enabled: 'E-mailové aktualizace kalendářů jsou povoleny pro tuto sérii schůzek. Účastníci dostanou e-mail s informacemi, když provedete změny.
'
disabled: 'E-mailové aktualizace kalendářů jsou zakázány pro tuto sérii schůzek. Účastníci nedostanou e-mail s informacemi, když provedete změny.
'
template:
enabled: 'E-mailové aktualizace kalendářů jsou povoleny. Účastníci budou dostávat aktualizované pozvánky e-mailem, když upravíte tuto šablonu či individuální výskyty.
'
disabled: 'E-mailové aktualizace kalendářů jsou zakázány pro tuto sérii schůzek. Účastníci nebudou dostávat aktualizované pozvánky e-mailem, když upravíte tuto šablonu či individuální výskyty.
'
presentation_mode:
title: Prezentační mód
button_present: Prezentovat
@@ -130,6 +130,13 @@ RSpec.describe "Meetings", "Index", :js do
shared_examples "sidebar filtering" do |context:|
context "when showing all meetings without invitations" do
let!(:meeting_without_participants) do
create(:meeting,
project:,
title: "Meeting without any participants!",
start_time: business_day_at_noon + 3.hours).tap { |m| m.participants.delete_all }
end
it "does not show under My meetings, but in All meetings" do
meetings_page.visit!
meetings_page.expect_no_meetings_listed
@@ -137,12 +144,15 @@ RSpec.describe "Meetings", "Index", :js do
meetings_page.set_sidebar_filter "All meetings"
# It now includes the ongoing meeting I'm not invited to
if context == :global
[ongoing_meeting, meeting, tomorrows_meeting, other_project_meeting]
else
[ongoing_meeting, meeting, tomorrows_meeting]
end
# It now includes the ongoing meeting I'm not invited to,
# as well as meetings without any invited participants
expected_meetings =
if context == :global
[ongoing_meeting, meeting, meeting_without_participants, tomorrows_meeting, other_project_meeting]
else
[ongoing_meeting, meeting, meeting_without_participants, tomorrows_meeting]
end
meetings_page.expect_meetings_listed(*expected_meetings)
end
end
@@ -182,8 +192,12 @@ RSpec.describe "Meetings", "Index", :js do
it "show all past meetings" do
meetings_page.expect_meetings_listed_in_table(yesterdays_meeting, meeting, ongoing_meeting)
meetings_page.expect_meetings_not_listed(tomorrows_meeting)
end
it "keeps the past filter selected when changing advanced filters (Regression #61875)" do
meetings_page.set_sidebar_filter "My meetings"
meetings_page.set_quick_filter upcoming: false
# keeps the past filter selected when changing advanced filters (Regression #61875)" do
meetings_page.open_filters
meetings_page.remove_filter "invited_user_id"
@@ -0,0 +1 @@
@import "resource_planner_views/work_package_list/table_component"
@@ -34,6 +34,16 @@ module ResourceAllocations
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
# `dialog_id` and `submit_label` default to the create wizard's; the edit
# dialog passes its own.
def initialize(dialog_id: ResourceAllocations::NewDialogComponent::DIALOG_ID,
submit_label: I18n.t("resource_management.allocate_resource_dialog.submit"))
super
@dialog_id = dialog_id
@submit_label = submit_label
end
def wrapper_key
ResourceAllocations::NewDialogComponent::FOOTER_ID
end
@@ -43,7 +53,7 @@ module ResourceAllocations
component_collection do |buttons|
buttons.with_component(
Primer::Beta::Button.new(
data: { "close-dialog-id": ResourceAllocations::NewDialogComponent::DIALOG_ID },
data: { "close-dialog-id": @dialog_id },
mr: 1
)
) { I18n.t(:button_cancel) }
@@ -54,7 +64,7 @@ module ResourceAllocations
form: ResourceAllocations::NewDialogComponent::FORM_ID,
type: :submit
)
) { I18n.t("resource_management.allocate_resource_dialog.submit") }
) { @submit_label }
end
end
end
@@ -3,8 +3,8 @@
primer_form_with(
model: @allocation,
scope: :resource_allocation,
url: project_resource_allocations_path(@project),
method: :post,
url: form_url,
method: form_method,
html: {
data: {
turbo_stream: true,
@@ -35,11 +35,16 @@ module ResourceAllocations
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(allocation:, project:, allocation_kind:)
# `dialog_id` names the dialog hosting the form (autocompleter dropdowns
# attach to it): the create wizard's by default, the edit dialog's when
# editing a persisted allocation.
def initialize(allocation:, project:, allocation_kind:,
dialog_id: ResourceAllocations::NewDialogComponent::DIALOG_ID)
super
@allocation = allocation
@project = project
@allocation_kind = allocation_kind
@dialog_id = dialog_id
end
def wrapper_key
@@ -48,12 +53,24 @@ module ResourceAllocations
private
attr_reader :dialog_id
def filter_based?
@allocation_kind.to_s == "filter"
end
def dialog_id
ResourceAllocations::NewDialogComponent::DIALOG_ID
# A persisted allocation submits an update to itself; a new one goes
# through the create flow (with its confirmation step).
def form_url
if @allocation.persisted?
project_resource_allocation_path(@project, @allocation)
else
project_resource_allocations_path(@project)
end
end
def form_method
@allocation.persisted? ? :patch : :post
end
def form_list_component(form)
@@ -0,0 +1,62 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 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.
++#%>
<%=
render(
Primer::Alpha::Dialog.new(
id: DIALOG_ID,
title:,
size: :large,
classes: "Overlay--size-large-portrait",
data: { "keep-open-on-submit": true }
)
) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body(classes: "Overlay-body_autocomplete_height") do
render(
ResourceAllocations::AllocationStep::FormComponent.new(
allocation:,
project:,
allocation_kind:,
dialog_id: DIALOG_ID
)
)
end
dialog.with_footer do
render(
ResourceAllocations::AllocationStep::FooterComponent.new(
dialog_id: DIALOG_ID,
submit_label: I18n.t("resource_management.edit_allocation_dialog.submit")
)
)
end
end
%>
@@ -0,0 +1,59 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourceAllocations
# A single-step dialog to edit a persisted allocation, reusing the create
# wizard's allocation form (which submits an update for persisted records).
class EditDialogComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
DIALOG_ID = "edit-resource-allocation-dialog"
def initialize(project:, allocation:)
super
@project = project
@allocation = allocation
end
private
attr_reader :project, :allocation
def allocation_kind
allocation.principal_explicit? ? "principal" : "filter"
end
def title
I18n.t("resource_management.edit_allocation_dialog.title")
end
end
end
@@ -0,0 +1,53 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 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.
++#%>
<%= component_wrapper do %>
<%= flex_layout do |body| %>
<%= body.with_row(mb: 3) do %>
<%= render(ResourceAllocations::ProgressComponent.new(work_package:, allocations:)) %>
<% end %>
<% if allocations.any? %>
<%= body.with_row do %>
<%= render(Primer::Beta::BorderBox.new) do |box| %>
<% allocations.each do |allocation| %>
<% box.with_row do %>
<%= render ResourceAllocations::ListItemComponent.new(
allocation:,
project:,
visible: visible_principal?(allocation),
overbooked: overbooked?(allocation),
editable: editable?
) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,68 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourceAllocations
# The body of the work package allocations dialog: the allocation progress
# summary and one row per allocation. Streamable so the dialog content can be
# refreshed after an allocation changes. Allocations whose principal is not
# visible to the current user are still listed, but anonymised.
class ListComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(project:, work_package:, allocations:, visible_principal_ids:, overbooked_ids: Set.new)
super
@project = project
@work_package = work_package
@allocations = allocations
@visible_principal_ids = visible_principal_ids
@overbooked_ids = overbooked_ids
end
private
attr_reader :project, :work_package, :allocations, :visible_principal_ids, :overbooked_ids
def visible_principal?(allocation)
allocation.principal_id.nil? || visible_principal_ids.include?(allocation.principal_id)
end
def overbooked?(allocation)
overbooked_ids.include?(allocation.id)
end
def editable?
return @editable if defined?(@editable)
@editable = User.current.allowed_in_project?(:allocate_user_resources, project)
end
end
end
@@ -0,0 +1,70 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 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.
++#%>
<%=
render(
Primer::Alpha::Dialog.new(
id: DIALOG_ID,
title:,
size: :large,
classes: "Overlay--size-large-portrait",
data: { "keep-open-on-submit": true }
)
) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body do
render(ResourceAllocations::ListComponent.new(project:, work_package:, allocations:, visible_principal_ids:, overbooked_ids:))
end
dialog.with_footer do
# `mr: :auto` pushes the allocate action to the left of the footer's
# flex row, leaving the close button right-aligned.
concat(
render(
Primer::Beta::Button.new(
scheme: :link,
tag: :a,
href: allocate_resource_path,
mr: :auto,
data: { controller: "async-dialog" }
)
) do |button|
button.with_leading_visual_icon(icon: :"person-add")
t("resource_management.work_package_allocations_dialog.allocate_resource")
end
)
concat(
render(Primer::Beta::Button.new(data: { "close-dialog-id": DIALOG_ID })) do
t(:button_close)
end
)
end
end
%>
@@ -0,0 +1,62 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourceAllocations
# The dialog shell around a work package's allocation list, with a footer to
# allocate another resource.
class ListDialogComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
DIALOG_ID = "work-package-allocations-dialog"
def initialize(project:, work_package:, allocations:, visible_principal_ids:, overbooked_ids: Set.new)
super
@project = project
@work_package = work_package
@allocations = allocations
@visible_principal_ids = visible_principal_ids
@overbooked_ids = overbooked_ids
end
private
attr_reader :project, :work_package, :allocations, :visible_principal_ids, :overbooked_ids
def title
I18n.t("resource_management.work_package_allocations_dialog.title")
end
def allocate_resource_path
new_project_resource_allocation_path(project, work_package_id: work_package.id)
end
end
end
@@ -0,0 +1,61 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 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.
++#%>
<%= flex_layout(align_items: :center) do |row| %>
<%= row.with_column(mr: 2) do %>
<%= render(leading_visual) %>
<% end %>
<%= row.with_column(flex: 1) do %>
<%= render(Primer::Beta::Text.new(font_size: :small)) { name } %>
<% end %>
<%= row.with_column do %>
<%= render(Primer::Beta::Label.new(size: :large)) { duration } %>
<% end %>
<% if overbooked? %>
<%= row.with_column(ml: 2, classes: "d-flex") do %>
<%= render Primer::Beta::Octicon.new(
icon: :"alert-fill",
color: :danger,
id: overbooked_icon_id,
"aria-label": overbooked_message
) %>
<%= render Primer::Alpha::Tooltip.new(
for_id: overbooked_icon_id,
type: :description,
text: overbooked_message,
direction: :s
) %>
<% end %>
<% end %>
<% if menu? %>
<%= row.with_column(ml: 2) do %>
<%= context_menu %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,167 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourceAllocations
# A single allocation row in the work package allocations dialog: the member's
# avatar and name (or an anonymous placeholder when the principal is not
# visible to the current user) and the allocated hours.
class ListItemComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include AvatarHelper
AVATAR_SIZE = 24
# `editable` enables the row's edit/delete menu (the caller checks the
# permission). It stays hidden for an anonymised row regardless, since the
# edit form would reveal the hidden user.
def initialize(allocation:, project:, visible:, overbooked: false, editable: false)
super
@allocation = allocation
@project = project
@visible = visible
@overbooked = overbooked
@editable = editable
end
private
attr_reader :allocation, :project
def visible?
@visible
end
def overbooked?
@overbooked
end
def menu?
@editable && visible?
end
def context_menu
render(Primer::Alpha::ActionMenu.new(size: :small, anchor_align: :end)) do |menu|
menu.with_show_button(icon: "kebab-horizontal",
"aria-label": t("resource_management.work_package_allocations_dialog.context_menu_label"),
scheme: :invisible)
edit_item(menu)
delete_item(menu)
end
end
def edit_item(menu)
menu.with_item(
label: I18n.t(:button_edit),
tag: :a,
href: helpers.edit_project_resource_allocation_path(project, allocation),
content_arguments: { data: { controller: "async-dialog" } }
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
def delete_item(menu)
menu.with_item(
label: I18n.t(:button_delete),
scheme: :danger,
href: helpers.project_resource_allocation_path(project, allocation),
form_arguments: {
method: :delete,
data: {
turbo_confirm: t("resource_management.work_package_allocations_dialog.delete_confirmation"),
turbo_stream: true
}
}
) do |item|
item.with_leading_visual_icon(icon: :trash)
end
end
def name
if allocation.principal
visible? ? allocation.principal.name : hidden_label
else
allocation.filter_name.presence || unassigned_label
end
end
# An allocation without a user yet (filter placeholder or lost principal)
# shows a person-add icon instead of an avatar. Sized to match the avatar so
# the leading column keeps the same width and the row stays aligned.
def leading_visual
if allocation.principal
Primer::OpenProject::AvatarWithFallback.new(size: AVATAR_SIZE, **avatar_options)
else
Primer::Beta::Octicon.new(icon: :"person-add", size: :medium, color: :muted, "aria-hidden": true)
end
end
# Only a visible principal exposes a real avatar (and an identity-revealing
# initials/colour seed). A hidden one falls back to a generated avatar keyed
# to the allocation, so the user cannot be correlated.
def avatar_options
if visible?
{
src: avatar_url(allocation.principal),
alt: allocation.principal.name,
unique_id: allocation.principal.id
}
else
{
alt: name,
unique_id: "resource-allocation-#{allocation.id}"
}
end
end
# Formatted per the instance's duration format setting, e.g. "2d 4h".
def duration
DurationConverter.output(allocation.allocated_hours)
end
def overbooked_icon_id
"resource-allocation-overbooked-#{allocation.id}"
end
def overbooked_message
t("resource_management.work_package_allocations_dialog.overbooked")
end
def hidden_label
t("resource_management.work_package_allocations_dialog.hidden_user")
end
def unassigned_label
t("resource_management.work_package_list.allocated_members.unassigned")
end
end
end
@@ -0,0 +1,61 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 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.
++#%>
<% if work_scheduled? %>
<%= flex_layout do |column| %>
<%= column.with_row(mb: 1) do %>
<%= flex_layout(justify_content: :space_between, align_items: :center) do |line| %>
<%= line.with_column do %>
<%= render(Primer::Beta::Text.new(font_size: :small)) { summary } %>
<% end %>
<%= line.with_column do %>
<%= render(Primer::Beta::Text.new(font_size: :small, color: :muted)) { percentage_label } %>
<% end %>
<% end %>
<% end %>
<%= column.with_row do %>
<%= render(Primer::Beta::ProgressBar.new) do |bar| %>
<% bar.with_item(percentage: bar_percentage, bg: bar_color, aria: { label: percentage_label }) %>
<% end %>
<% end %>
<% end %>
<% else %>
<%= render Primer::Beta::Octicon.new(
icon: :"alert-fill",
color: :danger,
id: no_work_tooltip_id,
"aria-label": no_work_message
) %>
<%= render Primer::Alpha::Tooltip.new(
for_id: no_work_tooltip_id,
type: :description,
text: no_work_message,
direction: :e
) %>
<% end %>
@@ -0,0 +1,111 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourceAllocations
# Renders how much of a work package's scheduled work is covered by resource
# allocations: an "12h / 35h" label (allocated / scheduled), a percentage,
# and a colored bar. Shared by the work package list column and the
# allocations dialog.
class ProgressComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def initialize(work_package:, allocations:)
super
@work_package = work_package
@allocations = allocations
end
# Without scheduled work there is nothing to allocate against (and no
# sensible denominator), so fall back to the muted placeholder.
def work_scheduled?
scheduled_hours.positive?
end
private
attr_reader :work_package, :allocations
def allocated_hours
allocations.sum { |allocation| allocation.allocated_time.to_i } / 60.0
end
# Total work: the rolled-up value for a parent, falling back to the work
# package's own estimate for a leaf (where `derived_estimated_hours` is nil).
def scheduled_hours
(work_package.derived_estimated_hours || work_package.estimated_hours).to_f
end
# Share of the scheduled work covered by allocations. Capped at 100 for the
# bar width; the raw value still drives the label and over-allocation color.
def ratio
return 0 unless work_scheduled?
((allocated_hours / scheduled_hours) * 100).round
end
def bar_percentage
ratio.clamp(0, 100)
end
def bar_color
if ratio > 100
:danger_emphasis
elsif ratio == 100
:success_emphasis
else
:accent_emphasis
end
end
def summary
t("resource_management.allocation.summary",
allocated: hours_label(allocated_hours),
scheduled: hours_label(scheduled_hours))
end
def percentage_label
helpers.number_to_percentage(ratio, precision: 0)
end
def hours_label(hours)
t("resource_management.allocation.hours",
value: helpers.number_with_precision(hours, precision: 1, strip_insignificant_zeros: true))
end
def no_work_message
t("resource_management.allocation.no_work")
end
def no_work_tooltip_id
"allocation-no-work-#{work_package.id}"
end
end
end
@@ -34,6 +34,14 @@ module ResourceAllocations
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
# `dialog_id` names the dialog the cancel button closes: the create
# wizard's by default, the edit dialog's when confirming an update.
def initialize(dialog_id: ResourceAllocations::NewDialogComponent::DIALOG_ID)
super
@dialog_id = dialog_id
end
def wrapper_key
ResourceAllocations::NewDialogComponent::FOOTER_ID
end
@@ -58,7 +66,7 @@ module ResourceAllocations
footer.with_column(mr: 1) do
render(
Primer::Beta::Button.new(
data: { "close-dialog-id": ResourceAllocations::NewDialogComponent::DIALOG_ID }
data: { "close-dialog-id": @dialog_id }
)
) { I18n.t(:button_cancel) }
end
@@ -84,8 +84,8 @@
body.with_row(w: :full) do
form_with(
url: project_resource_allocations_path(@project),
method: :post,
url: form_url,
method: form_method,
id: ResourceAllocations::NewDialogComponent::FORM_ID,
data: { turbo_stream: true }
) do
@@ -59,6 +59,20 @@ module ResourceAllocations
private
# A confirmed resubmit goes back to where the values came from: the
# update of a persisted allocation or the create flow for a new one.
def form_url
if @allocation.persisted?
project_resource_allocation_path(@project, @allocation)
else
project_resource_allocations_path(@project)
end
end
def form_method
@allocation.persisted? ? :patch : :post
end
def overbooking_heading
t("resource_management.allocate_resource_dialog.overbooking.title")
end
@@ -32,12 +32,15 @@ module ResourcePlannerViews
class ContentComponent < ApplicationComponent
include OpTurbo::Streamable
def initialize(view:, project:, resource_planner:)
def initialize(view:, project:, resource_planner:, work_packages: [], allocations: {}, visible_principal_ids: nil)
super
@view = view
@project = project
@resource_planner = resource_planner
@work_packages = work_packages
@allocations = allocations
@visible_principal_ids = visible_principal_ids
end
private
@@ -48,7 +51,10 @@ module ResourcePlannerViews
ResourcePlannerViews::WorkPackageList::ContentComponent.new(
view: @view,
project: @project,
resource_planner: @resource_planner
resource_planner: @resource_planner,
work_packages: @work_packages,
allocations: @allocations,
visible_principal_ids: @visible_principal_ids
)
end
end
@@ -0,0 +1,50 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 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.
++#%>
<% if named? %>
<%= flex_layout(align_items: :center) do |container| %>
<%= container.with_column(flex_shrink: 0) do %>
<%= render(Primer::OpenProject::AvatarStack.new(tooltipped: true, body_arguments: { label: tooltip_label })) do |stack| %>
<% avatar_options.each do |options| %>
<% stack.with_avatar_with_fallback(**options) %>
<% end %>
<% end %>
<% end %>
<%= container.with_column(ml: 2, classes: "min-width-0") do %>
<%= render(Primer::Beta::Truncate.new) { lead_name } %>
<% end %>
<% if additional? %>
<%= container.with_column(ml: 1, flex_shrink: 0) do %>
<%= render(Primer::Beta::Label.new(scheme: :secondary, inline: true)) { additional_label } %>
<% end %>
<% end %>
<% end %>
<% else %>
<%= render(Primer::Beta::Text.new(color: :muted)) { anonymous_label } %>
<% end %>
@@ -0,0 +1,137 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourcePlannerViews::WorkPackageList
# Renders the members allocated to a work package as an avatar stack (with the
# stack's built-in "+N" overflow). An allocation with an assigned principal
# shows that user's avatar; a filter-based allocation with no principal yet
# shows a generated avatar derived from its filter name.
class AllocatedMembersComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include AvatarHelper
AVATAR_SIZE = 20
# `visible_principal_ids` are the principals the current user may see. Members
# whose principal is not in that set are still counted, but never named or
# given a (potentially identity-revealing) avatar. A nil set means no
# restriction.
def initialize(allocations:, visible_principal_ids: nil)
super
@allocations = allocations
@visible_principal_ids = visible_principal_ids
end
def render?
allocations.any?
end
# Whether at least one member can be named — a visible user or a filter
# placeholder. When false, every member is hidden and only a count is shown.
def named?
identifiable.any?
end
private
attr_reader :allocations, :visible_principal_ids
# Members we may reveal: filter placeholders (no user) and allocations whose
# principal is visible to the current user.
def identifiable
@identifiable ||= allocations.select do |allocation|
allocation.principal_id.nil? ||
visible_principal_ids.nil? ||
visible_principal_ids.include?(allocation.principal_id)
end
end
def avatar_options
identifiable.map { |allocation| avatar_options_for(allocation) }
end
# The name shown beside the stack. The stack's own overflow indicator is not
# numeric, so the count of the remaining members is spelled out separately.
def lead_name
member_name(identifiable.first)
end
# Members beyond the named lead, including those hidden from this user, so
# hidden allocations are surfaced as a count rather than silently dropped.
def additional_count
allocations.size - 1
end
def additional?
additional_count.positive?
end
def additional_label
t("resource_management.work_package_list.allocated_members.additional", count: additional_count)
end
# Shown when none of the members may be named: a bare count of the members.
def anonymous_label
t("resource_management.work_package_list.allocated_members.other_users", count: allocations.size)
end
# A real principal resolves to their avatar (falling back to generated
# initials when they have no image); otherwise the fallback generates a
# deterministic avatar from the member name.
def avatar_options_for(allocation)
user = allocation.principal
{
src: (avatar_url(user) if user),
alt: member_name(allocation),
unique_id: user&.id || "resource-allocation-#{allocation.id}",
size: AVATAR_SIZE
}
end
# Shown in the stack's hover tooltip, since the names are not rendered inline.
# Only names members we may reveal.
def tooltip_label
identifiable.map { |allocation| member_name(allocation) }.join(", ")
end
# The assigned user's name, the filter name for an unassigned filter
# allocation, or a generic label for an allocation that lost its principal
# (e.g. the assigned user was deleted) — so the avatar always has a label.
def member_name(allocation)
allocation.principal&.name.presence || allocation.filter_name.presence || unassigned_label
end
def unassigned_label
t("resource_management.work_package_list.allocated_members.unassigned")
end
end
end
@@ -39,7 +39,9 @@ See COPYRIGHT and LICENSE files for more details.
rows: work_packages,
view: @view,
project: @project,
resource_planner: @resource_planner
resource_planner: @resource_planner,
allocations: @allocations,
visible_principal_ids: @visible_principal_ids
)
%>
<% if @view.manually_picked? %>
@@ -30,18 +30,19 @@
module ResourcePlannerViews::WorkPackageList
class ContentComponent < ApplicationComponent
def initialize(view:, project:, resource_planner:)
def initialize(view:, project:, resource_planner:, work_packages: [], allocations: {}, visible_principal_ids: nil)
super
@view = view
@project = project
@resource_planner = resource_planner
@work_packages = work_packages
@allocations = allocations
@visible_principal_ids = visible_principal_ids
end
private
def work_packages
@view.effective_query&.results&.work_packages || WorkPackage.none
end
attr_reader :work_packages, :allocations, :visible_principal_ids
end
end
@@ -0,0 +1,40 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 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.
++#%>
<%=
render(Primer::Alpha::Dialog.new(id: DIALOG_ID, title:, size: :medium_portrait)) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body do
helpers.turbo_frame_tag("work_package_progress_modal") do
render(modal_component)
end
end
end
%>
@@ -0,0 +1,55 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourcePlannerViews::WorkPackageList
# Hosts the core progress modal inside a Primer dialog so a work package's
# work / remaining work / % complete can be edited from the list. The modal
# body keeps the `work_package_progress_modal` turbo frame the live preview
# navigates, and carries its own submit button.
class EditTotalWorkDialogComponent < ApplicationComponent
include OpTurbo::Streamable
DIALOG_ID = "edit-total-work-dialog"
def initialize(modal_component:)
super
@modal_component = modal_component
end
private
attr_reader :modal_component
def title
I18n.t("resource_management.work_package_list.context_menu.edit_total_work")
end
end
end
@@ -99,11 +99,13 @@ module ResourcePlannerViews::WorkPackageList
end
def allocation
render(Primer::Beta::Text.new(color: :muted)) { allocation_placeholder }
render(ResourceAllocations::ProgressComponent.new(work_package:, allocations:))
end
def allocated_members
render(Primer::Beta::Text.new(color: :muted)) { allocation_placeholder }
return render(Primer::Beta::Text.new(color: :muted)) { allocation_placeholder } if allocations.empty?
render(AllocatedMembersComponent.new(allocations:, visible_principal_ids: table.visible_principal_ids))
end
def button_links
@@ -112,6 +114,10 @@ module ResourcePlannerViews::WorkPackageList
private
def allocations
@allocations ||= table.allocations_for(work_package)
end
def allocation_placeholder
I18n.t("resource_management.work_package_list.allocation_placeholder")
end
@@ -123,7 +129,7 @@ module ResourcePlannerViews::WorkPackageList
scheme: :invisible)
see_allocation_item(menu)
edit_total_work_item(menu)
edit_total_work_item(menu) if allowed_to_edit_work?
add_user_group_item(menu)
if manual?
@@ -136,15 +142,25 @@ module ResourcePlannerViews::WorkPackageList
end
def see_allocation_item(menu)
menu.with_item(label: t("resource_management.work_package_list.context_menu.see_allocation"),
disabled: true) do |item|
menu.with_item(
label: t("resource_management.work_package_list.context_menu.see_allocation"),
tag: :a,
href: helpers.project_work_package_resource_allocations_path(table.project, work_package),
content_arguments: { data: { controller: "async-dialog" } }
) do |item|
item.with_leading_visual_icon(icon: :hourglass)
end
end
def edit_total_work_item(menu)
menu.with_item(label: t("resource_management.work_package_list.context_menu.edit_total_work"),
disabled: true) do |item|
menu.with_item(
label: t("resource_management.work_package_list.context_menu.edit_total_work"),
tag: :a,
href: helpers.edit_project_resource_planner_view_work_package_progress_path(
table.project, table.resource_planner, table.view, work_package
),
content_arguments: { data: { controller: "async-dialog" } }
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
@@ -211,6 +227,10 @@ module ResourcePlannerViews::WorkPackageList
table.manual?
end
def allowed_to_edit_work?
User.current.allowed_in_project?(:edit_work_packages, work_package.project)
end
def position_index
@position_index ||= table.rows.index { |wp| wp.id == work_package.id }
end
@@ -34,18 +34,26 @@ module ResourcePlannerViews::WorkPackageList
mobile_columns :subject, :priority
attr_reader :view, :project, :resource_planner
attr_reader :view, :project, :resource_planner, :visible_principal_ids
def initialize(view:, project:, resource_planner:, **)
def initialize(view:, project:, resource_planner:, allocations: {}, visible_principal_ids: nil, **)
super(**)
@view = view
@project = project
@resource_planner = resource_planner
@allocations = allocations
@visible_principal_ids = visible_principal_ids
end
def manual? = view.manually_picked?
# The allocations of a work package, taken from the page-wide map the
# controller loaded; shared by the allocation progress and members columns.
def allocations_for(work_package)
@allocations[work_package.id] || []
end
main_column :subject
def sortable? = false
@@ -54,6 +62,10 @@ module ResourcePlannerViews::WorkPackageList
def has_actions? = true
# Scopes this table's styling (see table_component.sass) without touching the
# shared border-box grid defaults used by every other table.
def container_class = "op-resource-work-package-list"
def mobile_title
I18n.t("resource_management.work_package_list.mobile_title")
end
@@ -0,0 +1,27 @@
@import "helpers"
// The shared border-box grid top-aligns row content and distributes columns
// evenly. The resource planner work package list has a fixed, known set of
// columns, so we center the content and hardcode the column widths from the
// design. Scoped via the table's container_class so no other table is affected.
@media screen and (min-width: $breakpoint-md)
.op-resource-work-package-list
.op-border-box-grid
align-items: center
// Column widths as fractions of the 1200px design (376/80/200/200/200),
// so they scale with the available width. Only the action button is fixed.
// The header and body are independent grids; sharing one template keeps
// their tracks aligned. minmax(0, ) lets a track shrink below its content.
grid-template-columns: minmax(0, 376fr) minmax(0, 80fr) minmax(0, 200fr) minmax(0, 200fr) minmax(0, 200fr) 40px
// Subject keeps its main-column semantics (row header role, no truncation)
// but must occupy a single track in the explicit template above rather than
// spanning two as the shared grid would have it.
.op-border-box-grid__header--main-column,
.op-border-box-grid__row-item--main-column
grid-column: auto
// Content is centered here, so the top-alignment nudge the shared component
// applies to the action button is not wanted.
.op-border-box-grid__row-action
margin-top: 0
@@ -0,0 +1,51 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ResourceManagement
# Builds the content component for a resource planner view. Loads the view's
# work packages and their allocations in one place so the allocation columns
# (progress bar and members) share a single query rather than each issuing
# their own. Requires @project and @resource_planner to be set.
module PlannerViewContent
def work_package_list_content(view)
work_packages = view.is_a?(ResourceWorkPackageList) ? view.work_packages.to_a : []
allocations = ResourceAllocation.allocated_for_work_packages(work_packages)
ResourcePlannerViews::ContentComponent.new(
view:,
project: @project,
resource_planner: @resource_planner,
work_packages:,
allocations:,
visible_principal_ids: ResourceAllocation.visible_principal_ids(allocations.values.flatten, current_user)
)
end
end
end
@@ -35,6 +35,7 @@ module ::ResourceManagement
before_action :find_project_by_project_id
before_action :authorize
before_action :find_resource_allocation, only: %i[edit update destroy]
def new
respond_with_dialog ResourceAllocations::NewDialogComponent.new(
@@ -54,30 +55,61 @@ module ::ResourceManagement
# date picker. Uses the EmptyContract so in-progress input never surfaces
# validation errors while the user types.
def refresh_form
allocation = set_attributes(create_params, contract_class: EmptyContract).result
allocation = set_attributes(allocation_params, contract_class: EmptyContract).result
replace_via_turbo_stream(
component: ResourceAllocations::AllocationStep::ScheduleViolationBannerComponent.new(allocation:)
)
respond_with_turbo_streams
end
def edit; end
def edit
respond_with_dialog ResourceAllocations::EditDialogComponent.new(
project: @project,
allocation: @resource_allocation
)
end
def create
# The confirmation step's "Back" button resubmits the carried form values
# so the editable step can be re-rendered pre-filled.
return render_back_step if params[:back].present?
validation = set_attributes(create_params)
validation = set_attributes(allocation_params)
return render_allocation_step(validation.result, status: :unprocessable_entity) if validation.failure?
return render_warning_step(validation.result) if needs_confirmation?(validation.result)
persist_allocation
end
def update; end
def update
# The confirmation step's "Back" button resubmits the carried form values
# so the editable step can be re-rendered pre-filled.
return render_edit_form(set_update_attributes.result) if params[:back].present?
def destroy; end
validation = set_update_attributes
return render_edit_form(validation.result, status: :unprocessable_entity) if validation.failure?
if needs_confirmation?(validation.result)
return render_warning_step(validation.result,
dialog_id: ResourceAllocations::EditDialogComponent::DIALOG_ID)
end
persist_update
end
def destroy
entity = @resource_allocation.entity
call = ResourceAllocations::DeleteService
.new(user: current_user, model: @resource_allocation)
.call
if call.success?
render_destroy_success(entity)
else
render_error_flash_message_via_turbo_stream(message: call.errors.full_messages.to_sentence)
respond_with_turbo_streams
end
end
private
@@ -94,7 +126,7 @@ module ::ResourceManagement
respond_with_turbo_streams(status:)
end
def render_warning_step(allocation)
def render_warning_step(allocation, dialog_id: ResourceAllocations::NewDialogComponent::DIALOG_ID)
ranges = overbooked_ranges(allocation)
replace_via_turbo_stream(
@@ -109,7 +141,7 @@ module ::ResourceManagement
)
)
replace_via_turbo_stream(
component: ResourceAllocations::WarningStep::FooterComponent.new
component: ResourceAllocations::WarningStep::FooterComponent.new(dialog_id:)
)
respond_with_turbo_streams
end
@@ -117,17 +149,17 @@ module ::ResourceManagement
def persist_allocation
call = ResourceAllocations::CreateService
.new(user: current_user, model: ResourceAllocation.new)
.call(create_params)
.call(allocation_params)
if call.success?
render_create_success
render_create_success(call.result)
else
render_allocation_step(call.result, status: :unprocessable_entity)
end
end
def render_back_step
render_allocation_step(set_attributes(create_params).result)
render_allocation_step(set_attributes(allocation_params).result)
end
# A final confirmation step is shown only when the allocation would overbook
@@ -149,11 +181,14 @@ module ::ResourceManagement
def compute_overbooked_ranges(allocation)
return [] unless overbooking_checkable?(allocation)
# `exclude_id` drops the persisted version of an allocation being edited,
# so its old booking does not count against the new one.
availability(allocation).overbooking_with(
start_date: allocation.start_date,
end_date: allocation.end_date,
minutes: allocation.allocated_time,
work_package_id: allocation.entity_id
work_package_id: allocation.entity_id,
exclude_id: allocation.id
)
end
@@ -183,14 +218,99 @@ module ::ResourceManagement
.call(attributes)
end
def render_create_success
def render_create_success(allocation)
render_success_flash_message_via_turbo_stream(
message: I18n.t("resource_management.allocate_resource_dialog.success_message")
)
close_dialog_via_turbo_stream("##{ResourceAllocations::NewDialogComponent::DIALOG_ID}")
refresh_allocations_list(allocation.entity)
notify_allocation_change(allocation.entity)
respond_with_turbo_streams
end
def set_update_attributes
ResourceAllocations::SetAttributesService
.new(user: current_user, model: @resource_allocation, contract_class: ResourceAllocations::UpdateContract)
.call(allocation_params)
end
def persist_update
call = ResourceAllocations::UpdateService
.new(user: current_user, model: @resource_allocation)
.call(allocation_params)
if call.success?
render_update_success(call.result)
else
render_edit_form(call.result, status: :unprocessable_entity)
end
end
def render_edit_form(allocation, status: :ok)
replace_via_turbo_stream(
component: ResourceAllocations::AllocationStep::FormComponent.new(
allocation:,
project: @project,
allocation_kind:,
dialog_id: ResourceAllocations::EditDialogComponent::DIALOG_ID
),
status:
)
replace_via_turbo_stream(
component: ResourceAllocations::AllocationStep::FooterComponent.new(
dialog_id: ResourceAllocations::EditDialogComponent::DIALOG_ID,
submit_label: I18n.t("resource_management.edit_allocation_dialog.submit")
)
)
respond_with_turbo_streams(status:)
end
def render_update_success(allocation)
render_success_flash_message_via_turbo_stream(
message: I18n.t("resource_management.edit_allocation_dialog.success_message")
)
close_dialog_via_turbo_stream("##{ResourceAllocations::EditDialogComponent::DIALOG_ID}")
refresh_allocations_list(allocation.entity)
notify_allocation_change(allocation.entity)
respond_with_turbo_streams
end
def render_destroy_success(entity)
render_success_flash_message_via_turbo_stream(
message: I18n.t("resource_management.work_package_allocations_dialog.delete_success")
)
refresh_allocations_list(entity)
notify_allocation_change(entity)
respond_with_turbo_streams
end
# Re-renders the allocation list of the work package's allocations dialog.
# The stream is a no-op on the client when that dialog is not open.
def refresh_allocations_list(work_package)
return unless work_package.is_a?(WorkPackage)
allocations = ResourceAllocation.allocated_for_work_packages([work_package])[work_package.id] || []
replace_via_turbo_stream(
component: ResourceAllocations::ListComponent.new(
project: @project,
work_package:,
allocations:,
visible_principal_ids: ResourceAllocation.visible_principal_ids(allocations, current_user),
overbooked_ids: ResourceAllocation.overbooked_ids(allocations)
)
)
end
# Announces that an allocation of the work package changed. A resource
# planner table open on the page reloads the affected work package in
# response; the controller stays unaware of which view (if any) is on
# screen. The stream is a harmless no-op when nothing listens.
def notify_allocation_change(entity)
return unless entity.is_a?(WorkPackage)
dispatch_event_via_turbo_stream("resource-allocations:changed", detail: { work_package_id: entity.id })
end
def allocation_kind
params[:allocation_kind].presence || "principal"
end
@@ -214,7 +334,16 @@ module ::ResourceManagement
.to_h
end
def create_params
# Only allocations of work packages reachable by the current user within
# the project may be touched; anything else 404s.
def find_resource_allocation
@resource_allocation = ResourceAllocation
.where(entity_type: "WorkPackage",
entity_id: WorkPackage.visible(current_user).where(project: @project))
.find(params.expect(:id))
end
def allocation_params
permitted = params
.expect(resource_allocation: %i[principal_id filter_name start_date end_date allocated_hours
entity_type entity_id])
@@ -30,6 +30,7 @@
module ::ResourceManagement
class ResourcePlannerViewsController < BaseController
include OpTurbo::ComponentStream
include PlannerViewContent
menu_item :resource_management
@@ -46,7 +47,9 @@ module ::ResourceManagement
only: %i[new_work_package add_work_package remove_work_package
move_work_package reorder_work_package]
def show; end
def show
@content_component = work_package_list_content(@view)
end
def new
if params[:view_class_name].present?
@@ -188,13 +191,7 @@ module ::ResourceManagement
end
def replace_work_package_list
replace_via_turbo_stream(
component: ResourcePlannerViews::ContentComponent.new(
view: @view,
project: @project,
resource_planner: @resource_planner
)
)
replace_via_turbo_stream(component: work_package_list_content(@view))
end
def render_configure_step(view, status: :ok)
@@ -242,13 +239,7 @@ module ::ResourceManagement
selected_view: view
)
)
replace_via_turbo_stream(
component: ResourcePlannerViews::ContentComponent.new(
view:,
project: @project,
resource_planner: @resource_planner
)
)
replace_via_turbo_stream(component: work_package_list_content(view))
close_dialog_via_turbo_stream("##{ResourcePlannerViews::EditDialogComponent::DIALOG_ID}")
respond_with_turbo_streams
end
@@ -30,6 +30,7 @@
module ::ResourceManagement
class ResourcePlannersController < BaseController
include OpTurbo::ComponentStream
include PlannerViewContent
menu_item :resource_management
@@ -47,6 +48,7 @@ module ::ResourceManagement
def show
@view = default_view
@content_component = work_package_list_content(@view)
render "resource_management/resource_planner_views/show"
end
@@ -0,0 +1,131 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ::ResourceManagement
# Edits a work package's progress (work / remaining work / % complete) from a
# resource planner work package list, reusing the core progress modal. The
# modal form posts back here so a successful save can close the dialog and
# refresh the list inline, instead of going through the Angular-driven core
# `WorkPackages::ProgressController` flow.
class WorkPackageProgressController < BaseController
include OpTurbo::ComponentStream
include FlashMessagesHelper
include PlannerViewContent
include WorkPackages::Progress::ModalParams
menu_item :resource_management
layout false
before_action :find_project_by_project_id
before_action :find_resource_planner
before_action :find_view
before_action :find_work_package
before_action :authorize_edit_work_package
# The `.visible` finders above enforce read access (view_resource_planners /
# view_work_packages); `authorize_edit_work_package` gates the edit itself.
authorization_checked! :edit, :update, :preview
def edit
respond_with_dialog ResourcePlannerViews::WorkPackageList::EditTotalWorkDialogComponent.new(
modal_component: progress_modal_component(submit_path: update_path)
)
end
def preview
set_progress_attributes_to_work_package
render template: "work_packages/progress/modal",
locals: { progress_modal_component: progress_modal_component(submit_path: update_path) }
end
def update
call = WorkPackages::UpdateService
.new(user: current_user, model: @work_package)
.call(work_package_progress_params)
call.success? ? render_update_success : render_update_failure(call)
end
private
def render_update_success
render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
close_dialog_via_turbo_stream(
"##{ResourcePlannerViews::WorkPackageList::EditTotalWorkDialogComponent::DIALOG_ID}"
)
replace_via_turbo_stream(component: work_package_list_content(@view))
respond_with_turbo_streams
end
def render_update_failure(call)
extra_errors = extra_error_messages(call)
render_error_flash_message_via_turbo_stream(message: extra_errors) if extra_errors.present?
# `@work_package` already carries the rejected attributes and errors from
# the failed service call, so the modal re-renders with them in place.
update_via_turbo_stream(
component: progress_modal_component(submit_path: update_path),
method: "morph"
)
respond_with_turbo_streams(status: :unprocessable_entity)
end
def update_path
project_resource_planner_view_work_package_progress_path(
@project, @resource_planner, @view, @work_package
)
end
def find_resource_planner
@resource_planner = ResourcePlanner
.visible(current_user)
.where(project: @project)
.with_children
.find(params.expect(:resource_planner_id))
end
def find_view
@view = @resource_planner.children.find(params.expect(:view_id))
end
def find_work_package
@work_package = WorkPackage
.visible(current_user)
.where(project: @project)
.find(params.expect(:work_package_id))
end
def authorize_edit_work_package
deny_access unless User.current.allowed_in_project?(:edit_work_packages, @project)
end
end
end
@@ -0,0 +1,71 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 ::ResourceManagement
# Lists the allocations of a single work package in a dialog. Visible to users
# who may view the work package and hold the `view_resource_planners`
# permission in its project.
class WorkPackageResourceAllocationsController < BaseController
include OpTurbo::ComponentStream
menu_item :resource_management
before_action :find_project_by_project_id
before_action :find_work_package
before_action :authorize
def index
respond_with_dialog ResourceAllocations::ListDialogComponent.new(
project: @project,
work_package: @work_package,
allocations:,
visible_principal_ids: ResourceAllocation.visible_principal_ids(allocations, current_user),
overbooked_ids: ResourceAllocation.overbooked_ids(allocations)
)
end
private
def allocations
@allocations ||=
ResourceAllocation.allocated_for_work_packages([@work_package])[@work_package.id] || []
end
# `WorkPackage.visible` enforces the view-work-package permission; a
# non-visible (or out-of-project) id therefore 404s. The
# `view_resource_planners` permission is enforced by `authorize`.
def find_work_package
@work_package = WorkPackage
.visible(current_user)
.where(project: @project)
.find(params.expect(:work_package_id))
end
end
end
@@ -31,6 +31,11 @@
class ResourceAllocation < ApplicationRecord
ALLOWED_ENTITY_TYPES = %w[WorkPackage].freeze
# `allocated_time` is stored in minutes in an integer column. Cap it at 5000
# hours so an absurdly large input is rejected with a validation error rather
# than overflowing the column and raising ActiveModel::RangeError on save.
MAX_ALLOCATED_TIME = (5000.hours / 1.minute).to_i
belongs_to :entity, polymorphic: true, optional: false
belongs_to :principal, class_name: "User", optional: true, inverse_of: :resource_allocations
belongs_to :requested_by, class_name: "User", optional: true
@@ -58,11 +63,69 @@ class ResourceAllocation < ApplicationRecord
scope :needs_principal_assignment, -> { where(principal_explicit: false, principal_id: nil) }
scope :for_principal, ->(principal) { where(principal:) }
# The `allocated` allocations for the given work packages, grouped by work
# package id and with principals eager-loaded. Loaded once per page so the
# allocation columns (progress bar and members) share a single query.
def self.allocated_for_work_packages(work_packages)
allocated
.where(entity_type: "WorkPackage", entity_id: work_packages.map(&:id))
.includes(:principal)
.order(:id)
.group_by(&:entity_id)
end
# The subset of the given allocations' principal ids that `user` may see.
# Used to anonymise members the current user is not allowed to know about.
def self.visible_principal_ids(allocations, user)
principal_ids = allocations.filter_map(&:principal_id).uniq
return Set.new if principal_ids.empty?
Principal.visible(user).where(id: principal_ids).pluck(:id).to_set
end
# The ids of the given allocations that fall into a range in which their
# assigned user is overbooked. Users without configured working hours are
# skipped — their capacity is unknown, not zero (mirroring the check made
# when an allocation is created). The users' working hours and booked
# allocations are each fetched in one query; only the per-user capacity
# calendar still queries per checked user.
def self.overbooked_ids(allocations)
checkable = overbooking_checkable_principals(allocations)
return Set.new if checkable.empty?
booked = allocated.for_principal(checkable).group_by(&:principal_id)
overbooked = checkable.flat_map { |principal| overbooked_ids_of(principal, booked.fetch(principal.id, [])) }
overbooked.to_set & allocations.map(&:id)
end
# The ids of all allocations falling into a range in which the user is
# overbooked, given the user's booked allocations.
def self.overbooked_ids_of(principal, booked)
ResourceAllocations::Availability
.new(user: principal, allocations: booked)
.overbooked_ranges
.flat_map { |range| range.items.map(&:id) }
end
private_class_method :overbooked_ids_of
# The given allocations' assigned users whose capacity is known, i.e. who
# have working hours configured, fetched in a single query.
def self.overbooking_checkable_principals(allocations)
principals = allocations.filter_map(&:principal).uniq
checkable_ids = UserWorkingHours.for_user(principals).distinct.pluck(:user_id).to_set
principals.select { |principal| checkable_ids.include?(principal.id) }
end
private_class_method :overbooking_checkable_principals
validates :state, :start_date, :end_date, presence: true
validates :allocated_time,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validate :allocated_time_within_limit
validates :entity_type,
inclusion: { in: ALLOWED_ENTITY_TYPES },
allow_blank: true
@@ -151,6 +214,17 @@ class ResourceAllocation < ApplicationRecord
private
# Capped above to keep `allocated_time` within the integer column. The field is
# entered in hours, so the message reports the limit in the same duration
# format rather than the raw minutes the column stores.
def allocated_time_within_limit
return if allocated_time.blank?
return if allocated_time <= MAX_ALLOCATED_TIME
errors.add(:allocated_time, :less_than_or_equal_to,
count: DurationConverter.output(MAX_ALLOCATED_TIME / 60.0))
end
def starts_before_entity?
entity_start_date.present? && start_date.present? && start_date < entity_start_date
end
@@ -63,6 +63,11 @@ class ResourceWorkPackageList < PersistedView
effective_query&.manually_sorted? || false
end
# The work packages selected by this view's query.
def work_packages
effective_query&.results&.work_packages || WorkPackage.none
end
private
def manual_mode?(filter_mode)
@@ -40,8 +40,12 @@ module ResourceAllocations
# checking a not-yet-persisted allocation.
CANDIDATE_ID = :candidate
def initialize(user:)
# `allocations` takes the user's `allocated` allocations when the caller
# already loaded them (e.g. in bulk for several users); they are queried
# lazily otherwise.
def initialize(user:, allocations: nil)
@user = user
@allocations = allocations
end
def overbooked?
@@ -41,4 +41,25 @@ See COPYRIGHT and LICENSE files for more details.
end %>
<%= render(ResourcePlanners::SubViewsComponent.new(resource_planner: @resource_planner, selected_view: @view)) %>
<%= render(ResourcePlannerViews::ContentComponent.new(view: @view, project: @project, resource_planner: @resource_planner)) %>
<%#
Reloadable frame so allocation changes made in the dialogs refresh the table
in place. The frame has no `src`, so it is not fetched on load — it just holds
the server-rendered content. The reload-frame-on-event controller points its
`src` at this same view and morphs it in when an allocation of any listed work
package changes.
%>
<%
# When the planner does not have a view yet, we cannot add frame data
frame_data = if @view
{
controller: "reload-frame-on-event",
"reload-frame-on-event-event-name-value": "resource-allocations:changed",
"reload-frame-on-event-url-value": project_resource_planner_view_path(@project, @resource_planner, @view)
}
else
{}
end
%>
<%= turbo_frame_tag("resource-planner-view-content", refresh: :morph, data: frame_data) do %>
<%= render(@content_component) %>
<% end %>
@@ -57,6 +57,10 @@ af:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ af:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ af:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ ar:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ ar:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,27 @@ ar:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
zero: "%{count} users"
one: "%{count} user"
two: "%{count} users"
few: "%{count} users"
many: "%{count} users"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ az:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ az:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ az:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ be:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ be:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,25 @@ be:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
few: "%{count} users"
many: "%{count} users"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ bg:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ bg:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ bg:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ ca:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ ca:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ ca:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ ckb-IR:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ ckb-IR:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ ckb-IR:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ cs:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ cs:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,25 @@ cs:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
few: "%{count} users"
many: "%{count} users"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ da:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ da:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ da:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ de:
make_private: Als Privat festlegen
make_public: Veröffentlichen
unfavorite: Aus Favoriten entfernen
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ de:
caption: Eine benutzerdefinierte Liste von Artikeln, die Sie manuell hinzufügen und entfernen. Eine Filterung ist nicht möglich.
label: Manuell ausgewählt
title: Ansicht konfigurieren
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Machen Sie diese Ansicht zu einem Favoriten, um sie im oberen Bereich des Seitenleistenmenüs hinzuzufügen.
label_new_resource_planner: Ressourcenplaner erstellen
label_resource_planner: Ressourcenplaner
@@ -114,9 +122,23 @@ de:
user_card:
caption: Erstellen Sie eine Ansicht auf der Basis von Benutzern und sehen Sie deren Details und Zuordnung in einer Kachelansicht von Benutzerkarten
label: Benutzer-Karten
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Arbeitspaket hinzufügen
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: Es gibt noch keine Arbeitspakete, die den Filtern dieser Ansicht entsprechen.
@@ -57,6 +57,10 @@ el:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ el:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ el:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ eo:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ eo:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ eo:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -3,11 +3,11 @@ es:
activerecord:
attributes:
resource_allocation:
allocated_hours: Hours
allocated_hours: Horas
allocated_time: Tiempo asignado
end_date: Fecha de finalización
entity: Entidad
filter_name: Resource filter name
filter_name: Nombre del filtro de recursos
principal: Asignado
start_date: Fecha de inicio
state: Estado
@@ -57,29 +57,33 @@ es:
make_private: Marcar como privado
make_public: Marcar como público
unfavorite: Eliminar de favoritos
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
caption: Set filter criteria based on user attributes to create a placeholder resource.
label: Filter criteria
label: Allocation type
caption: Establecer criterios de filtro basados en atributos de usuario para crear un recurso de marcador de posición (placeholder).
label: Criterios de filtrado
label: Tipo de asignación
principal:
caption: Allocate hours for a specific user.
label: User
caption: Asignar horas para un usuario específico.
label: Usuario
outside_dates:
description: The selected resource dates, %{resource_dates}, are outside of the work package's dates, %{work_package_dates}.
description: Las fechas del recurso seleccionado, %{resource_dates}, están fuera de las fechas del paquete de trabajo, %{work_package_dates}.
overbooking:
capacity_summary: "%{available} available · %{scheduled} scheduled"
description: The selected user, %{user}, would be allocated beyond their working hours during this period.
hidden_work: Other allocations
schedule_availability: "%{schedule} (%{factor}% available for project work)"
schedule_change: until %{date}, then %{schedule}
schedule_note: This user works %{schedule}.
submit: Allocate overtime
title: Do you want to allocate overtime to the user?
submit: Allocate
success_message: Resource allocated.
title: Allocate resource
capacity_summary: "%{available} disponible · %{scheduled} programado"
description: El usuario seleccionado, %{user}, sería asignado más allá de su horario laboral durante este periodo.
hidden_work: Otras asignaciones
schedule_availability: "%{schedule} (%{factor}% disponible para trabajo de proyecto)"
schedule_change: hasta %{date}, luego %{schedule}
schedule_note: Este usuario trabaja %{schedule}.
submit: Asignar horas extra
title: "¿Quieres asignar horas extra al usuario?"
submit: Asignar
success_message: Recurso asignado.
title: Asignar recurso
blankslate:
desc: Cree un planificador de recursos para empezar a planificar la capacidad de este proyecto.
title: Aún no hay planificadores de recursos
@@ -94,6 +98,10 @@ es:
caption: Una lista personalizada de elementos que puedes añadir y eliminar manualmente. No se puede filtrar los resultados.
label: Selección manual
title: Configurar vista
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Convierta esta vista en favorita para añadirla a la sección superior del menú de la barra lateral.
label_new_resource_planner: Nuevo planificador de recursos
label_resource_planner: Planificador de recursos
@@ -114,9 +122,23 @@ es:
user_card:
caption: Crea una vista basada en los usuarios y consulta sus datos y su asignación en una lista de fichas de usuario
label: Lista de tarjetas de usuario
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Añadir paquete de trabajo
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: Todavía no hay paquetes de trabajo que se ajusten a los filtros de esta vista.
@@ -57,6 +57,10 @@ et:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ et:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ et:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ eu:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ eu:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ eu:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ fa:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ fa:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ fa:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ fi:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ fi:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ fi:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ fil:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ fil:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ fil:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ fr:
make_private: Rendre privé
make_public: Rendre public
unfavorite: Supprimer des favoris
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ fr:
caption: Une liste personnalisée d'éléments que vous ajoutez et supprimez manuellement. Le filtrage n'est pas possible.
label: Sélection manuelle
title: Configurer la vue
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Faites de cette vue un favori pour l'ajouter à la section supérieure du menu de la barre latérale.
label_new_resource_planner: Nouveau planificateur de ressources
label_resource_planner: Planificateur de ressources
@@ -114,9 +122,23 @@ fr:
user_card:
caption: Créez une vue basée sur des utilisateurs et affichez leurs informations et leur affectation dans une liste de fiches des utilisateurs
label: Liste des fiches des utilisateurs
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Ajouter un lot de travaux
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: Aucun lot de travaux ne correspond encore aux filtres de cette vue.
@@ -57,6 +57,10 @@ he:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ he:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,25 @@ he:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
two: "%{count} users"
many: "%{count} users"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ hi:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ hi:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ hi:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ hr:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ hr:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,24 @@ hr:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
few: "%{count} users"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ hu:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ hu:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ hu:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ hy:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ hy:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ hy:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ id:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ id:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,22 @@ id:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ it:
make_private: Rendi privato
make_public: Rendi pubblico
unfavorite: Rimuovi dai preferiti
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ it:
caption: Un elenco personalizzato di elementi che puoi aggiungere e rimuovere manualmente. Il filtraggio non è disponibile.
label: Selezionato manualmente
title: Configura vista
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Aggiungi questa vista ai preferiti per mostrarla nella sezione superiore del menu laterale.
label_new_resource_planner: Nuovo pianificatore di risorse
label_resource_planner: Pianificatore di risorse
@@ -114,9 +122,23 @@ it:
user_card:
caption: Crea una vista basata sugli utenti per consultare i loro dettagli e le allocazioni in un elenco di schede utente
label: Elenco di schede utenti
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Aggiungi macro-attività
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: Non ci sono ancora macro-attività che corrispondono ai filtri di questa vista.
@@ -57,6 +57,10 @@ ja:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ ja:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,22 @@ ja:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ ka:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ ka:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ ka:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ kk:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ kk:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ kk:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ ko:
make_private: 비공개로 설정
make_public: 공개로 설정
unfavorite: 즐겨찾기에서 제거
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ ko:
caption: 수동으로 추가 및 제거하는 항목의 사용자 지정 목록입니다. 필터링은 불가능합니다.
label: 수동으로 직접 선택함
title: 보기 구성
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: 이 보기를 즐겨찾기로 지정하여 사이드바 메뉴의 상단 섹션에 추가합니다.
label_new_resource_planner: 새로운 리소스 플래너
label_resource_planner: 리소스 플래너
@@ -114,9 +122,22 @@ ko:
user_card:
caption: 사용자에 기반한 보기를 만들고 사용자 카드 목록에서 해당 세부 정보 및 할당을 확인하세요
label: 사용자 카드 목록
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: 작업 패키지 추가
allocated_members:
additional: "+%{count}"
other_users:
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: 아직 이 보기의 필터와 일치하는 작업 패키지가 없습니다.
@@ -57,6 +57,10 @@ lt:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ lt:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,25 @@ lt:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
few: "%{count} users"
many: "%{count} users"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ lv:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ lv:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,24 @@ lv:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
zero: "%{count} users"
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ mn:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ mn:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,23 @@ mn:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
one: "%{count} user"
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.
@@ -57,6 +57,10 @@ ms:
make_private: Make private
make_public: Make public
unfavorite: Remove from favorites
allocation:
hours: "%{value}h"
no_work: No work planned on this work package
summary: "%{allocated} / %{scheduled}"
allocate_resource_dialog:
kind:
filter:
@@ -94,6 +98,10 @@ ms:
caption: A custom list of items you manually add and remove. Filtering is not possible.
label: Manually hand-picked
title: Configure view
edit_allocation_dialog:
submit: Save
success_message: Resource allocation updated.
title: Edit allocation
favorite_caption: Make this view a favorite to add it on the top section of the sidebar menu.
label_new_resource_planner: New resource planner
label_resource_planner: Resource planner
@@ -114,9 +122,22 @@ ms:
user_card:
caption: Create a view based on users and see their details and allocation in a list of user cards
label: Users card list
work_package_allocations_dialog:
allocate_resource: Allocate resource
context_menu_label: Allocation actions
delete_confirmation: Are you sure you want to delete this allocation? This cannot be undone.
delete_success: Resource allocation deleted.
hidden_user: Hidden user
overbooked: The user is allocated beyond their working hours during this period.
title: Allocation
work_package_list:
add_work_package_dialog:
title: Add work package
allocated_members:
additional: "+%{count}"
other_users:
other: "%{count} users"
unassigned: Unassigned
allocation_placeholder: "—"
blank:
description: There are no work packages matching this view's filters yet.

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