Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ecb08e9
feat: add label structure navigator
level09 Jul 22, 2026
15557f7
style: compact label navigator
level09 Jul 22, 2026
d3ab58e
style: simplify label tree rows
level09 Jul 23, 2026
076e0df
fix(labels): accept path fields the edit form sends back
level09 Jul 25, 2026
1c5099f
Merge branch 'main' into feat/label-structure-navigator
level09 Jul 25, 2026
56230e6
fix(labels): open the tree on click and mark grouping-only labels
level09 Jul 25, 2026
88d727e
Merge remote-tracking branch 'origin/feat/label-structure-navigator' …
level09 Jul 25, 2026
e1af946
fix(labels): let the activator open the label tree menu
level09 Jul 25, 2026
13eb3f0
style(labels): align the Arabic label line with the English one
level09 Jul 25, 2026
37f53b5
fix(labels): distinguish retired labels from grouping nodes in the tree
level09 Jul 25, 2026
933bbbe
feat(labels): pin the label tree as a side dock over the workspace
level09 Jul 25, 2026
ab27898
Revert "feat(labels): pin the label tree as a side dock over the work…
level09 Jul 25, 2026
bcd821c
feat(labels): open the label tree from the labels field inside entity…
level09 Jul 25, 2026
23c7967
fix(labels): read tree translations from the window global
level09 Jul 25, 2026
ce62335
feat(labels): float the label tree as a draggable panel with its own …
level09 Jul 25, 2026
c8111e3
feat(labels): let the label tree button be dragged and remember where…
level09 Jul 25, 2026
0a04fec
feat(labels): sit the label tree button in the app bar and free its d…
level09 Jul 25, 2026
d35c304
feat(labels): show only the leaf segment of each label in the tree
level09 Jul 25, 2026
2ef2cd6
feat(labels): pin the selected label's full path above the tree and t…
level09 Jul 25, 2026
14a1853
feat(labels): render the selected path as a breadcrumb with the label…
level09 Jul 25, 2026
74d974e
style(labels): shrink the breadcrumb so long paths fit without scrolling
level09 Jul 25, 2026
0bc52c4
feat(labels): split labels from verified labels in the tree and start…
level09 Jul 25, 2026
64f5f2d
feat(labels): show the Arabic path under the English one in the bread…
level09 Jul 25, 2026
eca3eb5
Merge branch 'main' into feat/label-structure-navigator
level09 Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions enferno/admin/templates/admin/jsapi.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,26 @@ thisFieldIsRequired_: "{{ _('This field is required.') }}",
pleaseEnterAValidNumber_: "{{ _('Please enter a valid number.') }}",


}

// shared by the label tree in the app bar and inside the entity dialogs
var labelTreeTranslations = {
labelStructure: "{{ _('Label Structure') }}",
readOnly: "{{ _('Read only') }}",
search: "{{ _('Search labels') }}",
close: "{{ _('Close') }}",
loadError: "{{ _('Could not load labels') }}",
retry: "{{ _('Retry') }}",
noLabels: "{{ _('No labels found') }}",
manageLabels: "{{ _('Manage Labels') }}",
bulletins: "{{ _('Bulletins') }}",
actors: "{{ _('Actors') }}",
incidents: "{{ _('Incidents') }}",
offline: "{{ _('Offline') }}",
labels: "{{ _('Labels') }}",
verifiedLabels: "{{ _('Verified Labels') }}",
groupingOnly: "{{ _('Grouping only, cannot be assigned') }}",
retired: "{{ _('Retired, kept for existing records only') }}",
}

// add localized configurations for lists with static values
Expand Down
6 changes: 6 additions & 0 deletions enferno/admin/templates/nav-bar.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
<v-spacer></v-spacer>

<template v-slot:append>
{% if current_user.roles_in(['Admin', 'Mod', 'DA']) %}
<label-structure-navigator
:can-manage="{{ current_user.roles_in(['Admin', 'Mod'])|tojson }}"

></label-structure-navigator>
{% endif %}
<update-banner></update-banner>
{% if config.OCR_PROVIDER == 'google_vision' %}
<v-tooltip location="bottom">
Expand Down
3 changes: 3 additions & 0 deletions enferno/admin/validation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,9 @@ class LabelValidationModel(StrictValidationModel):
# sent by tree view edit (build_tree node shape), not used by from_json
parent_label_id: Optional[int] = None
children: Optional[List] = None
# computed by to_dict for hierarchy display, not used by from_json
path: Optional[str] = None
path_ar: Optional[str] = None


