Merge pull request #21421 from opf/merge-release/17.0-20251212035035

Merge release/17.0 into dev
This commit is contained in:
Klaus Zanders
2025-12-12 13:45:40 +01:00
committed by GitHub
91 changed files with 565 additions and 244 deletions
+1 -1
View File
@@ -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
View File
@@ -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) }
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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. Dont forget to save your changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 145 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,
@@ -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
@@ -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');
}
@@ -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>
@@ -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
+6 -3
View File
@@ -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));
+1 -1
View File
@@ -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]);
}
@@ -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;
}
}
/**
+28 -1
View File
@@ -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
+66 -1
View File
@@ -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|
+2 -2
View File
@@ -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
+12 -4
View File
@@ -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
+18 -1
View File
@@ -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