Plan: Process Version Tags
Overview
Add named, colored tags to process versions. Tags are project-scoped entities; process versions hold a many-to-many relationship to them. The FlowView widget gains a tag-filter bar and per-node tag display with an add-tag control. Filtering is purely client-side and does not affect the global ProcessContext.
Backend
1. New DB models (backend/models/process.py)
ProcessTag — project-level tag definition:
| Column | Type | Notes |
|---|---|---|
id |
String UUID PK | |
project_id |
String FK → projects.id CASCADE | indexed |
name |
String(100) | not null |
color |
String(32) | CSS hex, e.g. #28a745 |
created_at |
DateTime |
Add tags = relationship("ProcessTag", ...) on Project.
ProcessVersionTag — join table (M2M):
| Column | Type | Notes |
|---|---|---|
process_version_id |
Integer FK → process_versions.id CASCADE | composite PK |
tag_id |
String FK → process_tags.id CASCADE | composite PK |
added_at |
DateTime | |
added_by |
String(255) | username |
Add tags = relationship("ProcessTag", secondary=ProcessVersionTag, ...) on ProcessVersion.
New column on ProcessVersion:
| Column | Type | Notes |
|---|---|---|
tags_history |
JSON | default []; append-only log |
Each history entry (stored verbatim by name+color, not by ID, so it survives tag renames/deletions):
{
"action": "added" | "removed",
"at": "<ISO datetime>",
"by": "<username>",
"name": "<tag name>",
"color": "<tag color>"
}
2. ProcessVersion.to_dict() addition
Include current tags from the M2M relationship:
"tags": [{"id": t.id, "name": t.name, "color": t.color} for t in self.tags]
Requires selectinload(ProcessVersion.tags) wherever versions are queried.
3. Alembic migration
Single migration file: add_process_tags:
- Create process_tags table
- Create process_version_tags table
- Add tags_history JSON column to process_versions (default [])
4. New API router (backend/routers/tags.py)
Mounted at /projects/{project_id}/tags and /process/{process_id}/versions/{version}/tags.
| Method | Path | Action |
|---|---|---|
GET |
/projects/{project_id}/tags |
List project tags |
POST |
/projects/{project_id}/tags |
Create tag {name, color} |
PUT |
/projects/{project_id}/tags/{tag_id} |
Update tag name/color |
DELETE |
/projects/{project_id}/tags/{tag_id} |
Delete tag (cascade removes joins) |
POST |
/process/{process_id}/versions/{version}/tags/{tag_id} |
Add tag to version; appends to tags_history |
DELETE |
/process/{process_id}/versions/{version}/tags/{tag_id} |
Remove tag; appends removal to tags_history |
All tag mutation endpoints require the caller to be a project member (reuse the existing membership check pattern from routers/projects.py).
Register the router in backend/main.py.
Frontend
5. API functions (frontend/src/datamodel/api.js)
Add six new functions following the existing fetch pattern:
getProjectTags(projectId) // GET /projects/{id}/tags
createProjectTag(projectId, tag) // POST /projects/{id}/tags
updateProjectTag(projectId, tagId, tag) // PUT /projects/{id}/tags/{tagId}
deleteProjectTag(projectId, tagId) // DELETE /projects/{id}/tags/{tagId}
addVersionTag(processId, version, tagId) // POST /process/{id}/versions/{v}/tags/{tagId}
removeVersionTag(processId, version, tagId) // DELETE /process/{id}/versions/{v}/tags/{tagId}
6. TanStack Query hooks (frontend/src/datamodel/useQueries.js)
New query key:
projectTags: (projectId) => ['projectTags', projectId],
New hooks:
- useProjectTags(projectId) — query, staleTime: 30s
- useCreateTag(projectId) — mutation, invalidates projectTags
- useUpdateTag(projectId) — mutation, invalidates projectTags
- useDeleteTag(projectId) — mutation, invalidates projectTags
- useAddVersionTag(projectId) — mutation, on success calls invalidateProject(projectId) via ProcessContext (so version.tags refreshes)
- useRemoveVersionTag(projectId) — same invalidation
7. New shared component: TagBadge
frontend/src/widgets/FlowView/TagBadge.js
A small pill rendered with the tag's color as background:
<span style={{ background: tag.color, color: contrastColor(tag.color), ... }}>
{tag.name}
</span>
Include a tiny utility contrastColor(hex) that returns black or white based on luminance.
8. Tag filter bar: TagFilterBar
frontend/src/widgets/FlowView/TagFilterBar.js
Props: { projectTags, selectedTagIds, onToggle }
Renders a horizontal row of TagBadge buttons above the ReactFlow canvas. Clicking a tag toggles its presence in the active filter set. Active tags show a darker border/outline. If no tags are selected the bar shows a placeholder "Filter by tag…" hint.
9. Tag add/remove control in ProcessNode
frontend/src/widgets/FlowView/ProcessNode.js
Below the existing state badge, add:
- Tag chips row — maps
versionObj.tags→<TagBadge>. Each badge has an×remove button (callsremoveVersionTag). - "+ tag" button — opens a small inline dropdown listing project tags not yet on this version. Selecting one calls
addVersionTag. The dropdown also has a "New tag…" entry that opens a small inline form (name + color picker) to create a new tag and immediately apply it.
The dropdown should be a <div> with position: absolute rendered inside the node card, similar to how Bootstrap dropdowns work. Click-outside closes it.
ProcessNode receives two new props: projectTags and currentProject. The parent (FlowView/index.js) passes these down via the node data object.
10. Filter logic in FlowView/index.js
State addition:
const [selectedFilterTagIds, setSelectedFilterTagIds] = useState(new Set());
Also fetch project tags:
const { data: projectTags = [] } = useProjectTags(currentProject);
Compute visibleProcesses via useMemo whenever processes or selectedFilterTagIds changes. When the filter is empty, visibleProcesses = processes (identity — no copy). When active:
1. taggedVersions: Map<processId, Set<versionNumber>>
— versions whose tags superset selectedFilterTagIds (match by tag id)
2. transitiveDeps: Map<processId, Set<versionNumber>>
— BFS/DFS from all tagged versions along dependency edges
(dep.source_process_id → dep.source_process_version)
until no new entries
3. visibleProcesses:
for each process in processes:
if process.id in taggedVersions:
include with versions filtered to taggedVersions[process.id]
else if process.id in transitiveDeps:
include with versions filtered to transitiveDeps[process.id]
else: exclude
visibleProcesses (not processes) is passed to the node/edge building useEffect. The selectedVersions for newly visible processes default to the latest version in their filtered set. Processes absent from visibleProcesses are simply not rendered.
The tag filter bar is rendered as an absolutely-positioned div above the <ReactFlow> component, inside the outer wrapper div.
Sequence of implementation steps
- Backend models + migration
- Backend router + register in main.py
- Frontend API functions
- Frontend query hooks
TagBadgecomponentTagFilterBarcomponentProcessNodetag display + add/remove- Filter logic in
FlowView/index.js+ wireTagFilterBar
Each step can be reviewed independently. Steps 1–2 are backend-only; steps 3–8 are frontend-only with no further backend changes needed.