From 2fd80982ab923efa83c4bf6c0c758f49d3df79cc Mon Sep 17 00:00:00 2001 From: Erik Buer Date: Tue, 30 Jun 2026 23:23:08 +0200 Subject: [PATCH 1/9] RequirementSet settings MVP --- .../components/requirementInfoCard.js | 45 ++--- .../requirements/displayRequirement.js | 177 ++++++++++-------- .../requirements/displayRequirementSet.js | 1 - .../forms/editRequirementsSetForm.js | 98 +++++++++- .../components/requirements/modelConstants.js | 13 ++ ...ementset_created_by_is_visible_and_more.py | 58 ++++++ dokuly/requirements/models.py | 38 ++++ dokuly/requirements/requirementSetViews.py | 27 +++ 8 files changed, 353 insertions(+), 104 deletions(-) create mode 100644 dokuly/requirements/migrations/0024_requirementset_created_by_is_visible_and_more.py diff --git a/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js b/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js index 180fc45b..0ad2b280 100644 --- a/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js +++ b/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js @@ -24,6 +24,7 @@ import DeleteButton from "../../dokuly_components/deleteButton"; import NavigateButton from "../../dokuly_components/dokulyTable/components/navigateButton"; import GenericMultiSelector from "../../dokuly_components/genericMultiSelector"; import TextFieldEditor from "../../dokuly_components/dokulyTable/components/textFieldEditor"; +import { DEFAULT_REQUIREMENT_SET_SETTINGS } from "../modelConstants"; const renderAdditionalFields = (additionalFields, keyColumnMaxWidth) => { return Object.entries(additionalFields) @@ -54,6 +55,7 @@ const RequirementInfoCard = ({ rowPadding = "10px", readOnly = false, project = { id: -1 }, + requirementSetSettings = DEFAULT_REQUIREMENT_SET_SETTINGS, }) => { const navigate = useNavigate(); @@ -63,6 +65,10 @@ const RequirementInfoCard = ({ const [parentRequirement, setParentRequirement] = useState(null); const [superseedingRequirement, setSuperseedingRequirement] = useState(null); const [isTopLevelRequirement, setIsTopLevelRequiremnt] = useState(false); + const settings = { + ...DEFAULT_REQUIREMENT_SET_SETTINGS, + ...(requirementSetSettings || {}), + }; useEffect(() => { setIsTopLevelRequiremnt(item?.parent_requirement === null); @@ -279,7 +285,7 @@ const RequirementInfoCard = ({ - {(!item?.derived_from || item?.derived_from.length === 0) && ( + {settings.hierarchical_requirements_is_enabled && (!item?.derived_from || item?.derived_from.length === 0) && ( )} - {!item?.parentRequirement && + {settings.derived_from_enabled && + !item?.parentRequirement && !( (item?.derived_from.length === 0 && item?.state === "Approved") || (item?.derived_from.length === 0 && item?.state === "Rejected") @@ -348,7 +355,7 @@ const RequirementInfoCard = ({ )} {/* Cant be superseded with subrequirements. Cant be superseded if it is verified.*/} - {((item?.superseded_by !== null) || + {settings.superseded_by_enabled && ((item?.superseded_by !== null) || (number_of_subrequirements !== -1 && !item?.is_verified && item?.state !== "Approved" && item?.state !== "Rejected")) && ( )} - - - Obligation level: - - - - changeField("obligation_level", newObligationLevel) - } - dropdownValues={OBLIGATION_LEVEL_OPTIONS} - placeholder="Select Obligation Level" - borderIfPlaceholder={true} - readOnly={readOnly || - ((item?.derived_from.length === 0 && item?.state === "Approved") || (item?.derived_from.length === 0 && item?.state === "Rejected")) - } - textSize="16px" - /> - - - + {settings.external_requirement_id_is_enabled && ( + )} + {settings.requirement_type_is_enabled && ( + )} + {settings.verification_class_is_enabled && ( + )} - {created_by !== null && created_by !== undefined && ( + {settings.created_by_is_visible && created_by !== null && created_by !== undefined && ( { const pageNumber = Number.isNaN(selectedPage) || selectedPage < 1 ? 1 : selectedPage; const isRequirementLocked = requirement?.state === "Approved" || requirement?.state === "Rejected"; const hasStatementText = Boolean((requirement?.statement || "").trim()); + const requirementSetSettings = { + ...DEFAULT_REQUIREMENT_SET_SETTINGS, + hierarchical_requirements_is_enabled: + requirementSet?.hierarchical_requirements_is_enabled ?? true, + derived_from_enabled: requirementSet?.derived_from_enabled ?? true, + superseded_by_enabled: requirementSet?.superseded_by_enabled ?? true, + external_requirement_id_is_enabled: + requirementSet?.external_requirement_id_is_enabled ?? true, + requirement_type_is_enabled: requirementSet?.requirement_type_is_enabled ?? true, + verification_class_is_enabled: requirementSet?.verification_class_is_enabled ?? true, + created_by_is_visible: requirementSet?.created_by_is_visible ?? true, + verification_method_markdown_is_enabled: + requirementSet?.verification_method_markdown_is_enabled ?? true, + verification_results_markdown_is_enabled: + requirementSet?.verification_results_markdown_is_enabled ?? true, + }; return (
{ refresh={refresh} readOnly={readOnly} project={project} + requirementSetSettings={requirementSetSettings} /> {
- - -
- -
-
+ {requirementSetSettings.hierarchical_requirements_is_enabled && ( + + +
+ +
+
+ ) + } + { !hasVerificationReferences && subRequirements?.length > 0) || (requirement?.verification_method === "" && - requirement?.verification_results === "" && - !hasVerificationReferences && - requirement?.superseded_by) + requirement?.verification_results === "" && + !hasVerificationReferences && + requirement?.superseded_by) } hiddenText={verificationHiddenText} > - - - -
+ + + +
+ Method of {requirement?.verification_class} +
+ +
+ { + updateTextField(text, "verification_method"); }} - > - Method of {requirement?.verification_class} -
- -
- { - updateTextField(text, "verification_method"); - }} - showEmptyBorder={true} - readOnly={readOnly} - /> + showEmptyBorder={true} + readOnly={readOnly} + /> + + )} -
- Results -
- { - updateTextField(text, "verification_results"); - }} - showEmptyBorder={true} - readOnly={readOnly} - /> + {requirementSetSettings.verification_results_markdown_is_enabled && ( + <> +
+ Results +
+ { + updateTextField(text, "verification_results"); + }} + showEmptyBorder={true} + readOnly={readOnly} + /> + + )} +
{ referenceType="verification" />
+ @@ -430,7 +464,7 @@ const DisplayRequirement = (props) => { { - {/* Verified by: */} - {requirement?.is_verified && ( - - )} + {requirement?.is_verified && }
diff --git a/dokuly/frontend/src/components/requirements/displayRequirementSet.js b/dokuly/frontend/src/components/requirements/displayRequirementSet.js index 327c62f5..1fd5dbe7 100644 --- a/dokuly/frontend/src/components/requirements/displayRequirementSet.js +++ b/dokuly/frontend/src/components/requirements/displayRequirementSet.js @@ -164,7 +164,6 @@ const DisplayRequirementSet = (props) => { setRefresh={setRefresh} readOnly={readOnly} /> - diff --git a/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js b/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js index 030a1815..16201a8d 100644 --- a/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js +++ b/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js @@ -1,15 +1,17 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import EditButton from "../../dokuly_components/editButton"; import DokulyModal from "../../dokuly_components/dokulyModal"; -import { Form, Row } from "react-bootstrap"; +import { Form, Row, Col } from "react-bootstrap"; import { fetchProjects } from "../../projects/functions/queries"; import SubmitButton from "../../dokuly_components/submitButton"; import DeleteButton from "../../dokuly_components/deleteButton"; +import DokulyCheckFormGroup from "../../dokuly_components/dokulyCheckFormGroup"; import { deleteRequirementsSet, editRequirementSet, } from "../functions/queries"; import { useNavigate } from "react-router"; +import { DEFAULT_REQUIREMENT_SET_SETTINGS } from "../modelConstants"; const EditRequirementsSetForm = ({ requirementSet, setRefresh, readOnly }) => { const [open, setOpen] = useState(false); @@ -17,11 +19,43 @@ const EditRequirementsSetForm = ({ requirementSet, setRefresh, readOnly }) => { const [selectedProject, setSelectedProject] = useState(null); const [projects, setProjects] = useState([]); const [loadingProjects, setLoadingProjects] = useState(true); + const [settings, setSettings] = useState(DEFAULT_REQUIREMENT_SET_SETTINGS); const navigate = useNavigate(); + const mergedSettings = useMemo( + () => ({ + ...DEFAULT_REQUIREMENT_SET_SETTINGS, + hierarchical_requirements_is_enabled: + requirementSet?.hierarchical_requirements_is_enabled, + derived_from_enabled: requirementSet?.derived_from_enabled, + superseded_by_enabled: requirementSet?.superseded_by_enabled, + external_requirement_id_is_enabled: + requirementSet?.external_requirement_id_is_enabled, + requirement_type_is_enabled: requirementSet?.requirement_type_is_enabled, + verification_class_is_enabled: requirementSet?.verification_class_is_enabled, + created_by_is_visible: requirementSet?.created_by_is_visible, + verification_method_markdown_is_enabled: + requirementSet?.verification_method_markdown_is_enabled, + verification_results_markdown_is_enabled: + requirementSet?.verification_results_markdown_is_enabled, + }), + [ + requirementSet?.hierarchical_requirements_is_enabled, + requirementSet?.derived_from_enabled, + requirementSet?.superseded_by_enabled, + requirementSet?.external_requirement_id_is_enabled, + requirementSet?.requirement_type_is_enabled, + requirementSet?.verification_class_is_enabled, + requirementSet?.created_by_is_visible, + requirementSet?.verification_method_markdown_is_enabled, + requirementSet?.verification_results_markdown_is_enabled, + ] + ); + const onSubmit = () => { editRequirementSet(requirementSet.id, { display_name: requirementSetName, + ...settings, }) .then(() => { setOpen(false); @@ -67,6 +101,28 @@ const EditRequirementsSetForm = ({ requirementSet, setRefresh, readOnly }) => { } }, [requirementSet]); + useEffect(() => { + setSettings(mergedSettings); + }, [mergedSettings]); + + const toggleSetting = (key) => { + setSettings((previous) => ({ + ...previous, + [key]: !previous[key], + })); + }; + + const renderCheck = (label, key, helpText = null) => ( + toggleSetting(key)} + id={key} + showToolTip={Boolean(helpText)} + tooltipText={helpText} + /> + ); + return ( { onChange={(e) => setRequirementSetName(e.target.value)} /> +
+
Hierarchy & Structure
+ {renderCheck( + "Parent requirement (hierarchical)", + "hierarchical_requirements_is_enabled" + )} + {renderCheck("Derived from", "derived_from_enabled")} + {renderCheck("Superseded by", "superseded_by_enabled")} +
+ +
+
Core Requirement Fields
+ {renderCheck( + "External ID", + "external_requirement_id_is_enabled" + )} + {renderCheck( + "Requirement type", + "requirement_type_is_enabled" + )} + {renderCheck( + "Verification class", + "verification_class_is_enabled" + )} + {renderCheck("Created by", "created_by_is_visible")} +
+ +
+
Verification Fields
+ {renderCheck( + "Method of verification", + "verification_method_markdown_is_enabled" + )} + {renderCheck( + "Results", + "verification_results_markdown_is_enabled" + )} +
Date: Tue, 30 Jun 2026 23:32:45 +0200 Subject: [PATCH 2/9] bugfix wrong field names --- .../components/requirementInfoCard.js | 2 +- .../requirements/displayRequirement.js | 17 ++++++++++++----- .../forms/editRequirementsSetForm.js | 6 +++--- .../components/requirements/modelConstants.js | 2 +- dokuly/requirements/requirementSetViews.py | 2 +- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js b/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js index 0ad2b280..dd5f2072 100644 --- a/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js +++ b/dokuly/frontend/src/components/requirements/components/requirementInfoCard.js @@ -355,7 +355,7 @@ const RequirementInfoCard = ({ )} {/* Cant be superseded with subrequirements. Cant be superseded if it is verified.*/} - {settings.superseded_by_enabled && ((item?.superseded_by !== null) || + {settings.superseded_by_is_enabled && ((item?.superseded_by !== null) || (number_of_subrequirements !== -1 && !item?.is_verified && item?.state !== "Approved" && item?.state !== "Rejected")) && ( { const hasStatementReferences = (requirement?.statement_references || []).length > 0; const hasVerificationReferences = (requirement?.verification_references || []).length > 0; const hasAnyReferences = hasStatementReferences || hasVerificationReferences; + const selectedPage = Number.parseInt(selectedReference?.page_number, 10); const pageNumber = Number.isNaN(selectedPage) || selectedPage < 1 ? 1 : selectedPage; const isRequirementLocked = requirement?.state === "Approved" || requirement?.state === "Rejected"; @@ -231,7 +232,7 @@ const DisplayRequirement = (props) => { hierarchical_requirements_is_enabled: requirementSet?.hierarchical_requirements_is_enabled ?? true, derived_from_enabled: requirementSet?.derived_from_enabled ?? true, - superseded_by_enabled: requirementSet?.superseded_by_enabled ?? true, + superseded_by_is_enabled: requirementSet?.superseded_by_is_enabled ?? true, external_requirement_id_is_enabled: requirementSet?.external_requirement_id_is_enabled ?? true, requirement_type_is_enabled: requirementSet?.requirement_type_is_enabled ?? true, @@ -242,6 +243,10 @@ const DisplayRequirement = (props) => { verification_results_markdown_is_enabled: requirementSet?.verification_results_markdown_is_enabled ?? true, }; + const showDependenciesCard = + requirementSetSettings.hierarchical_requirements_is_enabled || + requirementSetSettings.derived_from_enabled || + requirementSetSettings.superseded_by_is_enabled; return (
{ requirementSetSettings={requirementSetSettings} /> - + {showDependenciesCard && ( + + )} { hierarchical_requirements_is_enabled: requirementSet?.hierarchical_requirements_is_enabled, derived_from_enabled: requirementSet?.derived_from_enabled, - superseded_by_enabled: requirementSet?.superseded_by_enabled, + superseded_by_is_enabled: requirementSet?.superseded_by_is_enabled, external_requirement_id_is_enabled: requirementSet?.external_requirement_id_is_enabled, requirement_type_is_enabled: requirementSet?.requirement_type_is_enabled, @@ -42,7 +42,7 @@ const EditRequirementsSetForm = ({ requirementSet, setRefresh, readOnly }) => { [ requirementSet?.hierarchical_requirements_is_enabled, requirementSet?.derived_from_enabled, - requirementSet?.superseded_by_enabled, + requirementSet?.superseded_by_is_enabled, requirementSet?.external_requirement_id_is_enabled, requirementSet?.requirement_type_is_enabled, requirementSet?.verification_class_is_enabled, @@ -151,7 +151,7 @@ const EditRequirementsSetForm = ({ requirementSet, setRefresh, readOnly }) => { "hierarchical_requirements_is_enabled" )} {renderCheck("Derived from", "derived_from_enabled")} - {renderCheck("Superseded by", "superseded_by_enabled")} + {renderCheck("Superseded by", "superseded_by_is_enabled")}
diff --git a/dokuly/frontend/src/components/requirements/modelConstants.js b/dokuly/frontend/src/components/requirements/modelConstants.js index af1fdcc2..51cb0739 100644 --- a/dokuly/frontend/src/components/requirements/modelConstants.js +++ b/dokuly/frontend/src/components/requirements/modelConstants.js @@ -44,7 +44,7 @@ export const OBLIGATION_LEVELS = ["Shall", "Should"]; export const DEFAULT_REQUIREMENT_SET_SETTINGS = { hierarchical_requirements_is_enabled: true, derived_from_enabled: true, - superseded_by_enabled: true, + superseded_by_is_enabled: true, external_requirement_id_is_enabled: true, requirement_type_is_enabled: true, verification_class_is_enabled: true, diff --git a/dokuly/requirements/requirementSetViews.py b/dokuly/requirements/requirementSetViews.py index 4aef3e5a..983d5d19 100644 --- a/dokuly/requirements/requirementSetViews.py +++ b/dokuly/requirements/requirementSetViews.py @@ -25,7 +25,7 @@ REQUIREMENT_SET_SETTING_FIELDS = ( "hierarchical_requirements_is_enabled", "derived_from_enabled", - "superseded_by_enabled", + "superseded_by_is_enabled", "external_requirement_id_is_enabled", "requirement_type_is_enabled", "verification_class_is_enabled", From 898c4903b633d281c5e4dec00ed6f24b25e5713e Mon Sep 17 00:00:00 2001 From: Erik Buer Date: Wed, 1 Jul 2026 09:11:26 +0200 Subject: [PATCH 3/9] Requirements: bugfix, allow adding verification references after approving requirement Signed-off-by: Erik Buer --- .../components/requirementDocumentReferenceSelector.js | 5 ++--- .../src/components/requirements/displayRequirement.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dokuly/frontend/src/components/requirements/components/requirementDocumentReferenceSelector.js b/dokuly/frontend/src/components/requirements/components/requirementDocumentReferenceSelector.js index 975bf93c..21e0a15a 100644 --- a/dokuly/frontend/src/components/requirements/components/requirementDocumentReferenceSelector.js +++ b/dokuly/frontend/src/components/requirements/components/requirementDocumentReferenceSelector.js @@ -31,7 +31,6 @@ const RequirementDocumentReferenceSelector = ({ referenceField = "statement_references", referenceType = "statement", }) => { - const isStateLocked = ["Approved", "Rejected"].includes(requirement?.state); const [options, setOptions] = useState([]); const [selectedReferences, setSelectedReferences] = useState([]); const [searchInput, setSearchInput] = useState(""); @@ -172,7 +171,7 @@ const RequirementDocumentReferenceSelector = ({ return (
- {!isStateLocked && ( + {!readOnly && ( {selectedReferences.length === 0 ? ( - No statement references added. + No references added. ) : (
{ /> Date: Wed, 1 Jul 2026 09:16:22 +0200 Subject: [PATCH 4/9] Show external requirement id in req peek view Signed-off-by: Erik Buer --- .../dokuly_components/requirementPill/requirementPill.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js b/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js index 37d6653d..69c275e0 100644 --- a/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js +++ b/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js @@ -8,9 +8,14 @@ const RequirementPeek = ({ requirement }) => { return (
-
+
Requirement {requirement.id}
+ {requirement.external_requirement_id && ( +
+ {requirement.external_requirement_id} +
+ )}
Date: Wed, 1 Jul 2026 09:16:53 +0200 Subject: [PATCH 5/9] allow duplicate external req ids, but give warning Signed-off-by: Erik Buer --- .../src/components/requirements/requirementsTable.js | 3 +-- dokuly/requirements/requirementViews.py | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/dokuly/frontend/src/components/requirements/requirementsTable.js b/dokuly/frontend/src/components/requirements/requirementsTable.js index a1b5a521..31fb2df5 100644 --- a/dokuly/frontend/src/components/requirements/requirementsTable.js +++ b/dokuly/frontend/src/components/requirements/requirementsTable.js @@ -132,9 +132,8 @@ const RequirementsTable = ({ ); if (duplicateExists) { toast.warning( - "External requirement ID already exists in this requirement set." + "Warning: This external requirement ID already exists in this requirement set." ); - return; } } } diff --git a/dokuly/requirements/requirementViews.py b/dokuly/requirements/requirementViews.py index 21ef5c28..db99d03b 100644 --- a/dokuly/requirements/requirementViews.py +++ b/dokuly/requirements/requirementViews.py @@ -130,16 +130,6 @@ def edit_requirement(request, id): requirement.rationale = data["rationale"] if "external_requirement_id" in data: external_requirement_id = (data["external_requirement_id"] or "").strip() - if external_requirement_id: - duplicate_exists = Requirement.objects.filter( - requirement_set=requirement.requirement_set, - external_requirement_id__iexact=external_requirement_id, - ).exclude(id=requirement.id).exists() - if duplicate_exists: - return Response( - "External requirement ID already exists in this requirement set.", - status=status.HTTP_400_BAD_REQUEST, - ) requirement.external_requirement_id = external_requirement_id if "parent_requirement" in data: requirement.parent_requirement_id = data["parent_requirement"] From 9635ef0b5a97799d9e10f8fbce58a72cf1a096b9 Mon Sep 17 00:00:00 2001 From: Erik Buer Date: Wed, 1 Jul 2026 09:25:31 +0200 Subject: [PATCH 6/9] Requirements table UX QoL Signed-off-by: Erik Buer --- .../requirements/displayRequirement.js | 1 + .../requirements/displayRequirementSet.js | 13 ++++++++ .../forms/editRequirementsSetForm.js | 4 +-- .../requirements/requirementsTable.js | 31 ++++++++++++++++++- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/dokuly/frontend/src/components/requirements/displayRequirement.js b/dokuly/frontend/src/components/requirements/displayRequirement.js index ccd1d007..585e6326 100644 --- a/dokuly/frontend/src/components/requirements/displayRequirement.js +++ b/dokuly/frontend/src/components/requirements/displayRequirement.js @@ -362,6 +362,7 @@ const DisplayRequirement = (props) => { refresh={refresh} readOnly={readOnly} profile={profile} + requirementSetSettings={requirementSetSettings} />
diff --git a/dokuly/frontend/src/components/requirements/displayRequirementSet.js b/dokuly/frontend/src/components/requirements/displayRequirementSet.js index 1fd5dbe7..fb25805f 100644 --- a/dokuly/frontend/src/components/requirements/displayRequirementSet.js +++ b/dokuly/frontend/src/components/requirements/displayRequirementSet.js @@ -19,6 +19,7 @@ import { checkProfileIsAllowedToEdit } from "../common/functions"; import { AuthContext } from "../App"; import EditRequirementsSetForm from "./forms/editRequirementsSetForm"; import RequirementsSetInfoCard from "./components/requirementsInfoCard"; +import { DEFAULT_REQUIREMENT_SET_SETTINGS } from "./modelConstants"; const DisplayRequirementSet = (props) => { const location = useLocation(); @@ -219,6 +220,18 @@ const DisplayRequirementSet = (props) => { setRefresh={setRefresh} refresh={refresh} readOnly={readOnly} + requirementSetSettings={{ + ...DEFAULT_REQUIREMENT_SET_SETTINGS, + hierarchical_requirements_is_enabled: requirementSet?.hierarchical_requirements_is_enabled ?? true, + derived_from_enabled: requirementSet?.derived_from_enabled ?? true, + superseded_by_is_enabled: requirementSet?.superseded_by_is_enabled ?? true, + external_requirement_id_is_enabled: requirementSet?.external_requirement_id_is_enabled ?? true, + requirement_type_is_enabled: requirementSet?.requirement_type_is_enabled ?? true, + verification_class_is_enabled: requirementSet?.verification_class_is_enabled ?? true, + created_by_is_visible: requirementSet?.created_by_is_visible ?? true, + verification_method_markdown_is_enabled: requirementSet?.verification_method_markdown_is_enabled ?? true, + verification_results_markdown_is_enabled: requirementSet?.verification_results_markdown_is_enabled ?? true, + }} />
diff --git a/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js b/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js index 7b74f21c..b1716164 100644 --- a/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js +++ b/dokuly/frontend/src/components/requirements/forms/editRequirementsSetForm.js @@ -150,8 +150,8 @@ const EditRequirementsSetForm = ({ requirementSet, setRefresh, readOnly }) => { "Parent requirement (hierarchical)", "hierarchical_requirements_is_enabled" )} - {renderCheck("Derived from", "derived_from_enabled")} - {renderCheck("Superseded by", "superseded_by_is_enabled")} + {renderCheck("Derived from relation", "derived_from_enabled")} + {renderCheck("Superseded by relation", "superseded_by_is_enabled")}
diff --git a/dokuly/frontend/src/components/requirements/requirementsTable.js b/dokuly/frontend/src/components/requirements/requirementsTable.js index 31fb2df5..a4951c9d 100644 --- a/dokuly/frontend/src/components/requirements/requirementsTable.js +++ b/dokuly/frontend/src/components/requirements/requirementsTable.js @@ -19,6 +19,7 @@ import { import DokulyTags from "../dokuly_components/dokulyTags/dokulyTags"; import AddButton from "../dokuly_components/AddButton"; import CheckBox from "../dokuly_components/checkBox"; +import { DEFAULT_REQUIREMENT_SET_SETTINGS } from "./modelConstants"; const RequirementsTable = ({ requirements = [], @@ -28,6 +29,7 @@ const RequirementsTable = ({ setRefresh, refresh, profile, + requirementSetSettings = DEFAULT_REQUIREMENT_SET_SETTINGS, }) => { const [tableTextSize, setTableTextSize] = useState("14px"); @@ -396,6 +398,10 @@ const RequirementsTable = ({ return hide_verification_cells(row) ? null : ( + + ) @@ -477,6 +484,28 @@ const RequirementsTable = ({ }, ]; + const allColumns = [ + { key: "id", col: columns.find((c) => c.key === "id") }, + { key: "external_requirement_id", col: columns.find((c) => c.key === "external_requirement_id"), setting: "external_requirement_id_is_enabled" }, + { key: "parent_requirement", col: columns.find((c) => c.key === "parent_requirement"), setting: "hierarchical_requirements_is_enabled" }, + { key: "derived_from", col: columns.find((c) => c.key === "derived_from"), setting: "derived_from_enabled" }, + { key: "type", col: columns.find((c) => c.key === "type"), setting: "requirement_type_is_enabled" }, + { key: "obligation_level", col: columns.find((c) => c.key === "obligation_level") }, + { key: "state", col: columns.find((c) => c.key === "state") }, + { key: "tags", col: columns.find((c) => c.key === "tags") }, + { key: "rationale", col: columns.find((c) => c.key === "rationale") }, + { key: "statement", col: columns.find((c) => c.key === "statement") }, + { key: "verification_class", col: columns.find((c) => c.key === "verification_class"), setting: "verification_class_is_enabled" }, + { key: "is_verified", col: columns.find((c) => c.key === "is_verified") }, + { key: "verification_method", col: columns.find((c) => c.key === "verification_method"), setting: "verification_method_markdown_is_enabled" }, + { key: "verification_results", col: columns.find((c) => c.key === "verification_results"), setting: "verification_results_markdown_is_enabled" }, + ]; + + const visibleColumns = allColumns + .filter(({ setting }) => !setting || requirementSetSettings[setting] !== false) + .map(({ col }) => col) + .filter(Boolean); + return ( {!readOnly && ( @@ -500,7 +529,7 @@ const RequirementsTable = ({ key={`RequirementsTable-${readOnly}`} // Force rerender to ensure readOnly state is updated tableName="RequirementsTable" data={filteredRequirements} - columns={columns} + columns={visibleColumns} itemsPerPage={50} onRowClick={rowEvents} defaultSort={{ columnNumber: 0, order: "asc" }} From 391cd86ca286a092f3f73fb03e38f2292cb8ee20 Mon Sep 17 00:00:00 2001 From: Erik Buer Date: Wed, 1 Jul 2026 09:41:21 +0200 Subject: [PATCH 7/9] DisplayRequirement: UX QoL Signed-off-by: Erik Buer --- .../components/requirements/displayRequirement.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dokuly/frontend/src/components/requirements/displayRequirement.js b/dokuly/frontend/src/components/requirements/displayRequirement.js index 585e6326..bbb7da39 100644 --- a/dokuly/frontend/src/components/requirements/displayRequirement.js +++ b/dokuly/frontend/src/components/requirements/displayRequirement.js @@ -226,6 +226,7 @@ const DisplayRequirement = (props) => { const selectedPage = Number.parseInt(selectedReference?.page_number, 10); const pageNumber = Number.isNaN(selectedPage) || selectedPage < 1 ? 1 : selectedPage; const isRequirementLocked = requirement?.state === "Approved" || requirement?.state === "Rejected"; + const isVerified = Boolean(requirement?.is_verified); const hasStatementText = Boolean((requirement?.statement || "").trim()); const requirementSetSettings = { ...DEFAULT_REQUIREMENT_SET_SETTINGS, @@ -301,7 +302,8 @@ const DisplayRequirement = (props) => { initialMarkdown={requirement?.rationale || ""} onSubmit={handleRationaleSubmit} showEmptyBorder={true} - readOnly={readOnly || requirement?.state === "Approved" || requirement?.state === "Rejected" } + showDownload={false} + readOnly={readOnly || isRequirementLocked || isVerified} /> @@ -317,7 +319,8 @@ const DisplayRequirement = (props) => { initialMarkdown={requirement?.statement || ""} onSubmit={handleStatementSubmit} showEmptyBorder={true} - readOnly={readOnly || isRequirementLocked} + showDownload={false} + readOnly={readOnly || isRequirementLocked || isVerified} /> )}
@@ -327,7 +330,7 @@ const DisplayRequirement = (props) => { /> { { updateTextField(text, "verification_method"); }} showEmptyBorder={true} + showDownload={false} readOnly={readOnly} /> @@ -438,6 +443,7 @@ const DisplayRequirement = (props) => { updateTextField(text, "verification_results"); }} showEmptyBorder={true} + showDownload={false} readOnly={readOnly} /> @@ -451,7 +457,7 @@ const DisplayRequirement = (props) => { /> Date: Wed, 1 Jul 2026 10:02:10 +0200 Subject: [PATCH 8/9] colorize external requirement id Signed-off-by: Erik Buer --- .../requirements/requirementsTable.js | 100 +++++++++++++++--- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/dokuly/frontend/src/components/requirements/requirementsTable.js b/dokuly/frontend/src/components/requirements/requirementsTable.js index a4951c9d..8ff1824c 100644 --- a/dokuly/frontend/src/components/requirements/requirementsTable.js +++ b/dokuly/frontend/src/components/requirements/requirementsTable.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { Col, Form, Row } from "react-bootstrap"; import { useNavigate } from "react-router"; import { toast } from "react-toastify"; @@ -46,7 +46,37 @@ const RequirementsTable = ({ label: state?.state, })); - const navigate = useNavigate(); + // Segment colors by depth — muted for shared prefixes, vibrant for unique suffixes. + // Mirrors the Dokuly palette: gray → teal → magenta → primary green + const SEGMENT_COLORS = ["#9CA3AF", "#108e82", "#da4678", "#165216"]; + const SEPARATOR_COLOR = "#D1D5DB"; + + // Split id into alternating [segment, separator, segment, separator, ...] tokens + function tokenizeExternalId(id) { + // Matches separators: optional spaces around / or - + return id.split(/([ \t]*[\/\-][ \t]*)/); + // Odd indices = separators, even indices = segments + } + + function renderExternalId(fullId) { + if (!fullId) return null; + const tokens = tokenizeExternalId(fullId); + let segmentIndex = 0; + return tokens.map((token, i) => { + if (i % 2 === 1) { + // separator + return ( + {token} + ); + } + const depth = segmentIndex; + segmentIndex++; + const color = SEGMENT_COLORS[Math.min(depth, SEGMENT_COLORS.length - 1)]; + return ( + {token} + ); + }); + } function hide_verification_cells(requirement) { return (requirement?.superseded_by !== null || @@ -197,18 +227,62 @@ const RequirementsTable = ({ { key: "external_requirement_id", header: "External ID", - maxWidth: "120px", - formatter: (row, column, searchString) => ( - - changeField(row.id, "external_requirement_id", newText) + maxWidth: "140px", + formatter: (row) => { + const fullId = row?.external_requirement_id || ""; + const isLocked = readOnly || row?.state === "Rejected" || row?.state === "Approved"; + + // Inline component so we can use hooks for edit toggling + const ColoredExternalIdCell = () => { + const [editing, setEditing] = useState(false); + const [value, setValue] = useState(fullId); + const inputRef = useRef(null); + + useEffect(() => { setValue(fullId); }, []); + useEffect(() => { + if (editing && inputRef.current) { + inputRef.current.focus(); + const len = inputRef.current.value.length; + inputRef.current.setSelectionRange(len, len); + } + }, [editing]); + + const commit = () => { + changeField(row.id, "external_requirement_id", value); + setEditing(false); + }; + + if (editing) { + return ( + setValue(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { + if (e.key === "Enter") commit(); + if (e.key === "Escape") { setValue(fullId); setEditing(false); } + }} + style={{ maxWidth: "130px" }} + /> + ); } - multiline={false} - searchString={searchString} - readOnly={readOnly || row?.state === "Rejected" || row?.state === "Approved"} - /> - ), + + return ( + !isLocked && setEditing(true)} + > + {fullId ? renderExternalId(fullId) : —} + + ); + }; + + return ; + }, csvFormatter: (row) => row?.external_requirement_id ? `${row?.external_requirement_id}` : "", }, From 4ed4ea581a29012313e8245d64bf8a0fefaa8d10 Mon Sep 17 00:00:00 2001 From: Erik Buer Date: Wed, 1 Jul 2026 10:03:16 +0200 Subject: [PATCH 9/9] Requirement ID, add formatting to tooltip also Signed-off-by: Erik Buer --- .../requirementPill/requirementPill.js | 5 +- .../requirements/functions/externalIdUtils.js | 40 ++++++++++++ .../requirements/requirementsTable.js | 63 +++++++------------ 3 files changed, 67 insertions(+), 41 deletions(-) create mode 100644 dokuly/frontend/src/components/requirements/functions/externalIdUtils.js diff --git a/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js b/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js index 69c275e0..d4296b27 100644 --- a/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js +++ b/dokuly/frontend/src/components/dokuly_components/requirementPill/requirementPill.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { OverlayTrigger, Popover } from "react-bootstrap"; import { getRequirement } from "../../requirements/functions/queries"; +import { renderExternalId } from "../../requirements/functions/externalIdUtils"; const RequirementPeek = ({ requirement }) => { if (!requirement) return null; @@ -12,8 +13,8 @@ const RequirementPeek = ({ requirement }) => { Requirement {requirement.id}
{requirement.external_requirement_id && ( -
- {requirement.external_requirement_id} +
+ {renderExternalId(requirement.external_requirement_id)}
)}
{ + if (i % 2 === 1) { + return ( + + {token} + + ); + } + const color = SEGMENT_COLORS[Math.min(segmentIndex, SEGMENT_COLORS.length - 1)]; + segmentIndex++; + return ( + + {token} + + ); + }); +} diff --git a/dokuly/frontend/src/components/requirements/requirementsTable.js b/dokuly/frontend/src/components/requirements/requirementsTable.js index 8ff1824c..91527f41 100644 --- a/dokuly/frontend/src/components/requirements/requirementsTable.js +++ b/dokuly/frontend/src/components/requirements/requirementsTable.js @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from "react"; -import { Col, Form, Row } from "react-bootstrap"; +import { Col, Form, Row, OverlayTrigger, Popover } from "react-bootstrap"; import { useNavigate } from "react-router"; import { toast } from "react-toastify"; @@ -20,6 +20,7 @@ import DokulyTags from "../dokuly_components/dokulyTags/dokulyTags"; import AddButton from "../dokuly_components/AddButton"; import CheckBox from "../dokuly_components/checkBox"; import { DEFAULT_REQUIREMENT_SET_SETTINGS } from "./modelConstants"; +import { renderExternalId } from "./functions/externalIdUtils"; const RequirementsTable = ({ requirements = [], @@ -46,38 +47,6 @@ const RequirementsTable = ({ label: state?.state, })); - // Segment colors by depth — muted for shared prefixes, vibrant for unique suffixes. - // Mirrors the Dokuly palette: gray → teal → magenta → primary green - const SEGMENT_COLORS = ["#9CA3AF", "#108e82", "#da4678", "#165216"]; - const SEPARATOR_COLOR = "#D1D5DB"; - - // Split id into alternating [segment, separator, segment, separator, ...] tokens - function tokenizeExternalId(id) { - // Matches separators: optional spaces around / or - - return id.split(/([ \t]*[\/\-][ \t]*)/); - // Odd indices = separators, even indices = segments - } - - function renderExternalId(fullId) { - if (!fullId) return null; - const tokens = tokenizeExternalId(fullId); - let segmentIndex = 0; - return tokens.map((token, i) => { - if (i % 2 === 1) { - // separator - return ( - {token} - ); - } - const depth = segmentIndex; - segmentIndex++; - const color = SEGMENT_COLORS[Math.min(depth, SEGMENT_COLORS.length - 1)]; - return ( - {token} - ); - }); - } - function hide_verification_cells(requirement) { return (requirement?.superseded_by !== null || requirement?.state === "Rejected" @@ -271,13 +240,29 @@ const RequirementsTable = ({ } return ( - !isLocked && setEditing(true)} + + + + {renderExternalId(fullId)} + + + + ) : <> + } + delay={{ show: 200, hide: 100 }} > - {fullId ? renderExternalId(fullId) : —} - + !isLocked && setEditing(true)} + > + {fullId ? renderExternalId(fullId) : —} + + ); };