feat(design): the channel menu's Tags and Priority submenus (C1, E28 S3e3b)
Tags A-Z as in-place checks (the menu stays open for the next); Priority as the fixed steps 10 5 3 2 1 0, an off-scale current value ticked in its place. A pick re-sorts the list at once, moving only that row (placeByPriority, the server's order), and the table / card pager and the focus follow it (DataTable and useCardPager take a reveal seq, listNav.focusNavItem lands on the row's own "…", a virtualized card revealed first). ActionMenu rows take check: many | one. The menu reads the row fresh from the cache and keys its target by id.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EyeOff, ExternalLink, History, Tv, UserMinus } from "lucide-react";
|
||||
import { ArrowUpDown, EyeOff, ExternalLink, History, Tags, Tv, UserMinus } from "lucide-react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { api, type ManagedChannel } from "../lib/api";
|
||||
import { api, type ManagedChannel, type Tag } from "../lib/api";
|
||||
import { priorityChoices } from "../lib/channelOrder";
|
||||
import { channelYouTubeUrl } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||
@@ -14,14 +15,24 @@ import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
// A subscribed channel's ONE action list (E28 C1): the manager row's "…", its right-click, long-press
|
||||
// and Shift+F10, in the table and in the card layout alike. The row keeps what the reader looks at
|
||||
// or tunes all the time (priority, tags, the stored count's ✓); everything that was a button on the
|
||||
// row — full history, hide, unsubscribe — is here, beside open, the pin and the copies.
|
||||
// row — full history, hide, unsubscribe — is here, beside open, the pin and the copies. Tags and
|
||||
// priority are here TOO, as submenus (owner, 2026-09-26): tagging a run of channels, or bubbling one
|
||||
// up the list, from the menu the hand is already in.
|
||||
|
||||
export function useChannelActions({
|
||||
onView,
|
||||
onOpenWizard,
|
||||
tags,
|
||||
onToggleTag,
|
||||
onPickPriority,
|
||||
}: {
|
||||
onView: (c: ManagedChannel) => void;
|
||||
onOpenWizard: () => void;
|
||||
/** The reader's own tags (no system tags) — the Tags submenu lists them A–Z. */
|
||||
tags: readonly Tag[];
|
||||
onToggleTag: (c: ManagedChannel, tagId: number) => void;
|
||||
/** A priority step picked: the caller re-sorts its list and follows the row. */
|
||||
onPickPriority: (c: ManagedChannel, priority: number) => void;
|
||||
}) {
|
||||
const qc = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
@@ -130,6 +141,47 @@ export function useChannelActions({
|
||||
};
|
||||
}
|
||||
|
||||
/** Tags A–Z, one check each, toggled in place (the menu stays open for the next one). With no tag
|
||||
* yet the row stays, off, and says where tags are made. */
|
||||
function tagsItem(c: ManagedChannel): ActionMenuItem {
|
||||
const base = {
|
||||
id: "tags",
|
||||
label: t("channels.tags.label"),
|
||||
icon: <Tags className="w-4 h-4" />,
|
||||
};
|
||||
if (tags.length === 0)
|
||||
return { ...base, disabled: true, note: t("channels.row.noTagsYet"), onSelect: () => {} };
|
||||
const on = new Set(c.tag_ids);
|
||||
return {
|
||||
...base,
|
||||
items: [...tags]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((tg) => ({
|
||||
id: `tag-${tg.id}`,
|
||||
label: tg.name,
|
||||
check: { kind: "many" as const, checked: on.has(tg.id) },
|
||||
onSelect: () => onToggleTag(c, tg.id),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** The fixed priority steps, the current one ticked; a pick re-sorts the list (the caller). */
|
||||
function priorityItem(c: ManagedChannel): ActionMenuItem {
|
||||
return {
|
||||
id: "priority",
|
||||
label: t("channels.row.priority"),
|
||||
icon: <ArrowUpDown className="w-4 h-4" />,
|
||||
items: priorityChoices(c.priority).map((p) => ({
|
||||
id: `priority-${p}`,
|
||||
label: String(p),
|
||||
check: { kind: "one" as const, checked: p === c.priority },
|
||||
onSelect: () => {
|
||||
if (p !== c.priority) onPickPriority(c, p);
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** The channel's menu rows; `pin` is the caller's comfort-shelf row (its "is it pinned?" read
|
||||
* follows the caller's own menu). */
|
||||
function items(c: ManagedChannel, { pin }: { pin: ActionMenuItem[] }): ActionMenuItem[] {
|
||||
@@ -142,6 +194,8 @@ export function useChannelActions({
|
||||
onSelect: () => onView(c),
|
||||
},
|
||||
...pin,
|
||||
{ ...tagsItem(c), separatorBefore: true },
|
||||
priorityItem(c),
|
||||
...[
|
||||
{
|
||||
id: "open-youtube",
|
||||
|
||||
Reference in New Issue
Block a user