fix(modal,tags): five UX follow-ups
1. Hide 'archive' kind from /tags default view. Archive tags only appear when the user explicitly requests them via ?kind=archive. _get_tags_with_previews gains a default_excluded_kinds tuple that applies when no explicit kind filter is set. 2. Hide the 'Add' submit button on desktop (>=768px). Enter in the tag input (and in the fandom picker when no row is highlighted) already submits; the button is only needed for mobile virtual keyboards where Enter-to-submit is unreliable. 3. Fandom picker keyboard parity: ArrowUp/Down navigate rows, Enter picks a highlighted row or (if no row is highlighted) submits the whole add-tag form. Escape clears the highlight. Mirrors the main tagInput's keyboard block. 4. Tag editor merge bug: the edit modal populated nameInput.value with data.tag.display_name — for a character with a fandom, that value is 'Name (Fandom)'. Saving without stripping the suffix routed to a rename that never collided with the canonical bare name, so the merge offer never fired. Now uses data.tag.name (the bare form). The (Fandom) display is still shown in the fandom input which is rendered separately. 5. Fandom picker styling: match the main tag-form input — same padding, border, background, focus state. Drop the separate 'Fandom' label; the placeholder 'Fandom (optional)' now carries the label role. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+10
-1
@@ -440,7 +440,8 @@ def gallery_jump():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, search=None, null_fandom_only=False):
|
def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, search=None, null_fandom_only=False,
|
||||||
|
default_excluded_kinds=('archive',)):
|
||||||
"""
|
"""
|
||||||
Helper function to get tags with counts and preview images.
|
Helper function to get tags with counts and preview images.
|
||||||
Returns (tag_data list, total_count, has_more).
|
Returns (tag_data list, total_count, has_more).
|
||||||
@@ -453,6 +454,11 @@ def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, sea
|
|||||||
count_q = db.session.query(func.count(Tag.id))
|
count_q = db.session.query(func.count(Tag.id))
|
||||||
if kind:
|
if kind:
|
||||||
count_q = count_q.filter(Tag.kind == kind)
|
count_q = count_q.filter(Tag.kind == kind)
|
||||||
|
else:
|
||||||
|
# Hide noisy auto-generated kinds (archive, etc.) from the default view;
|
||||||
|
# users can still reach them via an explicit ?kind=archive filter.
|
||||||
|
if default_excluded_kinds:
|
||||||
|
count_q = count_q.filter(~Tag.kind.in_(default_excluded_kinds))
|
||||||
if search:
|
if search:
|
||||||
count_q = count_q.filter(Tag.name.ilike(f'%{search}%'))
|
count_q = count_q.filter(Tag.name.ilike(f'%{search}%'))
|
||||||
if null_fandom_only:
|
if null_fandom_only:
|
||||||
@@ -467,6 +473,9 @@ def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, sea
|
|||||||
|
|
||||||
if kind:
|
if kind:
|
||||||
q = q.filter(Tag.kind == kind)
|
q = q.filter(Tag.kind == kind)
|
||||||
|
else:
|
||||||
|
if default_excluded_kinds:
|
||||||
|
q = q.filter(~Tag.kind.in_(default_excluded_kinds))
|
||||||
if search:
|
if search:
|
||||||
q = q.filter(Tag.name.ilike(f'%{search}%'))
|
q = q.filter(Tag.name.ilike(f'%{search}%'))
|
||||||
if null_fandom_only:
|
if null_fandom_only:
|
||||||
|
|||||||
@@ -118,7 +118,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
tagIdInput.value = data.tag.id;
|
tagIdInput.value = data.tag.id;
|
||||||
nameInput.value = data.tag.display_name;
|
// Use the bare name, not display_name — display_name for a character
|
||||||
|
// includes the "(Fandom)" suffix, which would make the name input's
|
||||||
|
// value a concatenation of the bare name and the fandom string.
|
||||||
|
// Saving that as-is would skip the rename-collision merge path and
|
||||||
|
// persist a malformed tag name.
|
||||||
|
nameInput.value = data.tag.name;
|
||||||
kindSelect.value = data.tag.kind || 'user';
|
kindSelect.value = data.tag.kind || 'user';
|
||||||
|
|
||||||
// Set fandom fields
|
// Set fandom fields
|
||||||
|
|||||||
@@ -879,12 +879,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fandomPickerSelectedIdx = -1;
|
||||||
|
|
||||||
|
function highlightFandomItem(idx) {
|
||||||
|
if (!fandomPickerAutocomplete) return;
|
||||||
|
const items = fandomPickerAutocomplete.querySelectorAll('.tag-autocomplete-item');
|
||||||
|
items.forEach((el, i) => el.classList.toggle('selected', i === idx));
|
||||||
|
fandomPickerSelectedIdx = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFandomRow(row) {
|
||||||
|
if (!row || !fandomPickerInput) return;
|
||||||
|
fandomPickerInput.value = row.dataset.fandomName;
|
||||||
|
fandomPickerInput.dataset.fandomId = row.dataset.fandomId;
|
||||||
|
clearFandomAutocomplete();
|
||||||
|
fandomPickerSelectedIdx = -1;
|
||||||
|
}
|
||||||
|
|
||||||
if (fandomPickerInput) {
|
if (fandomPickerInput) {
|
||||||
fandomPickerInput.addEventListener('input', () => {
|
fandomPickerInput.addEventListener('input', () => {
|
||||||
clearTimeout(fandomPickerDebounce);
|
clearTimeout(fandomPickerDebounce);
|
||||||
const term = fandomPickerInput.value.trim();
|
const term = fandomPickerInput.value.trim();
|
||||||
// Any typing invalidates a previously-selected fandom id.
|
// Any typing invalidates a previously-selected fandom id + highlight.
|
||||||
delete fandomPickerInput.dataset.fandomId;
|
delete fandomPickerInput.dataset.fandomId;
|
||||||
|
fandomPickerSelectedIdx = -1;
|
||||||
if (!term) {
|
if (!term) {
|
||||||
clearFandomAutocomplete();
|
clearFandomAutocomplete();
|
||||||
return;
|
return;
|
||||||
@@ -896,6 +914,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
.catch(() => clearFandomAutocomplete());
|
.catch(() => clearFandomAutocomplete());
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keyboard parity with the main tagInput: ArrowDown/Up to navigate rows,
|
||||||
|
// Enter to pick a highlighted row (or submit the whole form when no row
|
||||||
|
// is highlighted), Escape to clear the highlight.
|
||||||
|
fandomPickerInput.addEventListener('keydown', (e) => {
|
||||||
|
const rows = fandomPickerAutocomplete
|
||||||
|
? fandomPickerAutocomplete.querySelectorAll('.tag-autocomplete-item')
|
||||||
|
: [];
|
||||||
|
const count = rows.length;
|
||||||
|
if (e.key === 'ArrowDown' && count > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightFandomItem(Math.min(fandomPickerSelectedIdx + 1, count - 1));
|
||||||
|
} else if (e.key === 'ArrowUp' && count > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightFandomItem(Math.max(fandomPickerSelectedIdx - 1, 0));
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
if (fandomPickerSelectedIdx >= 0 && rows[fandomPickerSelectedIdx]) {
|
||||||
|
e.preventDefault();
|
||||||
|
pickFandomRow(rows[fandomPickerSelectedIdx]);
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
submitTag();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
highlightFandomItem(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fandomPickerAutocomplete) {
|
if (fandomPickerAutocomplete) {
|
||||||
@@ -903,11 +948,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const row = e.target.closest('[data-fandom-id]');
|
const row = e.target.closest('[data-fandom-id]');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
e.preventDefault(); // keep focus on the input
|
e.preventDefault(); // keep focus on the input
|
||||||
if (fandomPickerInput) {
|
pickFandomRow(row);
|
||||||
fandomPickerInput.value = row.dataset.fandomName;
|
|
||||||
fandomPickerInput.dataset.fandomId = row.dataset.fandomId;
|
|
||||||
}
|
|
||||||
clearFandomAutocomplete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-12
@@ -3046,24 +3046,39 @@ body.select-mode .gallery-infinite-container {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.fandom-picker-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--muted, #888);
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
.fandom-picker-input-row {
|
.fandom-picker-input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
/* Match the main .tag-form input styling so the picker feels like the
|
||||||
|
same control as the add-tag field above it. */
|
||||||
.fandom-picker-input {
|
.fandom-picker-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.4rem 0.6rem;
|
padding: 0.5rem 0.75rem;
|
||||||
background: var(--input-bg, #111);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
border: 1px solid var(--input-border, #333);
|
border-radius: 6px;
|
||||||
border-radius: 0.25rem;
|
background: rgba(255, 255, 255, 0.08);
|
||||||
color: var(--fg, #eee);
|
color: var(--text);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.fandom-picker-input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.fandom-picker-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--btn-primary);
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop: the form's submit button is redundant — Enter on the tag input
|
||||||
|
(and Enter in the fandom picker when no row is highlighted) already
|
||||||
|
submits. Keep the button visible on mobile where virtual keyboards make
|
||||||
|
Enter-to-submit less reliable. */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.tag-form > button[type="submit"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ambiguous-candidate picker (Task 24) */
|
/* Ambiguous-candidate picker (Task 24) */
|
||||||
|
|||||||
@@ -62,10 +62,9 @@
|
|||||||
<button type="submit">Add</button>
|
<button type="submit">Add</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="fandomPickerWrapper" class="fandom-picker-wrapper" aria-hidden="true">
|
<div id="fandomPickerWrapper" class="fandom-picker-wrapper" aria-hidden="true">
|
||||||
<label class="fandom-picker-label" for="fandomPickerInput">Fandom</label>
|
|
||||||
<div class="fandom-picker-input-row">
|
<div class="fandom-picker-input-row">
|
||||||
<input type="text" id="fandomPickerInput" class="fandom-picker-input"
|
<input type="text" id="fandomPickerInput" class="fandom-picker-input"
|
||||||
placeholder="Pick or create fandom (optional)" autocomplete="off">
|
placeholder="Fandom (optional)" autocomplete="off">
|
||||||
<button type="button" id="fandomPickerClear" class="btn-small"
|
<button type="button" id="fandomPickerClear" class="btn-small"
|
||||||
aria-label="Clear fandom" title="Clear fandom">×</button>
|
aria-label="Clear fandom" title="Clear fandom">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user