Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions app/frontend/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,42 @@ input[type="file"]#image-upload {
padding: 2px 4px;
font-size: 11px;
}
.layer-class-input.tagify {
--tag-pad: 0.2em 0.6em;
--tag-bg: #0052bf;
--tag-text-color: #fff;
--tags-border-color: silver;
--tag-remove-bg: #ce0078;
--tag-remove-btn-color: white;
min-width: 0;
border: none;
}
.layer-class-input.tagify .tagify__tag {
margin-top: 0;
}
.layer-class-input.tagify .tagify__input {
display: none;
}
.layer-class-input.tagify .tagify__tag__removeBtn {
font-size: 9px;
margin-left: 4px;
cursor: pointer;
}
.layer-add-tag-btn {
color: #0052bf;
font: bold 1em/1.4 Arial;
border: 0;
background: none;
box-shadow: 0 0 0 2px inset currentColor;
border-radius: 50%;
width: 1.2em;
height: 1.2em;
cursor: pointer;
transition: 0.1s ease-out;
}
.layer-add-tag-btn:hover {
box-shadow: 0 0 0 4px inset currentColor;
}
.layer-status-tag {
font-size: 11px;
padding: 2px 4px;
Expand Down
102 changes: 88 additions & 14 deletions app/frontend/static/js/layerViewController.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,53 @@ class LayerViewController {
this.stateManager = stateManager;
this.layers = [];
this.selectedLayerIds = [];
this.tagifyMap = new Map();
this.globalLabelPool = [];
this.Utils = window.Utils || { dispatchCustomEvent: (n,d)=>document.dispatchEvent(new CustomEvent(n,{detail:d})) };
}

async _refreshGlobalLabels() {
const projectId = this.stateManager && this.stateManager.getActiveProjectId && this.stateManager.getActiveProjectId();
if (!projectId || !window.apiClient) {
this.globalLabelPool = [];
return;
}
try {
const res = await window.apiClient.getProjectLabels(projectId);
if (res && Array.isArray(res.labels)) {
this.globalLabelPool = res.labels
.flatMap(l => l.split(',').map(t => t.trim()))
.filter(Boolean);
} else {
this.globalLabelPool = [];
}
} catch (e) {
this.globalLabelPool = [];
}
}

_gatherLabelPool() {
const set = new Set();
this.layers.forEach(l => {
if (l.classLabel) {
l.classLabel.split(',').forEach(t => {
t = t.trim();
if (t) set.add(t);
});
}
});
return Array.from(set);
}

_updateTagifyWhitelists() {
const pool = new Set([...this.globalLabelPool, ...this._gatherLabelPool()]);
const arr = Array.from(pool);
this.tagifyMap.forEach(t => {
t.settings.whitelist = arr;
if (t.dropdown) t.dropdown.refilter();
});
}

setLayers(layers) {
// Clone incoming array so external mutations (e.g., from ActiveImageState)
// do not directly modify our internal list and cause double updates.
Expand Down Expand Up @@ -76,8 +120,11 @@ class LayerViewController {
}
}

render() {
async render() {
if (!this.containerEl) return;
await this._refreshGlobalLabels();
this.tagifyMap.forEach(t => t.destroy());
this.tagifyMap.clear();
this.containerEl.innerHTML = '';
const listEl = document.createElement('ul');
listEl.className = 'layer-list';
Expand Down Expand Up @@ -142,22 +189,20 @@ class LayerViewController {
classInput.className = 'layer-class-input';
classInput.type = 'text';
classInput.placeholder = 'label';
classInput.value = layer.classLabel || '';
classInput.title = 'Class label';
classInput.addEventListener('mousedown', (e) => e.stopPropagation());
classInput.addEventListener('click', (e) => e.stopPropagation());
classInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
classInput.blur();
classInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});
classInput.addEventListener('change', (e) => {
e.stopPropagation();
layer.classLabel = classInput.value.trim();
this.Utils.dispatchCustomEvent('layer-class-changed', { layerId: layer.layerId, classLabel: layer.classLabel });
});

const addTagBtn = document.createElement('button');
addTagBtn.type = 'button';
addTagBtn.className = 'layer-add-tag-btn';
addTagBtn.textContent = '+';

const tagifyConfig = {
whitelist: Array.from(new Set([...this.globalLabelPool, ...this._gatherLabelPool()])),
dropdown: { maxItems: 20, enabled: 0, closeOnSelect: false },
editTags: { keepInvalid: false }
};

const statusTag = document.createElement('span');
statusTag.className = `layer-status-tag ${layer.status || ''}`;
Expand All @@ -175,6 +220,9 @@ class LayerViewController {
});

li.addEventListener('click', (e) => {
if (e.target.closest('.tagify__tag__removeBtn')) {
return; // allow Tagify to handle deletion
}
const additive = e.shiftKey;
this.selectLayer(layer.layerId, additive);
});
Expand All @@ -184,12 +232,38 @@ class LayerViewController {
li.appendChild(colorInput);
li.appendChild(nameInput);
li.appendChild(classInput);
li.appendChild(addTagBtn);
li.appendChild(statusTag);
li.appendChild(deleteBtn);

const tagify = new Tagify(classInput, tagifyConfig);
if (layer.classLabel) {
tagify.addTags(layer.classLabel.split(',').map(t => t.trim()).filter(Boolean));
}
addTagBtn.addEventListener('click', (e) => {
e.stopPropagation();
tagify.addEmptyTag();
});
const updateFromTagify = () => {
layer.classLabel = tagify.value.map(t => t.value).join(',');
this.Utils.dispatchCustomEvent('layer-class-changed', { layerId: layer.layerId, classLabel: layer.classLabel });
this._updateTagifyWhitelists();
};
tagify.on('add', updateFromTagify);
tagify.on('remove', updateFromTagify);
tagify.on('edit:updated', (e) => {
if (e.detail && e.detail.data && !e.detail.data.value.trim()) {
tagify.removeTag(e.detail.tag);
}
updateFromTagify();
});
this.tagifyMap.set(layer.layerId, tagify);

listEl.appendChild(li);
});

