fix: tag counts, chip focus, upload naming, share links and unsized clips hold after round 2
- A tag counts the followed channels the Subscriptions tab shows: hidden ones are left out, and a subscribe, unsubscribe, hide or tag toggle refreshes the count. The Tag manager's row and its hover list count the same channels as the chip; /api/tags sends both counts under their names. - A chip's count keeps the chip's colour after a dot (a dimmed count fell under 4.5:1). A pick in the open "+N" fold closes it; a chip that leaves the list hands the focus to the one in its place. The fold is a dialog of toggle buttons, not an empty menu. - Upload: a folder name left by a press is made once the press is over, if the dialog is still open, instead of guessing a dismissal from Modal's markup. While the folder is made, its place holds the focus and a live region says so. - Share: an expired link has no Copy and a warn badge; the link made on open goes into the cached list, so a reopen makes no second one; the New link form survives the link made on open. - Watch: a repeated Enter sends one unlock while one is on its way. See all buttons are named with their rail. - An unsized clip of a rotated file gets the upright size its crop cut.
This commit is contained in:
@@ -10,7 +10,8 @@ export interface Tag {
|
||||
/** Every channel link it has, a channel the user no longer follows included (an unsubscribe keeps
|
||||
* the link) — what deleting the tag removes. */
|
||||
channel_count: number;
|
||||
/** The user's own links on channels they follow: the rows the Channels page's filter shows. */
|
||||
/** The user's own links on channels they follow and have not hidden: the rows the Channels page's
|
||||
* filter shows (a hidden channel has its own tab). */
|
||||
subscribed_count: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { api, HttpError } from "./api";
|
||||
import { msg, notify } from "./notifications";
|
||||
import { useChannelTagActions } from "./useChannelTagActions";
|
||||
import { useChannelPriority } from "./useChannelPriority";
|
||||
import { qk } from "./queryKeys";
|
||||
|
||||
const notifyMock = vi.mocked(notify);
|
||||
beforeEach(() => notifyMock.mockClear());
|
||||
@@ -44,6 +45,23 @@ describe("useChannelTagActions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// A tag counts the followed channels it is on: a toggle that lands changes that number.
|
||||
it("refreshes the tags once a toggle lands", async () => {
|
||||
vi.spyOn(api, "attachChannelTag").mockResolvedValue({ ok: true } as never);
|
||||
const qc = new QueryClient();
|
||||
const spy = vi.spyOn(qc, "invalidateQueries");
|
||||
const { result } = renderHook(
|
||||
() => useChannelTagActions({ optimisticToggle: () => {}, onError: () => {} }),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
act(() => result.current.toggleTag("UC1", 7, false));
|
||||
await waitFor(() => expect(spy.mock.calls.map(([f]) => f?.queryKey)).toContainEqual(qk.tags()));
|
||||
});
|
||||
|
||||
it("names a refused create-and-attach", async () => {
|
||||
vi.spyOn(api, "createTag").mockRejectedValue(new HttpError(429, "slow"));
|
||||
const { result } = renderHook(
|
||||
|
||||
@@ -21,9 +21,23 @@ describe("a subscription change", () => {
|
||||
qk.feedSummary(),
|
||||
qk.discoveredChannels(),
|
||||
qk.watchedChannels(),
|
||||
qk.tags(),
|
||||
]);
|
||||
});
|
||||
|
||||
// A tag counts the followed channels it is on, not hidden: a hide changes the number as an
|
||||
// unsubscribe does, and both places that hide a channel must refresh it.
|
||||
it("a hide refreshes the tags wherever it is made", () => {
|
||||
const root = new URL("../", import.meta.url);
|
||||
const sites = readdirSync(root, { recursive: true, encoding: "utf8" })
|
||||
.map((f) => f.split("\\").join("/"))
|
||||
.filter((f) => f.endsWith(".tsx") && !f.includes(".test.") && !f.startsWith("design-lab/"))
|
||||
.map((f) => [f, readFileSync(new URL(f, root), "utf8")] as const)
|
||||
.filter(([, src]) => /api\.updateChannel\([^)]*hidden/.test(src));
|
||||
expect(sites.length).toBeGreaterThanOrEqual(2);
|
||||
for (const [f, src] of sites) expect(src, f).toContain("queryKey: qk.tags()");
|
||||
});
|
||||
|
||||
it("is refreshed through that one function wherever it is made", () => {
|
||||
const root = new URL("../", import.meta.url);
|
||||
const sites = readdirSync(root, { recursive: true, encoding: "utf8" })
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "./queryKeys";
|
||||
|
||||
/** Everything a subscribe or an unsubscribe changes: the channel itself, the manager's list and its
|
||||
* counts, the feed and its facets (the channel's videos come or go), and the two discovery lists (a
|
||||
* subscribed channel leaves them). ONE set for every site that makes the change: each used to keep
|
||||
* counts, the feed and its facets (the channel's videos come or go), the two discovery lists (a
|
||||
* subscribed channel leaves them), and the tags (a tag counts the followed channels it is on). ONE set for every site that makes the change: each used to keep
|
||||
* its own list, and each list missed something. */
|
||||
export function invalidateSubscription(qc: QueryClient, channelId: string): void {
|
||||
for (const queryKey of [
|
||||
@@ -15,6 +15,7 @@ export function invalidateSubscription(qc: QueryClient, channelId: string): void
|
||||
qk.feedSummary(),
|
||||
qk.discoveredChannels(),
|
||||
qk.watchedChannels(),
|
||||
qk.tags(),
|
||||
])
|
||||
void qc.invalidateQueries({ queryKey });
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ export function useChannelTagActions(opts: {
|
||||
? api.detachChannelTag(channelId, tagId)
|
||||
: api.attachChannelTag(channelId, tagId);
|
||||
call
|
||||
.then(() => opts.onSuccess?.(channelId))
|
||||
.then(() => {
|
||||
// A tag counts the followed channels it is on: the toggle changed that number.
|
||||
void qc.invalidateQueries({ queryKey: qk.tags() });
|
||||
opts.onSuccess?.(channelId);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
notifyActionError(err, msg("channels.tags.updateFailed"));
|
||||
opts.onError(channelId);
|
||||
|
||||
Reference in New Issue
Block a user