diff --git a/src/backend/api/database/library_metric_values/migrations/0002_librarymetricvalue_description.py b/src/backend/api/database/library_metric_values/migrations/0002_librarymetricvalue_description.py new file mode 100644 index 00000000..fac801d9 --- /dev/null +++ b/src/backend/api/database/library_metric_values/migrations/0002_librarymetricvalue_description.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-03-11 18:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_metric_values', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='librarymetricvalue', + name='description', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/src/backend/api/database/library_metric_values/models.py b/src/backend/api/database/library_metric_values/models.py index 9513776c..4fa46f31 100644 --- a/src/backend/api/database/library_metric_values/models.py +++ b/src/backend/api/database/library_metric_values/models.py @@ -13,6 +13,7 @@ class LibraryMetricValue(models.Model): metric = models.ForeignKey(Metric, on_delete=models.CASCADE) value = models.JSONField(null=True, blank=True) evidence = models.TextField(blank=True, null=True) + description = models.TextField(blank=True, null=True) collected_by = models.CharField(max_length=100, blank=True, null=True) last_modified = models.DateTimeField(auto_now=True) diff --git a/src/backend/api/database/library_metric_values/serializers.py b/src/backend/api/database/library_metric_values/serializers.py index c31660f7..5db4848c 100644 --- a/src/backend/api/database/library_metric_values/serializers.py +++ b/src/backend/api/database/library_metric_values/serializers.py @@ -14,4 +14,4 @@ class FlatMetricValueSerializer(serializers.ModelSerializer): # Note: These fields directly reference the ForeignKey IDs class Meta: model = LibraryMetricValue - fields = ('library', 'metric', 'value') \ No newline at end of file + fields = ('library', 'metric', 'value', 'description') \ No newline at end of file diff --git a/src/backend/api/database/library_metric_values/views.py b/src/backend/api/database/library_metric_values/views.py index f595be7c..2871e073 100644 --- a/src/backend/api/database/library_metric_values/views.py +++ b/src/backend/api/database/library_metric_values/views.py @@ -158,6 +158,7 @@ def domain_comparison(request, domain_id): if lib_id in by_lib: by_lib[lib_id]["metrics"][metric_name] = val.value by_lib[lib_id]["metrics"][f"{metric_name}_evidence"] = val.evidence + by_lib[lib_id]["metrics"][f"{metric_name}_description"] = val.description return Response( { @@ -191,6 +192,9 @@ def post(self, request, library_id): if key.endswith("_evidence"): metric_name = key.replace("_evidence", "") field_to_update = "evidence" + elif key.endswith("_description"): + metric_name = key.replace("_description", "") + field_to_update = "description" else: metric_name = key field_to_update = "value" @@ -204,7 +208,7 @@ def post(self, request, library_id): if field_to_update == "value": error_message, validated_value = validate_metric_value(metric, value_to_store) if error_message: - return Response({"error": error_message}, status=status.HTTP_400_BAD_REQUEST) + return Response({"error": f"{metric_name}: {error_message}"}, status=status.HTTP_400_BAD_REQUEST) value_to_store = validated_value LibraryMetricValue.objects.update_or_create( diff --git a/src/frontend/src/pages/ComparisonTool.tsx b/src/frontend/src/pages/ComparisonTool.tsx index d6dc21b2..d5191892 100644 --- a/src/frontend/src/pages/ComparisonTool.tsx +++ b/src/frontend/src/pages/ComparisonTool.tsx @@ -15,6 +15,7 @@ interface Metric { metric_ID: string; metric_name: string; metric_key?: string | null; + description?: string | null; } interface LibraryMetricRow { @@ -102,107 +103,74 @@ const ExpandableText: React.FC<{ lines?: 2 | 3; emptyText?: string; textStyle?: React.CSSProperties; -}> = ({ text, lines = 2, emptyText = "—", textStyle }) => { + description?: string; +}> = ({ text, lines = 2, emptyText = "—", textStyle, description }) => { const [open, setOpen] = useState(false); const [truncated, setTruncated] = useState(false); - const textRef = React.useRef(null); + const wrapRef = React.useRef(null); + const textRef = React.useRef(null); - React.useLayoutEffect(() => { - const el = textRef.current; - if (!el) return; + const clampStyle = lines === 2 ? clamp2Style : clamp3Style; - const check = () => { - setTruncated( - el.scrollHeight > el.clientHeight + 1 || - el.scrollWidth > el.clientWidth + 1 - ); - }; - - check(); - window.addEventListener("resize", check); - return () => window.removeEventListener("resize", check); - }, [text, lines]); + useEffect(() => { + if (textRef.current) { + const isOverflowing = textRef.current.scrollHeight > textRef.current.clientHeight; + setTruncated(isOverflowing); + } + }, [text]); - React.useEffect(() => { + useEffect(() => { if (!open) return; - - const onDocClick = (e: MouseEvent) => { - if (!wrapRef.current) return; - if (!wrapRef.current.contains(e.target as Node)) { + const handleClick = (e: MouseEvent) => { + if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) { setOpen(false); } }; - - const onEsc = (e: KeyboardEvent) => { - if (e.key === "Escape") setOpen(false); - }; - - document.addEventListener("mousedown", onDocClick); - document.addEventListener("keydown", onEsc); - return () => { - document.removeEventListener("mousedown", onDocClick); - document.removeEventListener("keydown", onEsc); - }; + window.addEventListener("mousedown", handleClick); + return () => window.removeEventListener("mousedown", handleClick); }, [open]); - if (!text) { + const showMoreButton = truncated || !!description; + + if (!text && !description) { return
{emptyText}
; } - const clampStyle = lines === 3 ? clamp3Style : clamp2Style; - return ( -
-
- {text} +
+
+ {text || "—"}
- {truncated && ( + {showMoreButton && ( )} {open && (
-
{text}
- - + {description ? ( + <> +
+ Value: +
+
{text || "—"}
+
+ Description: +
+
{description}
+ + ) : ( + text + )}
)}
@@ -297,18 +265,22 @@ const ComparisonToolPage: React.FC = () => { row.github_url || "", ...metricList.map((m) => { const v = row.metrics[m.metric_name]; + const vDesc = row.metrics[`${m.metric_name}_description`]; if (m.metric_key === "gitstats_report") { const url = v ? String(v) : ""; - if (!url) return ""; if (url.startsWith("http://") || url.startsWith("https://")) return url; if (url.startsWith("/")) return `${SITE_BASE}${url}`; - return url; } - return v ?? ""; + const mainValue = v ?? ""; + if (vDesc) { + return `${mainValue} (${vDesc})`; + } + + return mainValue; }), ]; @@ -465,7 +437,37 @@ const ComparisonToolPage: React.FC = () => { }} title={m.metric_name} > -
{m.metric_name}
+
+
{m.metric_name}
+ + {/* Added Help Icon Badge */} + {m.description && ( + (e.currentTarget.style.opacity = "1")} + onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.6")} + > + ? + + )} +
))} @@ -597,56 +599,59 @@ const ComparisonToolPage: React.FC = () => { {metricList.map((m) => { - const cellVal = row.metrics[m.metric_name]; + const cellVal = row.metrics[m.metric_name]; + const cellDesc = row.metrics[`${m.metric_name}_description`]; + + if (m.metric_key === "gitstats_report") { + const url = cellVal ? String(cellVal) : null; + return ( + +
+ {url ? ( + + View report + + ) : ( + "—" + )} +
+ + ); + } - if (m.metric_key === "gitstats_report") { - const url = cellVal ? String(cellVal) : null; - return ( - -
- {url ? ( - - View report - - ) : ( - "—" - )} -
- - ); - } - - return ( - - - - ); - })} + return ( + + + + ); +})} ))} diff --git a/src/frontend/src/pages/Edit.tsx b/src/frontend/src/pages/Edit.tsx index 9caf0a73..18a5d8e3 100644 --- a/src/frontend/src/pages/Edit.tsx +++ b/src/frontend/src/pages/Edit.tsx @@ -698,8 +698,9 @@ const EditMetricValuesModal: React.FC<{ > {metricList.map((m) => { const cellVal = row.metrics[m.metric_name]; + const descVal = row.metrics[m.metric_name + "_description"] || ""; const fieldError = fieldErrors[m.metric_name]; - const desc = (m.description || "").trim(); + const staticDesc = (m.description || "").trim(); return (
- {desc ? ( - ({desc}) - ) : ( -   - )} + {staticDesc ? `(${staticDesc})` :  }
- {m.metric_key === "gitstats_report" ? ( - cellVal ? ( - + {m.metric_key === "gitstats_report" ? ( + cellVal ? ( + + View report + + ) : ( +
+ ) + ) : m.scoring_dict && Object.keys(m.scoring_dict).length > 0 ? ( + ) : ( -
- — -
- ) - ) : m.scoring_dict && Object.keys(m.scoring_dict).length > 0 ? ( - - ) : ( - onChangeValue(m.metric_name, e.target.value)} + disabled={pageLoading} + style={{ borderColor: fieldError ? "rgba(255, 99, 99, 0.75)" : undefined }} + /> + )} +
+ +
+
+ Notes / Comments +
+