this.containerEl.appendChild(listEl);
this._updateTagifyWhitelists();
}
}

Expand Down
13 changes: 13 additions & 0 deletions docs/annotation_workflow_progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ It will be updated as new sprints add functionality.
- **Status Toggles**: The annotation view now has "Ready" and "Skip" switches to update image status, dispatching refresh events. Switches are automatically updated when a new image loads and disabled when no image is active.
- **Unified Change Handler**: A new `onImageDataChange()` function synchronizes the layer view, caches and status toggles whenever image or layer data changes.
- **Inline Layer Editing**: Mask name and label fields accept Enter to save changes without deselecting the text field, and edits trigger the unified change handler.
- **Tagify Labels**: Class labels in the layer view appear as Tagify tags
directly on each layer. Each tag shows a small "×" remove button and the
"+" button adds new tags. Suggestions come from all labels used across the
project and update dynamically as tags change.
- **Tagify Layout Fix**: Labels now appear in-line after the name field, fully
replacing the old text input without shifting the visibility toggle.
- **Tag Removal Fix**: The "×" remove button stops event propagation without
cancelling the default Tagify behaviour so tags delete correctly and layer
selection is unaffected.
- **Label Pool Parsing**: Suggestions split comma-separated labels returned by
the backend so autocompletion lists each label individually.
- **Layer Click Handling Fix**: Clicking the remove icon no longer selects the
layer, ensuring tag deletions persist on reload.
- **Layer Persistence**: Editing a mask's name or class now sends an update to the backend so changes are saved in the project database.
- **Mask Edit Persistence**: Saving edits to a mask now updates its RLE data and status in the database so changes survive reloads.
- **Color Persistence**: Layer colors are stored in the database, including the randomly assigned color when a layer is first created, and can be updated through the layer view.
Expand Down