diff --git a/convex/featuredPublicOrder.runtime.test.ts b/convex/featuredPublicOrder.runtime.test.ts new file mode 100644 index 0000000000..4f9d2b73d2 --- /dev/null +++ b/convex/featuredPublicOrder.runtime.test.ts @@ -0,0 +1,165 @@ +/// +/* @vitest-environment edge-runtime */ +import { convexTest } from "convex-test"; +import { expect, it } from "vitest"; +import { api } from "./_generated/api"; +import { syncPackageSearchDigestForPackageId } from "./functions"; +import schema from "./schema"; + +const modules = import.meta.glob("./**/*.ts"); +it("public Featured reads follow approved identity order and omit unbadged reservations", async () => { + const t = convexTest(schema, modules); + const data = await t.run(async (ctx) => { + const ownerUserId = await ctx.db.insert("users", { handle: "curator", role: "moderator" }); + const publisher = await ctx.db.insert("publishers", { + kind: "user", + displayName: "Curator", + handle: "curator", + linkedUserId: ownerUserId, + createdAt: 1, + updatedAt: 1, + }); + await ctx.db.patch(ownerUserId, { personalPublisherId: publisher }); + const items = []; + for (const name of ["telemetry", "editorial", "manual"]) { + const packageId = await ctx.db.insert("packages", { + name, + normalizedName: name, + displayName: name, + family: "code-plugin", + ownerUserId, + ownerPublisherId: publisher, + channel: "community", + isOfficial: false, + categories: ["developer-tools"], + tags: {}, + stats: { downloads: 1, installs: 1, stars: 0, versions: 1 }, + createdAt: 1, + updatedAt: 1, + }); + const skillId = await ctx.db.insert("skills", { + slug: name, + displayName: name, + ownerUserId, + ownerPublisherId: publisher, + moderationStatus: "active", + tags: {}, + badges: {}, + stats: { comments: 0, downloads: 1, stars: 0, versions: 1 }, + createdAt: 1, + updatedAt: 1, + }); + const storageId = await ctx.storage.store(new Blob(["x"])); + const file = { path: "SKILL.md", size: 1, storageId, sha256: "a".repeat(64) }; + const releaseId = await ctx.db.insert("packageReleases", { + packageId, + version: "1.0.0", + changelog: "Fixture", + distTags: ["latest"], + files: [{ ...file, path: "index.js" }], + integritySha256: "a".repeat(64), + verification: { tier: "structural", scope: "artifact-only", scanStatus: "clean" }, + createdBy: ownerUserId, + createdAt: 1, + }); + await ctx.db.patch(packageId, { + latestReleaseId: releaseId, + scanStatus: "clean", + tags: { latest: releaseId }, + }); + await syncPackageSearchDigestForPackageId(ctx, packageId); + const versionId = await ctx.db.insert("skillVersions", { + skillId, + version: "1.0.0", + changelog: "Fixture", + files: [file], + parsed: { frontmatter: {} }, + createdBy: ownerUserId, + createdAt: 1, + llmAnalysis: { status: "clean", checkedAt: 1 }, + }); + await ctx.db.patch(skillId, { latestVersionId: versionId, tags: { latest: versionId } }); + items.push({ name, packageId, skillId }); + } + return { ownerUserId, items }; + }); + const staff = t.withIdentity({ subject: `${data.ownerUserId}|session` }); + for (const item of data.items) { + await staff.mutation(api.packages.setBatch, { + packageId: item.packageId, + batch: "highlighted", + }); + await staff.mutation(api.skills.setBatch, { skillId: item.skillId, batch: "highlighted" }); + } + await t.run(async (ctx) => { + for (const artifactKind of ["plugin", "skill"] as const) { + await ctx.db.insert("featuredSelections", { + artifactKind, + revision: 1, + editorial: + artifactKind === "plugin" + ? [ + { + id: "plugin:pending", + name: "pending", + displayName: "Pending", + reason: "Awaiting publication", + }, + ] + : [], + updatedAt: 1, + updatedBy: data.ownerUserId, + published: { + reportId: "retained-report-fixture", + reportHash: "retained-evidence-hash", + at: 1, + byUserId: data.ownerUserId, + periodStart: 0, + periodEnd: 1, + items: [data.items[1], data.items[0]].map((item) => ({ + id: artifactKind === "plugin" ? `plugin:${item.name}` : `clawhub:${item.skillId}`, + version: "1.0.0", + selectionBasis: "telemetry" as const, + reason: "Reviewed", + })), + }, + }); + } + }); + const expected = ["editorial", "telemetry", "manual"]; + const plugins = await t.query(api.packages.listPublicPage, { + highlightedOnly: true, + paginationOpts: { cursor: null, numItems: 16 }, + }); + expect(plugins.page.map((item) => item.name)).toEqual(expected); + const skills = await t.query(api.skills.listPublicPageV4, { + highlightedOnly: true, + numItems: 16, + }); + expect(skills.page.map((item) => item.skill.slug)).toEqual(expected); + expect( + (await t.query(api.skills.listHighlightedPublic, { limit: 16 })).map((item) => item.skill.slug), + ).toEqual(expected); + expect( + (await t.query(api.skills.listWithLatest, { batch: "highlighted", limit: 16 })).map( + (item) => item.skill.slug, + ), + ).toEqual(expected); + const skillPackages = await t.query(api.skills.listPackageCatalogPage, { + highlightedOnly: true, + paginationOpts: { cursor: null, numItems: 16 }, + }); + expect(skillPackages.page.map((item) => item.name)).toEqual(expected); + let cursor: string | null = null; + const paginated: string[] = []; + for (let index = 0; index < expected.length; index++) { + const page: typeof skillPackages = await t.query(api.skills.listPackageCatalogPage, { + highlightedOnly: true, + paginationOpts: { cursor, numItems: 1 }, + }); + paginated.push(...page.page.map((item) => item.name)); + expect(page.isDone).toBe(index === expected.length - 1); + cursor = page.continueCursor; + } + expect(paginated).toEqual(expected); +}); diff --git a/convex/lib/featuredSelections.ts b/convex/lib/featuredSelections.ts index e4b2d76426..e234f14b25 100644 --- a/convex/lib/featuredSelections.ts +++ b/convex/lib/featuredSelections.ts @@ -62,3 +62,17 @@ export async function readPublishedFeaturedOrder( .unique(); return selection?.published?.items.map((item) => item.id) ?? []; } + +// The approved snapshot owns order; legacy/manual badges absent from it retain +// their existing stable order after the published selection. +export function orderPublishedFeatured( + items: readonly T[], + publishedIds: readonly string[], + identity: (item: T) => string, +): T[] { + const ranks = new Map(publishedIds.map((id, index) => [id, index])); + return [...items].sort( + (left, right) => + (ranks.get(identity(left)) ?? Infinity) - (ranks.get(identity(right)) ?? Infinity), + ); +} diff --git a/convex/packages.public.test.ts b/convex/packages.public.test.ts index bc3d5a780a..abe6577d20 100644 --- a/convex/packages.public.test.ts +++ b/convex/packages.public.test.ts @@ -1626,6 +1626,8 @@ function makeDigestCtx(options: { return null; }), query: vi.fn((table: string) => { + if (table === "featuredSelections") + return { withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })) }; if (table === "packageBadges") { return { withIndex: vi.fn(() => ({ diff --git a/convex/packages.ts b/convex/packages.ts index dac4f24b99..4e917fd1fe 100644 --- a/convex/packages.ts +++ b/convex/packages.ts @@ -70,6 +70,7 @@ import { } from "./lib/emails"; import { experimentalClawsEnabled, isClawFamilyPubliclyVisible } from "./lib/experimentalClaws"; import { assertFeaturedCapacity } from "./lib/featuredPolicy"; +import { orderPublishedFeatured, readPublishedFeaturedOrder } from "./lib/featuredSelections"; import { requireGitHubAccountAge } from "./lib/githubAccount"; import { normalizeGitHubRepository } from "./lib/githubActionsOidc"; import { readGlobalPublicPluginsCount } from "./lib/globalStats"; @@ -2872,14 +2873,17 @@ async function fetchHighlightedPackagePage( numItems: number; }, ) { - const entries = await fetchHighlightedPackageEntries(ctx, args); + const entries = orderPublishedFeatured( + await fetchHighlightedPackageEntries(ctx, args), + await readPublishedFeaturedOrder(ctx, "plugin"), + ({ digest }) => `plugin:${digest.name}`, + ); const items = await Promise.all( entries.map( async ({ digest, featuredAt }) => await toPublicPackageListItem(ctx, digest, featuredAt), ), ); - // fetchHighlightedPackageEntries follows the badge timestamp index newest-first. - // Preserve that editorial order instead of re-ranking Featured by popularity. + // The published selection supplies order independently from badge timestamps. if (!args.officialFirst) { return items.slice(0, args.numItems); } diff --git a/convex/skills.listPublicPageV4.test.ts b/convex/skills.listPublicPageV4.test.ts index c2b12170cf..5a728f87a8 100644 --- a/convex/skills.listPublicPageV4.test.ts +++ b/convex/skills.listPublicPageV4.test.ts @@ -105,7 +105,7 @@ describe("skills.listPublicPageV4", () => { }); }); - it("keeps highlighted results in newest-featured order", async () => { + it("keeps legacy highlighted results in newest-featured order without a published snapshot", async () => { const result = await listPublicPageV4Handler( makeHighlightedCtx([ makeDigest({ @@ -220,6 +220,9 @@ function makeHighlightedCtx(digests: Array>) { return { db: { query: vi.fn((table: string) => { + if (table === "featuredSelections") { + return { withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })) }; + } if (table === "skillBadges") { return { withIndex: vi.fn((_indexName: string, build: (q: EqBuilder) => unknown) => { diff --git a/convex/skills.ts b/convex/skills.ts index 16ac1b0259..47b0e5c49f 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -53,6 +53,7 @@ import { } from "./lib/downloadTrend"; import { embeddingVisibilityFor } from "./lib/embeddingVisibility"; import { assertFeaturedCapacity } from "./lib/featuredPolicy"; +import { orderPublishedFeatured, readPublishedFeaturedOrder } from "./lib/featuredSelections"; import { canHealSkillOwnershipByGitHubProviderAccountId, getGitHubProviderAccountId, @@ -2366,8 +2367,13 @@ async function loadHighlightedSkills(ctx: QueryCtx, limit: number) { .order("desc") .take(MAX_LIST_TAKE); + const ordered = orderPublishedFeatured( + entries, + await readPublishedFeaturedOrder(ctx, "skill"), + (badge) => `clawhub:${badge.skillId}`, + ); const skills: Doc<"skills">[] = []; - for (const badge of entries) { + for (const badge of ordered) { const skill = await ctx.db.get(badge.skillId); if (!skill || skill.softDeletedAt) continue; skills.push(skill); @@ -3938,13 +3944,7 @@ export const listWithLatest = query({ entries.filter((skill) => !skill.softDeletedAt), ); const withBadges = await attachBadgesToSkills(ctx, filtered); - const ordered = - args.batch === "highlighted" - ? [...withBadges].sort( - (a, b) => (b.badges?.highlighted?.at ?? 0) - (a.badges?.highlighted?.at ?? 0), - ) - : withBadges; - const limited = ordered.slice(0, limit); + const limited = withBadges.slice(0, limit); const items = await Promise.all( limited.map(async (skill) => { const latestVersion = await loadPublicLatestVersionForSkill(ctx, skill); @@ -6714,6 +6714,32 @@ export const listPackageCatalogPage = query({ if (args.topic !== undefined && !topic) { return { page: [], isDone: true, continueCursor: "" }; } + if (args.highlightedOnly) { + const skills = await loadHighlightedSkills(ctx, MAX_LIST_TAKE); + const page: PublicSkillCatalogItem[] = []; + for (const skill of skills) { + const digest = await ctx.db + .query("skillSearchDigest") + .withIndex("by_skill", (q) => q.eq("skillId", skill._id)) + .unique(); + if (!digest || !skillCatalogMatchesFilters(digest, { ...args, topic })) continue; + const item = await toPublicSkillCatalogItem(ctx, digest); + if (item) page.push(item); + } + const { offset } = decodeSkillCatalogCursor(args.paginationOpts.cursor); + const end = offset + args.paginationOpts.numItems; + const isDone = end >= page.length; + return { + page: page.slice(offset, end), + isDone, + continueCursor: encodeSkillCatalogCursor({ + cursor: null, + offset: isDone ? 0 : end, + pageSize: null, + done: isDone, + }), + }; + } if (topic) { return await listSkillPackageCatalogTopicPage(ctx, { ...args, @@ -7481,7 +7507,7 @@ async function listOfficialFirstSkillCategoryPage( }; } -/** Fetch highlighted skills newest-first via the skillBadges timestamp index. */ +/** Resolve current highlighted membership in its approved publication order. */ async function fetchHighlightedPage( ctx: QueryCtx, opts: { @@ -7524,7 +7550,11 @@ async function fetchHighlightedPage( digests.push(digest); } - const trimmed = digests.slice(0, opts.numItems); + const trimmed = orderPublishedFeatured( + digests, + await readPublishedFeaturedOrder(ctx, "skill"), + (digest) => `clawhub:${digest.skillId}`, + ).slice(0, opts.numItems); const items: PublicSkillEntry[] = []; for (const digest of trimmed) { diff --git a/convex/skills.versions.public.test.ts b/convex/skills.versions.public.test.ts index 7f282462f4..37cf1a09f6 100644 --- a/convex/skills.versions.public.test.ts +++ b/convex/skills.versions.public.test.ts @@ -957,7 +957,7 @@ describe("public skill version queries", () => { const ctx = { db: { query: vi.fn((table: string) => { - if (table === "officialPublishers") { + if (table === "officialPublishers" || table === "featuredSelections") { return { withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null), diff --git a/src/__tests__/home-listing-section.claw591.test.tsx b/src/__tests__/home-listing-section.claw591.test.tsx index bfa1832161..90687cb200 100644 --- a/src/__tests__/home-listing-section.claw591.test.tsx +++ b/src/__tests__/home-listing-section.claw591.test.tsx @@ -385,7 +385,7 @@ describe("HomeListingSection", () => { await waitFor(() => { expect(convexQueryMock).toHaveBeenCalledWith( "skills:listPublicPageV4", - expect.objectContaining({ highlightedOnly: true, numItems: 40 }), + expect.objectContaining({ highlightedOnly: true, numItems: 16 }), ); }); diff --git a/src/lib/homeListingData.claw591.test.ts b/src/lib/homeListingData.claw591.test.ts index 9c308d500a..d93bbae85d 100644 --- a/src/lib/homeListingData.claw591.test.ts +++ b/src/lib/homeListingData.claw591.test.ts @@ -219,7 +219,7 @@ describe("homeListingData", () => { ]); expect(convexQueryMock).toHaveBeenCalledWith( "skills:listPublicPageV4", - expect.objectContaining({ highlightedOnly: true, numItems: 40 }), + expect.objectContaining({ highlightedOnly: true, numItems: 16 }), ); }); @@ -260,11 +260,11 @@ describe("homeListingData", () => { ); expect(fetchPluginCatalogMock).toHaveBeenNthCalledWith( 1, - expect.objectContaining({ featured: true, isOfficial: undefined }), + expect.objectContaining({ featured: true, limit: 16 }), ); expect(fetchPluginCatalogMock).toHaveBeenNthCalledWith( 2, - expect.objectContaining({ featured: undefined, isOfficial: true }), + expect.objectContaining({ isOfficial: true }), ); }); diff --git a/src/lib/homeListingData.test.ts b/src/lib/homeListingData.test.ts index 47fb67e3a5..fc79122e0f 100644 --- a/src/lib/homeListingData.test.ts +++ b/src/lib/homeListingData.test.ts @@ -76,111 +76,73 @@ describe("homeListingData", () => { "skills:listPublicPageV4", expect.objectContaining({ highlightedOnly: true, - numItems: 40, - sort: "updated", + numItems: 16, }), ); }); - it("sorts filtered Featured skills newest-first by featuredAt", async () => { - convexQueryMock - .mockResolvedValueOnce({ - page: [ - { - skill: { - _id: "skills:older", - slug: "older", - displayName: "Older Featured", - categories: ["development"], - badges: { highlighted: { at: 100 } }, - stats: { downloads: 10_000 }, - }, + it("preserves the published Featured skill order while filtering across categories", async () => { + convexQueryMock.mockResolvedValue({ + page: [ + { + skill: { + _id: "skills:editorial", + slug: "editorial", + categories: ["development"], + badges: { highlighted: { at: 100 } }, + stats: { downloads: 1 }, }, - ], - hasMore: false, - nextCursor: null, - }) - .mockResolvedValueOnce({ - page: [ - { - skill: { - _id: "skills:newest", - slug: "newest", - displayName: "Newest Featured", - categories: ["integrations"], - badges: { highlighted: { at: 200 } }, - stats: { downloads: 1 }, - }, + }, + { + skill: { + _id: "skills:excluded", + slug: "excluded", + categories: ["writing"], + badges: { highlighted: { at: 500 } }, + stats: { downloads: 1000 }, }, - ], - hasMore: false, - nextCursor: null, - }); - + }, + { + skill: { + _id: "skills:telemetry", + slug: "telemetry", + categories: ["integrations"], + badges: { highlighted: { at: 200 } }, + stats: { downloads: 10000 }, + }, + }, + ], + hasMore: false, + nextCursor: null, + }); const result = await fetchHomeSkillListing( "featured", ["development", "integrations"], HOME_LISTING_PAGE_SIZE, ); - expect( result.page.map((entry) => ("skill" in entry ? entry.skill.slug : entry.trending.slug)), - ).toEqual(["newest", "older"]); - expect(convexQueryMock).toHaveBeenCalledTimes(2); - expect(convexQueryMock).toHaveBeenNthCalledWith( - 1, - "skills:listPublicPageV4", - expect.objectContaining({ categorySlug: "development" }), - ); - expect(convexQueryMock).toHaveBeenNthCalledWith( - 2, - "skills:listPublicPageV4", - expect.objectContaining({ categorySlug: "integrations" }), - ); + ).toEqual(["editorial", "telemetry"]); + expect(result.hasMore).toBe(false); + expect(convexQueryMock).toHaveBeenCalledTimes(1); }); - it("sorts filtered Featured plugins newest-first by featuredAt", async () => { - fetchPluginCatalogMock - .mockResolvedValueOnce({ - items: [ - { - ...featuredPlugin, - name: "older", - categories: ["tools"], - featuredAt: 100, - stats: { downloads: 10_000 }, - }, - ], - nextCursor: null, - }) - .mockResolvedValueOnce({ - items: [ - { - ...featuredPlugin, - name: "newest", - categories: ["gateway"], - featuredAt: 200, - stats: { downloads: 1 }, - }, - ], - nextCursor: null, - }); - + it("preserves the published Featured plugin order while filtering across categories", async () => { + fetchPluginCatalogMock.mockResolvedValue({ + items: [ + { ...featuredPlugin, name: "editorial", categories: ["tools"], featuredAt: 100 }, + { ...featuredPlugin, name: "excluded", categories: ["other"], featuredAt: 500 }, + { ...featuredPlugin, name: "telemetry", categories: ["gateway"], featuredAt: 200 }, + ], + nextCursor: null, + }); const result = await fetchHomePluginListing( "featured", ["tools", "gateway"], HOME_LISTING_PAGE_SIZE, ); - - expect(result.items.map((item) => item.name)).toEqual(["newest", "older"]); - expect(fetchPluginCatalogMock).toHaveBeenCalledTimes(2); - expect(fetchPluginCatalogMock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ category: "tools" }), - ); - expect(fetchPluginCatalogMock).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ category: "gateway" }), - ); + expect(result.items.map((item) => item.name)).toEqual(["editorial", "telemetry"]); + expect(result.hasMore).toBe(false); + expect(fetchPluginCatalogMock).toHaveBeenCalledTimes(1); }); }); diff --git a/src/lib/homeListingData.ts b/src/lib/homeListingData.ts index fee9eddd5f..cf74ac37ed 100644 --- a/src/lib/homeListingData.ts +++ b/src/lib/homeListingData.ts @@ -1,9 +1,10 @@ import { api } from "../../convex/_generated/api"; +import { FEATURED_CATALOG_SIZE } from "../../convex/lib/featuredPolicy"; import { convexHttp } from "../convex/client"; import { fetchCatalogDiscoveryCapabilities } from "./catalogDiscoveryCapabilities"; import { getSkillCategoriesForSkill } from "./categories"; import { fetchPluginCatalog, type PackageListItem } from "./packageApi"; -import type { PublicSkill, PublicUser } from "./publicUser"; +import type { PublicPublisher, PublicSkill, PublicUser } from "./publicUser"; import { fetchCanonicalTrendingPage, type CanonicalTrendingItem, @@ -17,7 +18,7 @@ export type { TrendingFeedState } from "./trendingApi"; export type HomeNativeSkillListingEntry = { skill: PublicSkill; ownerHandle?: string | null; - owner?: PublicUser | null; + owner?: PublicUser | PublicPublisher | null; }; type HomeTrendingSkillListingEntry = { @@ -66,8 +67,6 @@ import { DISCOVERY_RECENT_WINDOW_MS as HOME_NEW_WINDOW_MS } from "../../convex/l const PLUGIN_CATALOG_PAGE_LIMIT = 100; const LEGACY_NEW_PLUGIN_MAX_REQUESTS = 10; const TRENDING_SEARCH_PAGE_LIMIT = 100; -// Featured is intentionally a finite editorial feed: the latest 40 badge-history rows. -const FEATURED_SKILL_LIMIT = 40; export function homeListingCacheKey({ kind, @@ -235,12 +234,21 @@ export async function fetchHomeSkillListing( }; } - // highlightedOnly is a dedicated backend path ordered by skillBadges.by_kind_at; - // the nominal sort below is ignored for Featured and never chooses its candidate set. + if (tab === "featured") { + // Filter the finite published selection once, preserving order across categories. + const result = await convexHttp.query(api.skills.listPublicPageV4, { + numItems: FEATURED_CATALOG_SIZE, + highlightedOnly: true, + }); + const items = result.page.filter((entry) => + skillMatchesAnyHomeCategory(entry.skill, categorySlugs), + ); + return { page: items.slice(0, numItems), hasMore: items.length > numItems }; + } const capabilities = tab === "new" ? await fetchCatalogDiscoveryCapabilities() : { apiVersion: 1 as const }; const newestCutoff = Date.now() - HOME_NEW_WINDOW_MS; - const requestLimit = tab === "featured" ? FEATURED_SKILL_LIMIT : numItems; + const requestLimit = numItems; const categoriesToFetch = categorySlugs.length > 0 ? categorySlugs : [null]; const results = await Promise.all( categoriesToFetch.map(async (categorySlug) => { @@ -254,7 +262,6 @@ export async function fetchHomeSkillListing( numItems: requestLimit - page.length, sort: tab === "new" || tab === "official" ? "newest" : "updated", dir: "desc", - highlightedOnly: tab === "featured" ? true : undefined, officialOnly: tab === "official" ? true : undefined, ...(tab === "new" && capabilities.apiVersion >= 1 ? { createdAfter: newestCutoff } : {}), categorySlug: categorySlug ?? undefined, @@ -288,11 +295,6 @@ export async function fetchHomeSkillListing( ); const items = uniqueHomeSkillEntries(results.flatMap((result) => result.page)).sort( (left, right) => { - if (tab === "featured") { - return ( - (right.skill.badges?.highlighted?.at ?? 0) - (left.skill.badges?.highlighted?.at ?? 0) - ); - } if (tab === "new" || tab === "official") { return right.skill.createdAt - left.skill.createdAt; } @@ -301,11 +303,7 @@ export async function fetchHomeSkillListing( ); return { page: items.slice(0, numItems), - hasMore: - tab === "featured" - ? numItems < FEATURED_SKILL_LIMIT && - (items.length > numItems || results.some((result) => result.hasMore)) - : items.length > numItems || results.some((result) => result.hasMore), + hasMore: items.length > numItems || results.some((result) => result.hasMore), }; } @@ -315,6 +313,15 @@ export async function fetchHomePluginListing( limit: number, signal?: AbortSignal, ) { + if (tab === "featured") { + const result = await fetchPluginCatalog({ + featured: true, + limit: FEATURED_CATALOG_SIZE, + signal, + }); + const items = result.items.filter((item) => itemMatchesAnyHomeCategory(item, categorySlugs)); + return { items: items.slice(0, limit), hasMore: items.length > limit }; + } const categoriesToFetch = categorySlugs.length > 0 ? categorySlugs : [null]; const newestCutoff = Date.now() - HOME_NEW_WINDOW_MS; if (tab === "new") { @@ -417,7 +424,6 @@ export async function fetchHomePluginListing( const result = await fetchPluginCatalog({ category: categorySlug ?? undefined, cursor: cursor ?? undefined, - featured: tab === "featured" ? true : undefined, isOfficial: tab === "official" ? true : undefined, sort: "updated", limit: Math.min(limit - items.length, PLUGIN_CATALOG_PAGE_LIMIT), @@ -433,7 +439,6 @@ export async function fetchHomePluginListing( }), ); const items = uniqueHomePlugins(results.flatMap((result) => result.items)).sort((left, right) => { - if (tab === "featured") return (right.featuredAt ?? 0) - (left.featuredAt ?? 0); return right.updatedAt - left.updatedAt; }); return { diff --git a/src/styles.css b/src/styles.css index a7f1547d02..6207e6ccd1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -23463,6 +23463,9 @@ a.search-empty-action { } .home-v2-listing-row-by { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; font-family: var(--font-mono), ui-monospace, monospace; font-size: 12px; font-weight: 500;