Red Strings: fix dependency icon picker + e2e test for editing a dependency

The icon picker popup read its own data context (the dependency row, which has
no setDependencyProps) instead of the source card, so picking an icon did
nothing. Capture the source card when the picker opens and use it in the popup.

Adds an e2e test that edits an existing dependency's relation type, color and
icon from the card detail and asserts each persists — this is the path that
previously threw the client 403 (selector/positional-$ update) and the icon
no-op, now both fixed. Verified locally: spec 27 passes 8/8 on chromium.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lauri OjansivuandClaude Opus 4.8 committed 2026-06-15 22:29:47 +03:00
1 parent 1db64f4dde
commit e8a8007814
2 files changed
+56 -3

No files matched your search

+10 -3
View File
@@ -16,8 +16,11 @@ const DEPENDENCY_ICON_CHOICES = [
'sitemap', 'share-alt', 'code-fork', 'tasks',
];
// Which dependency (target card id) the icon picker should apply its choice to.
// Which dependency the icon picker should apply its choice to. The icon popup's
// own data context is the dependency row (not the source card), so we capture
// the source card + target id here when the picker is opened.
let editingDependencyTargetId = null;
let editingDependencyCard = null;
import {
setupDatePicker,
datePickerRendered,
@@ -926,7 +929,8 @@ Template.cardDetails.events({
},
'click .js-dependency-icon'(event) {
if (!Utils.canModifyCard()) return;
// Remember which dependency the picked icon should apply to.
// Remember the source card + which dependency the picked icon applies to.
editingDependencyCard = Template.currentData();
editingDependencyTargetId = event.currentTarget.dataset.targetId;
Popup.open('cardDependencyIcon')(event);
},
@@ -2552,12 +2556,15 @@ Template.cardDependencyIconPopup.helpers({
Template.cardDependencyIconPopup.events({
'click .js-pick-dependency-icon'(event) {
event.preventDefault();
const sourceCard = Template.currentData();
// Use the source card captured when the picker was opened (the popup's own
// data context is the dependency row, which has no setDependencyProps).
const sourceCard = editingDependencyCard;
const icon = event.currentTarget.dataset.icon || DEFAULT_DEPENDENCY_ICON;
if (sourceCard && editingDependencyTargetId) {
sourceCard.setDependencyProps(editingDependencyTargetId, { icon });
}
editingDependencyTargetId = null;
editingDependencyCard = null;
Popup.back();
},
});
@@ -225,4 +225,50 @@ test.describe('Red Strings – card dependency overlay', () => {
expect((db.getCard(alphaId).cardDependencies || []).map(d => d.cardId)).toContain(betaId);
expect((db.getCard(betaId).cardDependencies || []).map(d => d.cardId)).toContain(alphaId);
});
test('editing an existing dependency type/color/icon from card detail persists (no 403)', async ({ page }) => {
// Start with one existing dependency to edit (the path that previously threw
// "Untrusted code may only updateAsync documents by ID").
db.setCardDependencies({
cardId: alphaId,
dependsOn: [{ cardId: betaId, type: 'related-to', color: '#eb144c', icon: 'link' }],
});
await loginWithToken(page, owner.id, owner.token);
await page.goto(`${BASE_URL}/b/${board.boardId}/${board.slug}/${alphaId}`, {
waitUntil: 'commit',
});
// Remove any transient rspack dev-server overlay that could intercept input.
const dismissDevOverlay = () =>
page.evaluate(() => {
document
.querySelectorAll('iframe[id*="webpack-dev-server"]')
.forEach(el => el.remove());
});
const depOf = () =>
(db.getCard(alphaId).cardDependencies || []).find(d => d.cardId === betaId);
// Change the relation type via the per-row dropdown.
const typeSelect = page.locator(`.js-dependency-type[data-target-id="${betaId}"]`);
await typeSelect.waitFor({ timeout: 15_000 });
await dismissDevOverlay();
await typeSelect.selectOption('blocks');
await expect.poll(() => depOf() && depOf().type, { timeout: 10_000 }).toBe('blocks');
// Change the color via the per-row color input (fires a change event).
await dismissDevOverlay();
await page.locator(`.js-dependency-color[data-target-id="${betaId}"]`).evaluate(el => {
el.value = '#00ff00';
el.dispatchEvent(new Event('change', { bubbles: true }));
});
await expect.poll(() => depOf() && depOf().color, { timeout: 10_000 }).toBe('#00ff00');
// Pick a different icon from the icon popup.
await dismissDevOverlay();
await page.locator(`.js-dependency-icon[data-target-id="${betaId}"]`).click();
await page.locator('.js-pick-dependency-icon[data-icon="lock"]').click();
await expect.poll(() => depOf() && depOf().icon, { timeout: 10_000 }).toBe('lock');
});
});