fix(frontend): reflect rule-applied tags in batch edit
BulkTagEditor optimistically marked only the clicked tag as common after a bulk add, so tags applied by auto-tag rules (resolved server-side) never appeared. Refetch /files/bulk/common-tags after each change and rebuild the common/partial sets from the response, so rule-applied tags and partial->common shifts show up. Backend bulk path was already correct — covered now by TestBulkTagAutoRule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -715,6 +715,53 @@ func TestRecordFileView(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusNotFound, resp.StatusCode, resp.String())
|
require.Equal(t, http.StatusNotFound, resp.StatusCode, resp.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBulkTagAutoRule verifies the bulk add path also applies then_tags.
|
||||||
|
func TestBulkTagAutoRule(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
h := setupSuite(t)
|
||||||
|
adminToken := h.login("admin", "admin")
|
||||||
|
|
||||||
|
resp := h.doJSON("POST", "/tags", map[string]any{"name": "outdoor"}, adminToken)
|
||||||
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
|
var outdoor map[string]any
|
||||||
|
resp.decode(t, &outdoor)
|
||||||
|
outdoorID := outdoor["id"].(string)
|
||||||
|
|
||||||
|
resp = h.doJSON("POST", "/tags", map[string]any{"name": "nature"}, adminToken)
|
||||||
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
|
var nature map[string]any
|
||||||
|
resp.decode(t, &nature)
|
||||||
|
natureID := nature["id"].(string)
|
||||||
|
|
||||||
|
resp = h.doJSON("POST", "/tags/"+outdoorID+"/rules", map[string]any{"then_tag_id": natureID}, adminToken)
|
||||||
|
require.Equal(t, http.StatusCreated, resp.StatusCode, resp.String())
|
||||||
|
|
||||||
|
file := h.uploadJPEG(adminToken, "park.jpg")
|
||||||
|
fileID := file["id"].(string)
|
||||||
|
|
||||||
|
// Bulk-add only "outdoor" to the file.
|
||||||
|
resp = h.doJSON("POST", "/files/bulk/tags", map[string]any{
|
||||||
|
"file_ids": []string{fileID},
|
||||||
|
"action": "add",
|
||||||
|
"tag_ids": []string{outdoorID},
|
||||||
|
}, adminToken)
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, resp.String())
|
||||||
|
|
||||||
|
// The auto-applied "nature" should be on the file too.
|
||||||
|
resp = h.doJSON("GET", "/files/"+fileID+"/tags", nil, adminToken)
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
var tagsResp []any
|
||||||
|
resp.decode(t, &tagsResp)
|
||||||
|
names := make([]string, 0, len(tagsResp))
|
||||||
|
for _, tg := range tagsResp {
|
||||||
|
names = append(names, tg.(map[string]any)["name"].(string))
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, []string{"outdoor", "nature"}, names)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Security regression tests
|
// Security regression tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -71,14 +71,24 @@
|
|||||||
return color ? `background-color: #${color}` : '';
|
return color ? `background-color: #${color}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refetch which tags are common/partial across the selection. Run after any
|
||||||
|
// bulk change so rule-applied tags (and partial→common shifts) show up — the
|
||||||
|
// server applies auto-tag rules, so we can't infer the result locally.
|
||||||
|
async function refreshCommon() {
|
||||||
|
const res = await api.post<{ common_tag_ids: string[]; partial_tag_ids: string[] }>(
|
||||||
|
'/files/bulk/common-tags',
|
||||||
|
{ file_ids: fileIds }
|
||||||
|
);
|
||||||
|
commonIds = new Set(res.common_tag_ids ?? []);
|
||||||
|
partialIds = new Set(res.partial_tag_ids ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
async function add(tagId: string) {
|
async function add(tagId: string) {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
await api.post('/files/bulk/tags', { file_ids: fileIds, action: 'add', tag_ids: [tagId] });
|
await api.post('/files/bulk/tags', { file_ids: fileIds, action: 'add', tag_ids: [tagId] });
|
||||||
commonIds = new Set([...commonIds, tagId]);
|
await refreshCommon();
|
||||||
partialIds.delete(tagId);
|
|
||||||
partialIds = new Set(partialIds);
|
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
@@ -90,9 +100,7 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
await api.post('/files/bulk/tags', { file_ids: fileIds, action: 'add', tag_ids: [tagId] });
|
await api.post('/files/bulk/tags', { file_ids: fileIds, action: 'add', tag_ids: [tagId] });
|
||||||
commonIds = new Set([...commonIds, tagId]);
|
await refreshCommon();
|
||||||
partialIds.delete(tagId);
|
|
||||||
partialIds = new Set(partialIds);
|
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
@@ -103,10 +111,7 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
await api.post('/files/bulk/tags', { file_ids: fileIds, action: 'remove', tag_ids: [tagId] });
|
await api.post('/files/bulk/tags', { file_ids: fileIds, action: 'remove', tag_ids: [tagId] });
|
||||||
commonIds.delete(tagId);
|
await refreshCommon();
|
||||||
partialIds.delete(tagId);
|
|
||||||
commonIds = new Set(commonIds);
|
|
||||||
partialIds = new Set(partialIds);
|
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user