class LabelRequestModel(BaseValidationModel):
Expand Down
2 changes: 1 addition & 1 deletion enferno/admin/views/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def api_labels() -> Response:


@admin.route("/api/labels/tree")
@roles_accepted("Admin", "Mod")
@roles_accepted("Admin", "Mod", "DA")
def api_labels_tree() -> Response:
"""Return labels as nested tree structure for admin tree view."""
verified_param = request.args.get("verified", None)
Expand Down
137 changes: 137 additions & 0 deletions enferno/static/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,143 @@ div.jsondiffpatch-child-node-type-array:after {
max-height: calc(100vh - 80px - 0.75rem);
}

.label-structure-panel {
width: min(720px, calc(100vw - 24px));
font-size: 13px;
}

.label-structure-panel .v-card-title {
min-height: 36px;
font-size: 0.95rem;
}

.label-structure-panel .v-field,
.label-structure-panel .v-btn {
font-size: 13px;
}

.label-structure-tree {
flex: 1 1 auto;
overflow-y: auto;
}

/* both sit above Vuetify's overlay layer (2400) so they survive a fullscreen dialog */
/* position comes from the component so it can be dragged. the app sets global
v-btn defaults (text variant, no elevation), so the shadow is restated here
rather than relying on the elevation prop */
.label-structure-fab {
position: fixed;
z-index: 2600;
cursor: move;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14);
}

.label-structure-dock {
position: fixed;
z-index: 2600;
max-height: min(80vh, 720px);
display: flex;
flex-direction: column;
/* native resize handle rather than a hand-rolled one, for the rare label that
still runs long */
resize: horizontal;
overflow: hidden;
min-width: 360px;
max-width: calc(100vw - 24px);
}

.label-structure-handle {
cursor: move;
user-select: none;
}

.label-structure-tree .v-list-item {
min-height: 26px;
padding-inline-end: 6px;
}

/* every level costs one indent step plus the width Vuetify reserves for the
prepend slot; both are trimmed so a six level path still fits */
.label-structure-tree .v-list-group {
--list-indent-size: 12px;
--prepend-width: 24px;
}

.label-structure-tree .v-list-item__spacer {
width: 4px;
}

.label-structure-tree .v-list-item__prepend > .v-list-item-action {
margin-inline-end: 2px;
}

/* the selected row's full path, pinned under the search box. this is the one
line he reads, so it outranks the tree rows in size and colour */
.label-structure-path {
flex: 0 0 auto;
/* small enough that a six level path fits without scrolling sideways, which
is the point of showing it at all */
font-size: 11.5px;
line-height: 1.05rem;
color: rgb(var(--v-theme-primary));
background: rgba(var(--v-theme-primary), 0.1);
border-block: thin solid rgba(var(--v-theme-primary), 0.25);
overflow-x: auto;
white-space: nowrap;
}

.label-structure-path-line {
display: flex;
align-items: center;
gap: 4px;
}

/* dir=rtl orders the segments right to left; the line itself stays left aligned
so it reads as a second line under the English one */
.label-structure-path-ar {
text-align: left;
white-space: nowrap;
}

.label-structure-scope .v-btn {
height: 24px;
font-size: 11px;
letter-spacing: 0;
text-transform: none;
}

.label-structure-crumb-sep {
opacity: 0.5;
}

.label-structure-crumb-leaf {
font-weight: 600;
}

.label-structure-tree .v-list-item-title,
.label-structure-title {
font-size: 12.5px;
line-height: 1.05rem;
}

.label-structure-title-ar {
font-size: 10.5px;
line-height: 0.95rem;
/* dir=rtl keeps the bidi order correct; alignment follows the English line above */
text-align: left;
}

.label-structure-tree .v-chip {
height: 16px;
padding-inline: 4px;
font-size: 9px;
}

.label-structure-panel .v-card-actions {
min-height: 32px;
padding: 2px 8px;
}


/* steppers enhancements */
.v-stepper > .v-window {
Expand Down
Loading
Loading