mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge pull request #21421 from opf/merge-release/17.0-20251212035035
Merge release/17.0 into dev
This commit is contained in:
@@ -198,7 +198,7 @@ gem "aws-sdk-core", "~> 3.239"
|
||||
# File upload via fog + screenshots on travis
|
||||
gem "aws-sdk-s3", "~> 1.205"
|
||||
|
||||
gem "openproject-token", "~> 8.2.0"
|
||||
gem "openproject-token", "~> 8.3.0"
|
||||
|
||||
gem "plaintext", "~> 0.3.7"
|
||||
|
||||
|
||||
+3
-3
@@ -891,7 +891,7 @@ GEM
|
||||
activesupport (>= 7.2.0)
|
||||
openproject-octicons (>= 19.30.1)
|
||||
view_component (>= 3.1, < 5.0)
|
||||
openproject-token (8.2.0)
|
||||
openproject-token (8.3.0)
|
||||
activemodel
|
||||
openssl (3.3.1)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
@@ -1666,7 +1666,7 @@ DEPENDENCIES
|
||||
openproject-reporting!
|
||||
openproject-storages!
|
||||
openproject-team_planner!
|
||||
openproject-token (~> 8.2.0)
|
||||
openproject-token (~> 8.3.0)
|
||||
openproject-two_factor_authentication!
|
||||
openproject-webhooks!
|
||||
openproject-xls_export!
|
||||
@@ -2048,7 +2048,7 @@ CHECKSUMS
|
||||
openproject-reporting (1.0.0)
|
||||
openproject-storages (1.0.0)
|
||||
openproject-team_planner (1.0.0)
|
||||
openproject-token (8.2.0) sha256=96d17e10f2c920e92a2e318d4f4e6557a4079ecd4c3121bd322bc75c01e6fb73
|
||||
openproject-token (8.3.0) sha256=67fcbc2319e050e4b64ee970477769a8589c5e94ec4be842cdd554ff715db7b7
|
||||
openproject-two_factor_authentication (1.0.0)
|
||||
openproject-webhooks (1.0.0)
|
||||
openproject-xls_export (1.0.0)
|
||||
|
||||
@@ -151,11 +151,12 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
|
||||
mobile_icon: nil, # Do not show on mobile as it is already part of the menu
|
||||
mobile_label: nil,
|
||||
href:,
|
||||
target: "_self",
|
||||
data: {
|
||||
turbo_stream: true,
|
||||
turbo_method: method
|
||||
},
|
||||
target: ""
|
||||
test_selector: "header-save-button"
|
||||
) do
|
||||
render(
|
||||
Primer::Beta::Octicon.new(
|
||||
|
||||
@@ -58,7 +58,7 @@ class Projects::Settings::TemplateController < Projects::SettingsController
|
||||
.call(templated: ActiveRecord::Type::Boolean.new.cast(params[:value]))
|
||||
|
||||
render_error_flash_message_via_turbo_stream(message: call.message) if call.failure?
|
||||
update_via_turbo_stream(component: Projects::Settings::Template::SettingsComponent.new(@project))
|
||||
update_via_turbo_stream(component: Projects::Settings::Template::SettingsComponent.new(@project.reload))
|
||||
|
||||
respond_with_turbo_streams do |format|
|
||||
format.html { project_settings_template_path(@project) }
|
||||
|
||||
@@ -791,7 +791,7 @@ af:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Stuur uitnodiging"
|
||||
success_message:
|
||||
|
||||
@@ -819,7 +819,7 @@ ar:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ az:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -805,7 +805,7 @@ be:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ bg:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -788,7 +788,7 @@ ca:
|
||||
required: "Si us plau, selecciona un rol"
|
||||
message:
|
||||
label: "Missatge d'invitació"
|
||||
description: "Enviarem un correu electrònic a l'usuari, al qual pots afegir un missatge personal aquí. Una explicació per a la invitació pot ser útil, o potser una mica d'informació amb relació al projecte per ajudar als usuaris a començar."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Enviar la invitació"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ ckb-IR:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -805,7 +805,7 @@ cs:
|
||||
required: "Vyberte prosím roli"
|
||||
message:
|
||||
label: "Pozvánka"
|
||||
description: "Pošleme uživateli e-mail, ke kterému můžete přidat osobní zprávu. Vysvětlení pozvánky by mohlo být užitečné, nebo pár informací o projektu, které by jim pomohly začít."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Odeslat pozvánku"
|
||||
success_message:
|
||||
|
||||
@@ -789,7 +789,7 @@ da:
|
||||
required: "Vælg venligst en rolle"
|
||||
message:
|
||||
label: "Invitations besked"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -788,7 +788,7 @@ de:
|
||||
required: "Bitte eine Rolle auswählen"
|
||||
message:
|
||||
label: "Einladungsnachricht"
|
||||
description: "Wir senden dem Benutzer eine E-Mail, der Sie hier eine persönliche Nachricht ergänzen können. Eine Erklärung für die Einladung oder nützliche Informationen zum eingeladenen Projekt könnten nützlich sein, dem Benutzer bei der Mitarbeit zu unterstützen."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Einladung senden"
|
||||
success_message:
|
||||
|
||||
@@ -787,7 +787,7 @@ el:
|
||||
required: "Παρακαλούμε επιλέξτε ένα ρόλο"
|
||||
message:
|
||||
label: "Μήνυμα πρόσκλησης"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Αποστολή πρόσκλησης"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ eo:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Sendi inviton"
|
||||
success_message:
|
||||
|
||||
@@ -789,7 +789,7 @@ es:
|
||||
required: "Por favor, seleccione un rol"
|
||||
message:
|
||||
label: "Mensaje de invitación"
|
||||
description: "Enviaremos un correo electrónico al usuario, al cual puede añadir un mensaje personal aquí. Una explicación sobre la invitación podría ser útil, o bien información básica sobre el proyecto para ayudarles a empezar."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Enviar invitación"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ et:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Saada kutse"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ eu:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ fa:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "ارسال دعوت نامه"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ fi:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Lähetä kutsu"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ fil:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Magpadala ng imbitasyon"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ fr:
|
||||
required: "Veuillez sélectionner un rôle"
|
||||
message:
|
||||
label: "Message d'invitation"
|
||||
description: "Nous enverrons un e-mail à l'utilisateur, auquel vous pourrez ajouter un message personnel ici. Une explication concernant l'invitation pourrait être utile ou peut-être quelques informations concernant le projet pour l'aider à démarrer."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Envoyer une invitation"
|
||||
success_message:
|
||||
|
||||
@@ -805,7 +805,7 @@ he:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ hi:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "आमंत्रण भेजें"
|
||||
success_message:
|
||||
|
||||
@@ -798,7 +798,7 @@ hr:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Pošalji pozivnicu"
|
||||
success_message:
|
||||
|
||||
@@ -790,7 +790,7 @@ hu:
|
||||
required: "Kérjük, válasszon szerepet"
|
||||
message:
|
||||
label: "Meghívó üzenet"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Meghívó küldése"
|
||||
success_message:
|
||||
|
||||
@@ -780,7 +780,7 @@ id:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Kirim undangan"
|
||||
success_message:
|
||||
|
||||
@@ -789,7 +789,7 @@ it:
|
||||
required: "Sei pregato di selezionare un ruolo"
|
||||
message:
|
||||
label: "Messaggio di invito"
|
||||
description: "Invieremo un'email all'utente, a cui puoi aggiungere qui un messaggio personale. Una spiegazione per l'invito potrebbe essere utile o forse qualche informazione sul progetto per aiutarli a iniziare."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Invia invito"
|
||||
success_message:
|
||||
|
||||
@@ -782,7 +782,7 @@ ja:
|
||||
required: "ロールを選択してください"
|
||||
message:
|
||||
label: "招待メッセージ"
|
||||
description: "私たちはユーザーに電子メールを送信します。ここに個人的なメッセージを追加することができます。招待状に関する説明や、プロジェクトに関するちょっとした情報などを添えていただくと、より効果的です。"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "招待状を送信"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ ka:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ kk:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -784,7 +784,7 @@ ko:
|
||||
required: "역할을 선택하십시오"
|
||||
message:
|
||||
label: "초대 메시지"
|
||||
description: "사용자에게 이메일이 전송될 것입니다. 여기에 개인 메시지를 추가할 수도 있습니다. 초대에 관한 유용한 설명을 넣거나, 프로젝트를 시작하는 데 도움이 되는 정보를 넣을 수 있습니다."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "초대장 보내기"
|
||||
success_message:
|
||||
|
||||
@@ -802,7 +802,7 @@ lt:
|
||||
required: "Prašome parinkti vaidmenį"
|
||||
message:
|
||||
label: "Kvietimo pranešimas"
|
||||
description: "Mes siųsime el. pašto laišką naudotojui, į kurį jūs galite pridėti asmeninį pranešimą. Paaiškinimas, kodėl šis pranešimas buvo atsiųstas, galėtų būti naudingas gavėjui. Taipogi šiek tiek informacijos apie projektą galbūt galėtų padėti pradėti darbą su projektu."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Siųsti pakvietimą"
|
||||
success_message:
|
||||
|
||||
@@ -798,7 +798,7 @@ lv:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Nosūtīt ielūgumu"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ mn:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -783,7 +783,7 @@ ms:
|
||||
required: "Sila pilih peranan"
|
||||
message:
|
||||
label: "Mesej jemputan"
|
||||
description: "Kami akan menghantar e-mel ke pengguna, dimana anda boleh menambah mesej peribadi di sini. Satu penerangan untuk jemputan akan berguna, atau mungkin sedikit maklumat berkenaan projek untuk bantu mereka bermula."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Hantar jemputan"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ ne:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -788,7 +788,7 @@ nl:
|
||||
required: "Selecteer een rol"
|
||||
message:
|
||||
label: "Uitnodigings bericht"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Verstuur uitnodiging"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@
|
||||
required: "Vennligst velg en rolle"
|
||||
message:
|
||||
label: "Melding i invitasjon"
|
||||
description: "Vi vil sende en e-post til brukeren, som du kan skrive en personlig melding til her. En forklaring på invitasjonen kan være nyttig, eller kanskje en litt informasjon om prosjektet for å hjelpe dem i gang."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitasjon"
|
||||
success_message:
|
||||
|
||||
@@ -802,7 +802,7 @@ pl:
|
||||
required: "Wybierz rolę"
|
||||
message:
|
||||
label: "Wiadomość z zaproszeniem"
|
||||
description: "Wyślemy do użytkownika wiadomość e-mail, do której możesz tutaj dodać prywatną wiadomość. Może to być wyjaśnienie zaproszenia lub ułatwiające rozpoczęcie pracy informacje na temat projektu."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Wyślij zaproszenie"
|
||||
success_message:
|
||||
|
||||
@@ -790,7 +790,7 @@ pt-BR:
|
||||
required: "Selecione um papel"
|
||||
message:
|
||||
label: "Mensagem de convite"
|
||||
description: "Enviaremos um e-mail para o usuário, à qual você pode adicionar uma mensagem pessoal por aqui. Uma explicação para o convite pode se útil, ou talvez algumas informações sobre o projeto para ajudá-lo a começar."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Enviar Convite"
|
||||
success_message:
|
||||
|
||||
@@ -789,7 +789,7 @@ pt-PT:
|
||||
required: "Selecione um perfil"
|
||||
message:
|
||||
label: "Mensagem de convite"
|
||||
description: "Enviaremos um e-mail ao utilizador, no qual pode adicionar uma mensagem pessoal. Uma explicação para o convite pode ajudar, ou talvez algumas informações sobre o projeto para que ele/ela possa começar."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Enviar convite"
|
||||
success_message:
|
||||
|
||||
@@ -798,7 +798,7 @@ ro:
|
||||
required: "Te rog selectează un rol"
|
||||
message:
|
||||
label: "Mesaj invitație"
|
||||
description: "Vom trimite un e-mail utilizatorului, la care puteți adăuga un mesaj personal aici. Ar putea fi utilă o explicație pentru invitație sau poate câteva informații despre proiect pentru a-i ajuta să înceapă."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Trimite invitație"
|
||||
success_message:
|
||||
|
||||
@@ -804,7 +804,7 @@ ru:
|
||||
required: "Пожалуйста, выберите роль"
|
||||
message:
|
||||
label: "Сообщение о приглашении"
|
||||
description: "Мы отправим пользователю электронное письмо, к которому вы можете что-нибудь добавить. Приглашение облегчит им начало работы, если у них будет немного информации о проекте."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Отправить приглашение"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ rw:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ si:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "ආරාධනය යවන්න"
|
||||
success_message:
|
||||
|
||||
@@ -805,7 +805,7 @@ sk:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Poslať pozvánku"
|
||||
success_message:
|
||||
|
||||
@@ -804,7 +804,7 @@ sl:
|
||||
required: "Izberite vlogo"
|
||||
message:
|
||||
label: "Sporočilo s povabilom"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Pošlji povabilo"
|
||||
success_message:
|
||||
|
||||
@@ -798,7 +798,7 @@ sr:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ sv:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Skicka inbjudan"
|
||||
success_message:
|
||||
|
||||
@@ -784,7 +784,7 @@ th:
|
||||
required: "กรุณาเลือกบทบาท"
|
||||
message:
|
||||
label: "ข้อความเชิญ"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -792,7 +792,7 @@ tr:
|
||||
required: "Lütfen bir rol seçin."
|
||||
message:
|
||||
label: "Davet Mesajı"
|
||||
description: "Kullanıcıya, buradan kişisel bir mesaj ekleyebileceğiniz bir e-posta göndereceğiz. Davet için bir açıklama yararlı olabilir veya belki de başlamalarına yardımcı olacak projeyle ilgili bir parça bilgi olabilir."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Davet gönder"
|
||||
success_message:
|
||||
|
||||
@@ -802,7 +802,7 @@ uk:
|
||||
required: "Виберіть роль"
|
||||
message:
|
||||
label: "Повідомлення із запрошенням"
|
||||
description: "Ми надішлемо електронний лист користувачу, у яке ви можете додати особисте повідомлення. Радимо додати пояснення про запрошення або, можливо, деяку інформацію про проєкт, щоб спростити користувачу початок роботи."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Надіслати запрошення"
|
||||
success_message:
|
||||
|
||||
@@ -791,7 +791,7 @@ uz:
|
||||
required: "Please select a role"
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
success_message:
|
||||
|
||||
@@ -784,7 +784,7 @@ vi:
|
||||
required: "Vui lòng chọn một vai trò"
|
||||
message:
|
||||
label: "Tin nhắn mời"
|
||||
description: "Chúng tôi sẽ gửi email đến người dùng, bạn có thể thêm một tin nhắn cá nhân tại đây. Một lời giải thích về lời mời có thể hữu ích, hoặc có thể là một chút thông tin về dự án để giúp họ bắt đầu."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "Gửi lời mời"
|
||||
success_message:
|
||||
|
||||
@@ -781,7 +781,7 @@ zh-CN:
|
||||
required: "请选择一个角色"
|
||||
message:
|
||||
label: "邀请消息"
|
||||
description: "我们将向用户发送一封电子邮件,您可以在此处添加要向其发送的个人消息。提供邀请说明可能会很有用,您也可以添加一些有关该项目的信息以帮助他们开始。"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "发送邀请"
|
||||
success_message:
|
||||
|
||||
@@ -783,7 +783,7 @@ zh-TW:
|
||||
required: "請選擇角色"
|
||||
message:
|
||||
label: "邀請訊息"
|
||||
description: "我們會寄送一封電子郵件給使用者,您可以在此加入個人訊息。說明邀請的原因或提供一些有關專案的資訊,能夠幫助他們更順利開始使用。"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
summary:
|
||||
next_button: "發送邀請函"
|
||||
success_message:
|
||||
|
||||
@@ -848,7 +848,7 @@ en:
|
||||
|
||||
message:
|
||||
label: "Invitation message"
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or prehaps a bit of information regarding the project to help them get started."
|
||||
description: "We will send an email to the user, to which you can add a personal message here. An explanation for the invitation could be useful, or perhaps a bit of information regarding the project to help them get started."
|
||||
|
||||
summary:
|
||||
next_button: "Send invitation"
|
||||
|
||||
@@ -68,6 +68,10 @@ You can migrate Confluence content into OpenProject using Markdown export and ma
|
||||
|
||||
This approach preserves most layout elements and is recommended for documentation or knowledge base content.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> This approach is only suitable for very few wiki pages. For a comprehensive Confluence migration, consider using our recommended alternative: migrating your Confluence spaces to [XWiki](https://migration.xwiki.com/en/Alternatives/xwiki-vs-confluence ), our open source partner for advanced and large-scale wiki and knowledge-base migrations. XWiki provides a more complete and scalable path when moving extensive Confluence content. [Find out more](https://www.openproject.org/alternative-atlassian-jira-data-center/).
|
||||
|
||||
### 5. Community-developed JIRA importer
|
||||
|
||||
A community-developed tool, the [OpenProject JIRA Importer](https://github.com/dotnetfactory/openproject-jira-importer) provides additional import capabilities.
|
||||
|
||||
@@ -75,6 +75,15 @@ The **Payload URL** must point to your OpenProject server's GitHub webhook endpo
|
||||
> [!NOTE]
|
||||
> For the events that should be triggered by the webhook, please select "Send me everything".
|
||||
|
||||
> [!IMPORTANT]
|
||||
> OpenProject only supports the following GitHub events:
|
||||
>
|
||||
> - check_run
|
||||
> - issue_comment
|
||||
> - ping
|
||||
> - pull_request
|
||||
> If the GitHub webhook sends an event that OpenProject does not support, a 404 error is returned by OpenProject.
|
||||
|
||||
You will need the API key you copied earlier in OpenProject. Append it to the *Payload URL* as a simple GET parameter named `key`. In the end the URL should look something like this:
|
||||
|
||||
`https://myopenproject.com/webhooks/github?key=42`
|
||||
|
||||
@@ -87,6 +87,9 @@ For the events that should be triggered by the webhook, please select the follow
|
||||
> [!NOTE]
|
||||
> Please note that the *Pipeline events* part of the integration is still in the early stages. If you have any feedback on the *Pipeline events*, please let us know [here](https://community.openproject.org/wp/54574).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> OpenProject only supports the events listed above. If the GitLab webhook sends an event that OpenProject does not support, a 404 error is returned by OpenProject.
|
||||
|
||||
> [!TIP]
|
||||
> If you are in a local network you might need to allow requests to the local network in your GitLab instance. You can find this settings in the **Outbound requests** section when you navigate to **Admin area -> Settings -> Network**.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Once you are in the right tab:
|
||||
|
||||
You will be able to adapt the following:
|
||||
|
||||
1. Adapt which **status changes** are allowed by the selected role for the selected work package type. Read the transitions from the Rows (Current status) to the columns (New status allowed), e.g. a status transition from NEW to IN PROGRESS and back would be allowed. Make sure to allow the "way back" in most cases, e.g. back from IN PROGRESS to NEW, to make sure you will be able to correct mistakes.
|
||||
1. Adapt which status changes are allowed by the selected role for the selected work package type. The matrix shows the **current status on the Y axis (rows)** and the **new status allowed on the X axis (columns)**. Read transitions from the rows to the columns, e.g., if the cell at the intersection of **NEW (row)** and **IN PROGRESS (column)** is checked, a transition from **NEW → IN PROGRESS** is allowed. If you want the role to be able to change statuses in **both directions** (e.g., from **NEW → IN PROGRESS** and from **IN PROGRESS → NEW**), make sure both corresponding cells are checked. In most workflows, allowing the “way back” (e.g., back from **IN PROGRESS** to **NEW**) is important so that mistakes can be corrected.
|
||||
2. In addition, you can specify if this role is allowed to make specific status changes if the user who has been assigned this role also is the **author of the work package**.
|
||||
3. Also you can set additional status transitions allowed if the user is the **assignee to a work package**.
|
||||
4. Don’t forget to save your changes.
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 145 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
@@ -61,6 +61,7 @@ import {
|
||||
import {
|
||||
ProjectPhaseAutocompleterComponent,
|
||||
} from './project-phase-autocompleter/project-phase-autocompleter.component';
|
||||
import { IconModule } from 'core-app/shared/components/icon/icon.module';
|
||||
|
||||
export const OPENPROJECT_AUTOCOMPLETE_COMPONENTS = [
|
||||
CreateAutocompleterComponent,
|
||||
@@ -95,6 +96,7 @@ export const OPENPROJECT_AUTOCOMPLETE_COMPONENTS = [
|
||||
DynamicModule,
|
||||
OpenprojectPrincipalRenderingModule,
|
||||
InviteUserButtonModule,
|
||||
IconModule,
|
||||
],
|
||||
exports: OPENPROJECT_AUTOCOMPLETE_COMPONENTS,
|
||||
declarations: OPENPROJECT_AUTOCOMPLETE_COMPONENTS,
|
||||
|
||||
+49
-12
@@ -3,8 +3,36 @@
|
||||
let-item
|
||||
let-clear="clear"
|
||||
>
|
||||
<span class="ng-value-icon left" (click)="clear(item)">×</span>
|
||||
<span class="ng-value-label">{{ item.name }}</span>
|
||||
<span class="d-flex">
|
||||
<span
|
||||
class="ng-value-icon left"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
(click)="clear(item)"
|
||||
(keydown.enter)="clear(item)"
|
||||
(keydown.space)="$event.preventDefault(); clear(item)"
|
||||
[attr.aria-label]="this.I18n.t('js.autocomplete_select.remove', { name: item.name })"
|
||||
>×</span>
|
||||
<span class="ng-value-label d-flex gap-2">
|
||||
<span class="ellipsis">{{ item.name }}</span>
|
||||
@if (portfolioModelsEnabled) {
|
||||
@switch (item._type) {
|
||||
@case ('Portfolio') {
|
||||
<span class="color-fg-muted flex-shrink-0">
|
||||
<svg briefcase-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.portfolio') }}
|
||||
</span>
|
||||
}
|
||||
@case ('Program') {
|
||||
<span class="color-fg-muted flex-shrink-0">
|
||||
<svg versions-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.program') }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template
|
||||
@@ -16,20 +44,29 @@
|
||||
class="ng-option-label"
|
||||
[ngStyle]="{ 'padding-left.px': item.numberOfAncestors * 16 }"
|
||||
>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<div
|
||||
<span class="d-flex gap-2">
|
||||
<span
|
||||
[title]="item.name"
|
||||
class="ellipsis"
|
||||
[opSearchHighlight]="search"
|
||||
>{{ item.name }}</div>
|
||||
@if (shouldShowWorkspaceTypeBadge(item)) {
|
||||
<div
|
||||
class="color-fg-muted"
|
||||
[innerHTML]="workspaceTypeIconWithLabel(item)"
|
||||
style="flex-shrink: 0;"
|
||||
></div>
|
||||
>{{ item.name }}</span>
|
||||
@if (portfolioModelsEnabled) {
|
||||
@switch (item._type) {
|
||||
@case ('Portfolio') {
|
||||
<span class="color-fg-muted flex-shrink-0">
|
||||
<svg briefcase-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.portfolio') }}
|
||||
</span>
|
||||
}
|
||||
@case ('Program') {
|
||||
<span class="color-fg-muted flex-shrink-0">
|
||||
<svg versions-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.program') }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
@if (item.disabled && item.disabledReason) {
|
||||
<div
|
||||
|
||||
+3
-43
@@ -33,16 +33,9 @@ import {
|
||||
ViewChild,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ConfigurationService } from 'core-app/core/config/configuration.service';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { IAutocompleterTemplateComponent } from 'core-app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component';
|
||||
import {
|
||||
toDOMString,
|
||||
versionsIconData,
|
||||
briefcaseIconData,
|
||||
SVGData,
|
||||
} from '@openproject/octicons-angular';
|
||||
import { IProjectAutocompleteItem } from './project-autocomplete-item';
|
||||
|
||||
@Component({
|
||||
templateUrl: './project-autocompleter-template.component.html',
|
||||
@@ -54,40 +47,7 @@ export class ProjectAutocompleterTemplateComponent implements IAutocompleterTemp
|
||||
@ViewChild('labelTemplate') labelTemplate?:TemplateRef<Element>;
|
||||
|
||||
readonly I18n = inject(I18nService);
|
||||
readonly sanitizer = inject(DomSanitizer);
|
||||
readonly configuration = inject(ConfigurationService);
|
||||
|
||||
shouldShowWorkspaceTypeBadge(project:IProjectAutocompleteItem):boolean {
|
||||
return !!project._type && project._type !== 'Project';
|
||||
}
|
||||
|
||||
workspaceTypeIconWithLabel(project:IProjectAutocompleteItem):SafeHtml {
|
||||
const workspaceType = project._type;
|
||||
if (!workspaceType) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const iconData = this.workspaceTypeSVGData(workspaceType);
|
||||
if (!iconData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const htmlString = toDOMString(iconData, 'small', { 'aria-hidden': 'true', class: 'octicon' });
|
||||
const translatedTypeName = this.I18n.t(`js.include_workspaces.types.${workspaceType.toLowerCase()}`);
|
||||
const iconWithText = htmlString + ' ' + translatedTypeName;
|
||||
return this.sanitizer.bypassSecurityTrustHtml(iconWithText);
|
||||
}
|
||||
|
||||
private workspaceTypeSVGData(workspaceType:string):SVGData|undefined {
|
||||
switch (workspaceType) {
|
||||
case 'Program': {
|
||||
return versionsIconData;
|
||||
}
|
||||
case 'Portfolio': {
|
||||
return briefcaseIconData;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
public portfolioModelsEnabled = this.configuration.activeFeatureFlags.includes('portfolioModels');
|
||||
}
|
||||
|
||||
+28
-2
@@ -32,7 +32,20 @@
|
||||
></svg>
|
||||
}
|
||||
@if (portfolioModelsEnabled) {
|
||||
<span class="color-fg-muted" [innerHTML]="workspaceTypeIconWithLabel(project)"></span>
|
||||
@switch (project._type) {
|
||||
@case ('Portfolio') {
|
||||
<span class="color-fg-muted">
|
||||
<svg briefcase-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.portfolio') }}
|
||||
</span>
|
||||
}
|
||||
@case ('Program') {
|
||||
<span class="color-fg-muted">
|
||||
<svg versions-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.program') }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
@if (currentProjectService.id === project.id.toString()) {
|
||||
@@ -60,7 +73,20 @@
|
||||
>
|
||||
<span>{{ project.name }}</span>
|
||||
@if (portfolioModelsEnabled) {
|
||||
<span class="color-fg-muted" [innerHTML]="workspaceTypeIconWithLabel(project)"></span>
|
||||
@switch (project._type) {
|
||||
@case ('Portfolio') {
|
||||
<span class="color-fg-muted">
|
||||
<svg briefcase-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.portfolio') }}
|
||||
</span>
|
||||
}
|
||||
@case ('Program') {
|
||||
<span class="color-fg-muted">
|
||||
<svg versions-icon size="small" />
|
||||
{{ this.I18n.t('js.include_workspaces.types.program') }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
-36
@@ -20,13 +20,6 @@ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service
|
||||
import { ConfigurationService } from 'core-app/core/config/configuration.service';
|
||||
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
|
||||
import { getMetaContent } from 'core-app/core/setup/globals/global-helpers';
|
||||
import {
|
||||
toDOMString,
|
||||
briefcaseIconData,
|
||||
SVGData,
|
||||
versionsIconData,
|
||||
} from '@openproject/octicons-angular';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: '[op-header-project-select-list]',
|
||||
@@ -69,8 +62,6 @@ export class OpHeaderProjectSelectListComponent implements OnInit, OnChanges {
|
||||
readonly elementRef:ElementRef,
|
||||
readonly cdRef:ChangeDetectorRef,
|
||||
readonly currentProjectService:CurrentProjectService,
|
||||
// eslint-disable-next-line @angular-eslint/prefer-inject
|
||||
readonly sanitizer:DomSanitizer
|
||||
) { }
|
||||
|
||||
ngOnInit():void {
|
||||
@@ -127,31 +118,4 @@ export class OpHeaderProjectSelectListComponent implements OnInit, OnChanges {
|
||||
|
||||
return `${url}?jump=${encodeURIComponent(currentMenuItem)}`;
|
||||
}
|
||||
|
||||
workspaceTypeIconWithLabel(project:IProjectData):SafeHtml {
|
||||
const workspaceType = project._type.toLowerCase();
|
||||
const iconData = this.workspaceTypeSVGData(workspaceType);
|
||||
if (!iconData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const htmlString = toDOMString(iconData, 'small', { 'aria-hidden': 'true', class: 'octicon' });
|
||||
const translatedTypeName = this.I18n.t(`js.include_workspaces.types.${workspaceType}`);
|
||||
const iconWithText = htmlString + ' ' + translatedTypeName;
|
||||
return this.sanitizer.bypassSecurityTrustHtml(iconWithText);
|
||||
}
|
||||
|
||||
private workspaceTypeSVGData(workspaceType:string):SVGData|undefined{
|
||||
switch (workspaceType) {
|
||||
case 'program': {
|
||||
return versionsIconData;
|
||||
}
|
||||
case 'portfolio': {
|
||||
return briefcaseIconData;
|
||||
}
|
||||
default: {
|
||||
return undefined; // Case fallthrough for eslint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +96,9 @@ import {
|
||||
ListUnorderedIconComponent,
|
||||
OpStopwatchStartIconComponent,
|
||||
ChevronDownIconComponent,
|
||||
VersionsIconComponent,
|
||||
BriefcaseIconComponent,
|
||||
} from '@openproject/octicons-angular';
|
||||
import { OpBaselineComponent } from 'core-app/features/work-packages/components/wp-baseline/baseline/baseline.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -200,6 +201,8 @@ import { OpBaselineComponent } from 'core-app/features/work-packages/components/
|
||||
PlusIconComponent,
|
||||
ReplyIconComponent,
|
||||
TriangleDownIconComponent,
|
||||
VersionsIconComponent,
|
||||
BriefcaseIconComponent,
|
||||
],
|
||||
declarations: [
|
||||
OpIconComponent,
|
||||
@@ -304,6 +307,8 @@ import { OpBaselineComponent } from 'core-app/features/work-packages/components/
|
||||
PlusIconComponent,
|
||||
ReplyIconComponent,
|
||||
TriangleDownIconComponent,
|
||||
VersionsIconComponent,
|
||||
BriefcaseIconComponent,
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -140,9 +140,12 @@ div.autocomplete
|
||||
height: initial !important
|
||||
min-height: initial !important
|
||||
|
||||
.ng-value-label
|
||||
.ng-value-label:not(:has(> *))
|
||||
display: initial !important
|
||||
|
||||
.ng-value-label:has(> *)
|
||||
min-width: 0
|
||||
|
||||
.ng-placeholder
|
||||
// Fix overflow by providing a max width to the input
|
||||
//
|
||||
|
||||
@@ -73,6 +73,7 @@ div.button_form
|
||||
|
||||
.advanced-filters--filter-value
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
|
||||
.advanced-filters--filter-value2
|
||||
margin-left: 0.5rem
|
||||
|
||||
@@ -34,7 +34,6 @@ import { BlockNoteView } from '@blocknote/mantine';
|
||||
import { getDefaultReactSlashMenuItems, SuggestionMenuController, useCreateBlockNote } from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { IUploadFile } from 'core-app/core/upload/upload.service';
|
||||
import { LiveCollaborationManager } from 'core-stimulus/helpers/live-collaboration-helpers';
|
||||
import { initializeOpBlockNoteExtensions, openProjectWorkPackageBlockSpec, openProjectWorkPackageSlashMenu } from 'op-blocknote-extensions';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import * as Y from 'yjs';
|
||||
@@ -76,10 +75,12 @@ export default function OpBlockNoteContainer({ inputField,
|
||||
|
||||
initializeOpBlockNoteExtensions({ baseUrl: openProjectUrl, locale: localeString });
|
||||
|
||||
let doc = LiveCollaborationManager.ydoc;
|
||||
let doc:Y.Doc;
|
||||
|
||||
let editorParams:Partial<BlockNoteEditorOptions<typeof schema.blockSchema, typeof schema.inlineContentSchema, typeof schema.styleSchema>>;
|
||||
if(hocuspocusProvider) {
|
||||
doc = hocuspocusProvider.document;
|
||||
|
||||
editorParams = {
|
||||
schema,
|
||||
collaboration: {
|
||||
@@ -95,7 +96,9 @@ export default function OpBlockNoteContainer({ inputField,
|
||||
dictionary: localeDictionary,
|
||||
...(isReadyForAttachmentUpload() && { uploadFile }),
|
||||
};
|
||||
} else { // collaboration disabled
|
||||
} else { // collaboration disabled (for test environments)
|
||||
doc = new Y.Doc();
|
||||
|
||||
if (inputText) {
|
||||
try {
|
||||
const update = Uint8Array.from(atob(inputText), c => c.charCodeAt(0));
|
||||
|
||||
@@ -81,7 +81,6 @@ function useCollaborationProvider(
|
||||
return () => {
|
||||
provider.off('synced', onSynced);
|
||||
provider.off('disconnect', onDisconnect);
|
||||
provider.destroy();
|
||||
};
|
||||
}, [provider, onSynced, onDisconnect]);
|
||||
}
|
||||
@@ -100,6 +99,7 @@ function useLocalDocumentSync(doc:Y.Doc, inputField:HTMLInputElement, enabled:bo
|
||||
|
||||
return () => {
|
||||
doc.off('update', updateInput);
|
||||
doc.destroy();
|
||||
};
|
||||
}, [doc, inputField, enabled]);
|
||||
}
|
||||
|
||||
+11
-8
@@ -31,6 +31,8 @@
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { LiveCollaborationManager } from 'core-stimulus/helpers/live-collaboration-helpers';
|
||||
import type { Doc } from 'yjs';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
@@ -44,14 +46,15 @@ export default class extends Controller {
|
||||
declare readonly documentNameValue:string;
|
||||
|
||||
connect():void {
|
||||
LiveCollaborationManager.initializeYjsProvider(
|
||||
new HocuspocusProvider({
|
||||
url: this.hocuspocusUrlValue,
|
||||
name: this.documentNameValue,
|
||||
token: this.oauthTokenValue,
|
||||
document: LiveCollaborationManager.ydoc,
|
||||
})
|
||||
);
|
||||
const ydoc:Doc = new Y.Doc();
|
||||
const provider = new HocuspocusProvider({
|
||||
url: this.hocuspocusUrlValue,
|
||||
name: this.documentNameValue,
|
||||
token: this.oauthTokenValue,
|
||||
document: ydoc,
|
||||
});
|
||||
|
||||
LiveCollaborationManager.initializeYjsProvider(provider, ydoc);
|
||||
}
|
||||
|
||||
disconnect():void {
|
||||
|
||||
@@ -42,7 +42,7 @@ export default class extends ApplicationController {
|
||||
declare readonly usersTarget:HTMLElement;
|
||||
declare readonly popoverTarget:HTMLElement;
|
||||
|
||||
private provider:HocuspocusProvider|undefined;
|
||||
private provider:HocuspocusProvider|null = null;
|
||||
private currentUsers = new Map<number, LiveUser>();
|
||||
|
||||
connect() {
|
||||
@@ -57,8 +57,11 @@ export default class extends ApplicationController {
|
||||
|
||||
disconnect() {
|
||||
this.currentUsers.clear();
|
||||
|
||||
this.provider?.off('awarenessUpdate', this.onAwarenessUpdate);
|
||||
this.provider?.on('stateless', this.onStateless);
|
||||
this.provider?.off('stateless', this.onStateless);
|
||||
|
||||
this.provider = null;
|
||||
}
|
||||
|
||||
toggle_popover() {
|
||||
@@ -66,7 +69,11 @@ export default class extends ApplicationController {
|
||||
}
|
||||
|
||||
private onAwarenessUpdate = (data:onAwarenessUpdateParameters) => {
|
||||
const changed = this.updateUsers(data.states);
|
||||
const awarenessStates = data.states;
|
||||
|
||||
if (awarenessStates.length === 0) return;
|
||||
|
||||
const changed = this.updateUsers(awarenessStates);
|
||||
if (changed) {
|
||||
this.triggerUpdateUsersUI();
|
||||
}
|
||||
|
||||
@@ -30,41 +30,42 @@
|
||||
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import type { Doc } from 'yjs';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
type Listener = (provider:HocuspocusProvider) => void;
|
||||
|
||||
class LiveCollaborationManagerClass {
|
||||
ydocInstance:Doc|null = null;
|
||||
yjsProviderInstance:HocuspocusProvider|null = null;
|
||||
yjsDocInstance:Doc|null = null;
|
||||
|
||||
private listeners:Listener[] = [];
|
||||
|
||||
/**
|
||||
* Initializes the YJS Provider
|
||||
* @param provider The provider to use
|
||||
* @param doc The Y.Doc instance to use
|
||||
* @returns void
|
||||
*/
|
||||
initializeYjsProvider(provider:HocuspocusProvider) {
|
||||
initializeYjsProvider(provider:HocuspocusProvider, doc:Doc) {
|
||||
this.destroy(); // Clean up old state first
|
||||
this.yjsProviderInstance = provider;
|
||||
this.yjsDocInstance = doc;
|
||||
this.listeners.forEach((listener) => listener(this.yjsProviderInstance!));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a shared Y.Doc instance
|
||||
*/
|
||||
get ydoc():Doc {
|
||||
this.ydocInstance ??= new Y.Doc();
|
||||
|
||||
return this.ydocInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the shared Y.Doc instance.
|
||||
* Cleans up the collaboration provider and Y.Doc instance.
|
||||
* This method should be called when a collaboration session is ended
|
||||
*/
|
||||
destroy():void {
|
||||
this.ydocInstance = null;
|
||||
if (this.yjsProviderInstance) {
|
||||
this.yjsProviderInstance.destroy();
|
||||
this.yjsProviderInstance = null;
|
||||
}
|
||||
|
||||
if (this.yjsDocInstance) {
|
||||
this.yjsDocInstance.destroy();
|
||||
this.yjsDocInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,9 +34,10 @@ RSpec.describe "Projects", "editing settings", :js do
|
||||
include_context "ng-select-autocomplete helpers"
|
||||
|
||||
let(:permissions) { %i(edit_project view_project_attributes edit_project_attributes) }
|
||||
let(:project_memberships) { { project => permissions } }
|
||||
|
||||
current_user do
|
||||
create(:user, member_with_permissions: { project => permissions })
|
||||
create(:user, member_with_permissions: project_memberships)
|
||||
end
|
||||
|
||||
shared_let(:project) do
|
||||
@@ -291,4 +292,30 @@ RSpec.describe "Projects", "editing settings", :js do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "workspace type badges in Subproject of field", with_flag: { portfolio_models: true } do
|
||||
shared_let(:portfolio) { create(:portfolio, name: "Parent Portfolio") }
|
||||
shared_let(:program) { create(:program, name: "Parent Program") }
|
||||
shared_let(:regular_project) { create(:project, name: "Regular Project") }
|
||||
let(:parent_permissions) { %i[add_subprojects] }
|
||||
|
||||
let(:project_memberships) do
|
||||
{ project => permissions,
|
||||
portfolio => parent_permissions,
|
||||
program => parent_permissions,
|
||||
regular_project => parent_permissions }
|
||||
end
|
||||
|
||||
it "displays workspace type badges for portfolios and programs" do
|
||||
general_page = Pages::Projects::Settings::General.new(project)
|
||||
general_page.visit!
|
||||
|
||||
parent_field = general_page.parent_project_field
|
||||
|
||||
# Verify badges for different project types
|
||||
parent_field.expect_option("Parent Portfolio", workspace_badge: "Portfolio")
|
||||
parent_field.expect_option("Parent Program", workspace_badge: "Program")
|
||||
parent_field.expect_option("Regular Project")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -127,7 +127,7 @@ RSpec.describe "Project list sharing",
|
||||
|
||||
wait_for_reload
|
||||
|
||||
projects_index_page.expect_can_only_save_as_label
|
||||
projects_index_page.expect_save_as_label
|
||||
projects_index_page.save_query_as("Member-of and active list")
|
||||
|
||||
wait_for_network_idle
|
||||
@@ -290,7 +290,7 @@ RSpec.describe "Project list sharing",
|
||||
|
||||
projects_index_page.open_filters
|
||||
projects_index_page.filter_by_active("yes")
|
||||
projects_index_page.expect_can_only_save_as_label
|
||||
projects_index_page.expect_save_as_label
|
||||
projects_index_page.save_query_as("Member-of and active list")
|
||||
|
||||
wait_for_network_idle
|
||||
|
||||
@@ -254,7 +254,7 @@ RSpec.describe "Persisted lists on projects index page",
|
||||
projects_page.open_filters
|
||||
projects_page.filter_by_membership("yes")
|
||||
|
||||
wait_for_reload # chnaging filters is still done via page reload
|
||||
wait_for_reload # changing filters is still done via page reload
|
||||
|
||||
# Since the query is static, no save button an no menu item is shown
|
||||
projects_page.expect_no_notification("Save")
|
||||
@@ -586,6 +586,71 @@ RSpec.describe "Persisted lists on projects index page",
|
||||
end
|
||||
end
|
||||
|
||||
it "allows saving queries via the header save button" do
|
||||
projects_page.visit!
|
||||
projects_page.set_sidebar_filter("Active projects")
|
||||
|
||||
expect_angular_frontend_initialized
|
||||
|
||||
# Modify the query
|
||||
projects_page.open_filters
|
||||
projects_page.filter_by_membership("yes")
|
||||
|
||||
# The "Save as" button and label should be visible
|
||||
projects_page.expect_header_save_button
|
||||
projects_page.expect_save_as_label
|
||||
|
||||
# Save the query via the header save button and the inline form
|
||||
projects_page.save_query_via_header
|
||||
projects_page.fill_in_the_name("My filtered projects")
|
||||
projects_page.click_on "Save"
|
||||
|
||||
wait_for_network_idle
|
||||
|
||||
# The "Save as" button should not be visible after saving the query.
|
||||
projects_page.expect_no_header_save_button
|
||||
|
||||
# The new query should be saved and selected
|
||||
projects_page.expect_sidebar_filter("My filtered projects", selected: true)
|
||||
|
||||
# The filters should be stored
|
||||
projects_page.expect_filter_count(2)
|
||||
projects_page.expect_filter_set("active")
|
||||
projects_page.expect_filter_set("member_of")
|
||||
|
||||
# Modify the saved query and save it again
|
||||
projects_page.open_filters
|
||||
projects_page.set_filter(list_custom_field.column_name,
|
||||
list_custom_field.name,
|
||||
"is (OR)",
|
||||
[list_custom_field.possible_values.first.value])
|
||||
wait_for_reload
|
||||
|
||||
# The "Save" button and label should be visible
|
||||
projects_page.expect_header_save_button
|
||||
projects_page.expect_save_label
|
||||
|
||||
# Save the query
|
||||
projects_page.save_query_via_header
|
||||
wait_for_reload
|
||||
|
||||
# The "Save" button should not be visible after saving the query.
|
||||
projects_page.expect_no_header_save_button
|
||||
|
||||
# Reload the query and verify all filters were saved
|
||||
projects_page.set_sidebar_filter("Active projects")
|
||||
projects_page.set_sidebar_filter("My filtered projects")
|
||||
|
||||
# All filters should be stored
|
||||
projects_page.expect_filter_count(3)
|
||||
projects_page.expect_filter_set "member_of"
|
||||
projects_page.expect_filter_set "active"
|
||||
projects_page.expect_filter_set(
|
||||
list_custom_field.column_name,
|
||||
value: list_custom_field.possible_values.first.value
|
||||
)
|
||||
end
|
||||
|
||||
it "cannot access another user`s list" do
|
||||
visit projects_path(query_id: another_users_projects_list.id)
|
||||
|
||||
|
||||
@@ -31,35 +31,40 @@
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Projects autocomplete page", :js do
|
||||
let!(:user) { create(:user) }
|
||||
let(:top_menu) { Components::Projects::TopMenu.new }
|
||||
shared_let(:user) { create(:user) }
|
||||
# we only need the public permissions: view_project, :view_news
|
||||
shared_let(:role) { create(:project_role, permissions: []) }
|
||||
|
||||
let!(:project) do
|
||||
create(:project,
|
||||
name: "Plain project",
|
||||
identifier: "plain-project")
|
||||
shared_let(:portfolio) do
|
||||
create(:portfolio, name: "Test Portfolio", members: { user => role })
|
||||
end
|
||||
|
||||
let!(:project2) do
|
||||
shared_let(:program) do
|
||||
create(:program, name: "Test Program", members: { user => role })
|
||||
end
|
||||
shared_let(:project) do
|
||||
create(:project, name: "Plain project", identifier: "plain-project", members: { user => role })
|
||||
end
|
||||
shared_let(:project2) do
|
||||
create(:project,
|
||||
name: "<strong>foobar</strong>",
|
||||
identifier: "foobar")
|
||||
identifier: "foobar",
|
||||
members: { user => role })
|
||||
end
|
||||
|
||||
let!(:project3) do
|
||||
shared_let(:project3) do
|
||||
create(:project,
|
||||
name: "Plain other project",
|
||||
parent: project2,
|
||||
identifier: "plain-project-2")
|
||||
identifier: "plain-project-2",
|
||||
members: { user => role })
|
||||
end
|
||||
let!(:project4) do
|
||||
shared_let(:project4) do
|
||||
create(:project,
|
||||
name: "Project with different name and identifier",
|
||||
parent: project2,
|
||||
identifier: "plain-project-4")
|
||||
identifier: "plain-project-4",
|
||||
members: { user => role })
|
||||
end
|
||||
|
||||
let!(:other_projects) do
|
||||
shared_let(:other_projects) do
|
||||
names = [
|
||||
"Very long project name with term at the END",
|
||||
"INK14 - Foo",
|
||||
@@ -70,26 +75,15 @@ RSpec.describe "Projects autocomplete page", :js do
|
||||
names.map do |name|
|
||||
identifier = name.gsub(/[ -]+/, "-").downcase
|
||||
|
||||
create(:project, name:, identifier:)
|
||||
create(:project, name:, identifier:, members: { user => role })
|
||||
end
|
||||
end
|
||||
let!(:non_member_project) do
|
||||
create(:project)
|
||||
end
|
||||
let!(:public_project) do
|
||||
create(:public_project)
|
||||
end
|
||||
# necessary to be able to see public projects
|
||||
let!(:non_member_role) { create(:non_member) }
|
||||
# we only need the public permissions: view_project, :view_news
|
||||
let(:role) { create(:project_role, permissions: []) }
|
||||
shared_let(:non_member_project) { create(:project) }
|
||||
shared_let(:public_project) { create(:public_project) }
|
||||
|
||||
include BecomeMember
|
||||
let(:top_menu) { Components::Projects::TopMenu.new }
|
||||
|
||||
before do
|
||||
([project, project2, project3] + other_projects).each do |p|
|
||||
add_user_to_project! user:, project: p, role:
|
||||
end
|
||||
login_as user
|
||||
visit root_path
|
||||
end
|
||||
@@ -134,13 +128,15 @@ RSpec.describe "Projects autocomplete page", :js do
|
||||
|
||||
top_menu.expect_result "Plain project"
|
||||
top_menu.expect_result "<strong>foobar</strong>", disabled: true
|
||||
top_menu.expect_item_with_hierarchy_level hierarchy_level: 2, item_name: "Plain other project"
|
||||
top_menu.expect_item_with_hierarchy_level hierarchy_level: 2,
|
||||
item_name: "Plain other project"
|
||||
|
||||
# Show hierarchy of project
|
||||
top_menu.search "Plain other project"
|
||||
|
||||
top_menu.expect_result "<strong>foobar</strong>", disabled: true
|
||||
top_menu.expect_item_with_hierarchy_level hierarchy_level: 2, item_name: "Plain other project"
|
||||
top_menu.expect_item_with_hierarchy_level hierarchy_level: 2,
|
||||
item_name: "Plain other project"
|
||||
|
||||
# find terms at the end of project names
|
||||
top_menu.search "END"
|
||||
@@ -166,7 +162,8 @@ RSpec.describe "Projects autocomplete page", :js do
|
||||
top_menu.search_and_select "Plain project"
|
||||
end
|
||||
|
||||
expect(page).to have_current_path(project_news_index_path(project), ignore_query: true)
|
||||
expect(page).to have_current_path(project_news_index_path(project),
|
||||
ignore_query: true)
|
||||
expect(page).to have_css(".news-menu-item.selected")
|
||||
end
|
||||
|
||||
@@ -187,4 +184,16 @@ RSpec.describe "Projects autocomplete page", :js do
|
||||
|
||||
top_menu.expect_current_project project2.name
|
||||
end
|
||||
|
||||
it "displays workspace type badges for portfolios and programs",
|
||||
with_flag: { portfolio_models: true } do
|
||||
retry_block do
|
||||
top_menu.toggle unless top_menu.open?
|
||||
top_menu.expect_open
|
||||
|
||||
top_menu.expect_result portfolio.name, workspace_badge: "Portfolio"
|
||||
top_menu.expect_result program.name, workspace_badge: "Program"
|
||||
top_menu.expect_result project.name, workspace_badge: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Inline editing work packages", :js do
|
||||
let(:manager_permissions) { %i[view_work_packages edit_work_packages] }
|
||||
let(:manager_role) do
|
||||
create(:project_role,
|
||||
permissions: %i[view_work_packages
|
||||
edit_work_packages])
|
||||
create(:project_role, permissions: manager_permissions)
|
||||
end
|
||||
let(:manager_projects) { { project => manager_role } }
|
||||
let(:manager) do
|
||||
create(:user,
|
||||
firstname: "Manager",
|
||||
lastname: "Guy",
|
||||
member_with_roles: { project => manager_role })
|
||||
member_with_roles: manager_projects)
|
||||
end
|
||||
let(:type) { create(:type) }
|
||||
let(:status1) { create(:status) }
|
||||
let(:status2) { create(:status) }
|
||||
|
||||
let(:project) { create(:project, types: [type]) }
|
||||
let(:work_package) do
|
||||
let(:project) { create(:project, name: "Test Project", types: [type]) }
|
||||
let!(:work_package) do
|
||||
create(:work_package,
|
||||
project:,
|
||||
type:,
|
||||
@@ -45,7 +45,6 @@ RSpec.describe "Inline editing work packages", :js do
|
||||
|
||||
context "simple work package" do
|
||||
before do
|
||||
work_package
|
||||
workflow
|
||||
|
||||
wp_table.visit!
|
||||
@@ -204,4 +203,54 @@ RSpec.describe "Inline editing work packages", :js do
|
||||
wp_table.expect_work_package_listed(work_package)
|
||||
end
|
||||
end
|
||||
|
||||
context "when editing the project field with workspace types",
|
||||
with_flag: { portfolio_models: true } do
|
||||
let!(:portfolio) { create(:portfolio, name: "Test Portfolio") }
|
||||
let!(:program) { create(:program, name: "Test Program") }
|
||||
let(:manager_permissions) { %i[view_work_packages edit_work_packages move_work_packages] }
|
||||
let(:manager_projects) do
|
||||
{
|
||||
project => manager_role,
|
||||
portfolio => manager_role,
|
||||
program => manager_role
|
||||
}
|
||||
end
|
||||
let(:query) do
|
||||
create(:public_query,
|
||||
user: manager,
|
||||
project: work_package.project,
|
||||
column_names: %w[subject project])
|
||||
end
|
||||
|
||||
before do
|
||||
wp_table.visit_query query
|
||||
wp_table.expect_work_package_listed(work_package)
|
||||
end
|
||||
|
||||
it "displays workspace type badges for portfolios and programs in the project column" do
|
||||
project_field = wp_table.edit_field(work_package, :project)
|
||||
project_field.activate!
|
||||
|
||||
# Search for portfolio and verify badge
|
||||
project_field.search_for("Test Portfolio")
|
||||
project_field.expect_option(
|
||||
"Test Portfolio",
|
||||
workspace_badge: "Portfolio"
|
||||
)
|
||||
|
||||
# Search for program and verify badge
|
||||
project_field.clear_search
|
||||
project_field.search_for("Test Program")
|
||||
project_field.expect_option(
|
||||
"Test Program",
|
||||
workspace_badge: "Program"
|
||||
)
|
||||
|
||||
# Search for regular project and verify there is no badge
|
||||
project_field.clear_search
|
||||
project_field.search_for("Project")
|
||||
project_field.expect_option("Test Project", workspace_badge: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,7 +84,10 @@ Capybara.add_selector :primer_text, locator_type: [String] do
|
||||
# `node_filter` applies the filter on the elements returned by the query so
|
||||
# that error message can list them if none matches.
|
||||
node_filter :color do |node, color|
|
||||
actual = node[:class].scan(/(?<=color-fg-)[\w-]+/)
|
||||
class_attr = node[:class]
|
||||
next false unless class_attr
|
||||
|
||||
actual = class_attr.scan(/(?<=color-fg-)[\w-]+/)
|
||||
color = color.to_s
|
||||
|
||||
actual.include?(color).tap do |res|
|
||||
@@ -101,11 +104,10 @@ end
|
||||
Capybara.add_selector :octicon, locator_type: [String, Symbol] do
|
||||
label "Octicon"
|
||||
|
||||
xpath do |locator|
|
||||
xpath = XPath.descendant(:svg)
|
||||
xpath = builder(xpath).add_attribute_conditions(class: "octicon")
|
||||
xpath = builder(xpath).add_attribute_conditions(class: "octicon-#{locator.to_s.downcase}") if locator
|
||||
xpath
|
||||
css do |locator|
|
||||
css_selector = "svg.octicon"
|
||||
css_selector += ".octicon-#{locator.to_s.downcase}" if locator
|
||||
css_selector
|
||||
end
|
||||
|
||||
expression_filter(:size, valid_values: [Numeric, *Primer::Beta::Octicon::SIZE_OPTIONS]) do |expr, size|
|
||||
|
||||
@@ -43,12 +43,12 @@ module Components
|
||||
if filter_name == "name_and_identifier"
|
||||
expect(page.find_by_id(filter_name).value).not_to be_empty
|
||||
elsif value
|
||||
within("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :hidden) do
|
||||
within("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :all) do
|
||||
expect(page).to have_css(".advanced-filters--filter-value", text: value, visible: :all)
|
||||
end
|
||||
else
|
||||
expect(page)
|
||||
.to have_css("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :hidden)
|
||||
.to have_css("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :all)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -99,12 +99,20 @@ module Components
|
||||
page.find autocompleter_selector, wait: 10
|
||||
end
|
||||
|
||||
def expect_result(name, disabled: false)
|
||||
def expect_result(name, disabled: false, workspace_badge: nil)
|
||||
within search_results do
|
||||
if disabled
|
||||
page.find(autocompleter_item_disabled_title_selector, text: name)
|
||||
selector = disabled ? autocompleter_item_disabled_title_selector : autocompleter_item_title_selector
|
||||
item = page.find(selector, text: name)
|
||||
|
||||
# Skip badge verification when workspace badge is not set
|
||||
next if workspace_badge.nil?
|
||||
|
||||
if workspace_badge
|
||||
expect(item).to have_octicon
|
||||
expect(item).to have_primer_text(workspace_badge, color: :muted)
|
||||
else
|
||||
page.find(autocompleter_item_title_selector, text: name)
|
||||
expect(item).to have_no_octicon
|
||||
expect(item).to have_no_primer_text(color: :muted)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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.
|
||||
# ++
|
||||
|
||||
require_relative "project_edit_field"
|
||||
|
||||
# For work package table inline editing
|
||||
class InlineProjectEditField < ProjectEditField
|
||||
def autocompleter
|
||||
field_container.find("op-project-autocompleter")
|
||||
end
|
||||
|
||||
def field_type
|
||||
"op-project-autocompleter"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
# ++
|
||||
|
||||
require_relative "select_edit_field"
|
||||
|
||||
class ProjectEditField < SelectField
|
||||
def autocompleter
|
||||
field_container
|
||||
end
|
||||
|
||||
def search_for(query)
|
||||
search_autocomplete(autocompleter,
|
||||
query:,
|
||||
results_selector: ".ng-dropdown-panel-items")
|
||||
end
|
||||
|
||||
def dropdown
|
||||
ng_find_dropdown(autocompleter, results_selector: ".ng-dropdown-panel-items")
|
||||
end
|
||||
|
||||
def expect_option(name, workspace_badge: false)
|
||||
within(dropdown) do
|
||||
option = page.find(".ng-option", text: name)
|
||||
|
||||
if workspace_badge
|
||||
expect(option).to have_octicon
|
||||
expect(option).to have_primer_text(workspace_badge, color: :muted)
|
||||
else
|
||||
expect(option).to have_no_octicon
|
||||
expect(option).to have_no_primer_text(color: :muted)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def clear_search
|
||||
ng_select_input(autocompleter).set("")
|
||||
end
|
||||
end
|
||||
@@ -351,6 +351,19 @@ module Pages
|
||||
wait_for_network_idle
|
||||
end
|
||||
|
||||
def save_query_via_header
|
||||
page.find('[data-test-selector="header-save-button"]').click
|
||||
wait_for_network_idle
|
||||
end
|
||||
|
||||
def expect_header_save_button
|
||||
expect(page).to have_css('[data-test-selector="header-save-button"]')
|
||||
end
|
||||
|
||||
def expect_no_header_save_button
|
||||
expect(page).to have_no_css('[data-test-selector="header-save-button"]')
|
||||
end
|
||||
|
||||
def save_query_as(name)
|
||||
click_more_menu_item("Save as")
|
||||
|
||||
@@ -359,10 +372,14 @@ module Pages
|
||||
click_on "Save"
|
||||
end
|
||||
|
||||
def expect_can_only_save_as_label
|
||||
def expect_save_as_label
|
||||
expect(page).to have_text(I18n.t("lists.can_be_saved_as"))
|
||||
end
|
||||
|
||||
def expect_save_label
|
||||
expect(page).to have_text(I18n.t("lists.can_be_saved"))
|
||||
end
|
||||
|
||||
def fill_in_the_name(name)
|
||||
within '[data-test-selector="project-query-name"]' do
|
||||
fill_in "Name", with: name
|
||||
|
||||
@@ -78,6 +78,12 @@ module Pages
|
||||
def find_field_label(label_text)
|
||||
page.find(:element, :label, text: label_text)
|
||||
end
|
||||
|
||||
def parent_project_field
|
||||
@parent_project_field ||= within_section "Project relations" do
|
||||
ProjectEditField.new(page, :parent, selector: "opce-project-autocompleter")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -390,6 +390,8 @@ module Pages
|
||||
DateEditField.new container, key, is_milestone: work_package.milestone?, is_table: true
|
||||
when :estimatedTime, :remainingTime
|
||||
ProgressEditField.new container, key
|
||||
when :project
|
||||
InlineProjectEditField.new container, key
|
||||
else
|
||||
EditField.new container, key
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user