Skip to content

Fixes #1747 Add Unused Tags Management to Cleanups Dashboard#2224

Open
sepej-osu wants to merge 2 commits intokarakeep-app:mainfrom
sepej-osu:issue-1747
Open

Fixes #1747 Add Unused Tags Management to Cleanups Dashboard#2224
sepej-osu wants to merge 2 commits intokarakeep-app:mainfrom
sepej-osu:issue-1747

Conversation

@sepej-osu
Copy link

@sepej-osu sepej-osu commented Dec 6, 2025

Fixes #1747

  • Removed the vertical separator and the number of tags the unused tag belongs to. Previously all Unused tags showed (Tag | 0)
  • Matched the style of the Cleanups dashboard with the Tags Dashboard. Duplicate tags are in a card, Unused Tags are in a separate card below it.
  • Moved hardcoded "DELETE ALL TAGS" confirmation into a locale so it can be translated.
  • Added a badge for the number of unused tags next to the Header Title. Did the same for the duplicate tags header.
Screenshot 2025-12-05 at 19-51-14 All Tags Karakeep Screenshot 2025-12-05 at 19-50-54 Karakeep

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 6, 2025

Walkthrough

Replaces the previous inline cleanups page with a lightweight page that renders a new client-side Cleanups component. Adds two new cleanup subcomponents (DuplicateTags, UnusedTags), renames TagDuplicationDetection → DuplicateTags, updates TagPill to support optional counts, and adds one i18n key.

Changes

Cohort / File(s) Summary
Page entry / wrapper
apps/web/app/dashboard/cleanups/page.tsx, apps/web/components/dashboard/cleanups/Cleanups.tsx
Page component simplified from an async/translation-driven render to a plain function that delegates UI to a new client-side Cleanups component which renders a header and includes the cleanup subcomponents.
Duplicate-tag detection UI
apps/web/components/dashboard/cleanups/DuplicateTags.tsx
Renames TagDuplicationDetectionDuplicateTags, refactors layout into Card components, adds badge showing suggestion count, and moves loading state inside the Card while preserving suggestion rendering and apply-all controls.
Unused-tags management
apps/web/components/dashboard/cleanups/UnusedTags.tsx
New UnusedTags component: paginated/searchable fetch of unused tags (limit 50), per-tag delete dialog, "delete all unused tags" action with toasts, load-more support, optional card wrapper and showCount prop. Uses internal modal state and i18n strings.
All-tags view adjustments
apps/web/components/dashboard/tags/AllTagsView.tsx
Removes previous explicit unused-tags handling and UI; now delegates unused tags display to the new UnusedTags component and simplifies fetching/derived-visible-tags logic by dropping empty-tags handling.
Tag pill UI
apps/web/components/dashboard/tags/TagPill.tsx
Adds optional showCount?: boolean prop (defaults to true) and renders tag count conditionally, adjusting layout when count is hidden.
i18n
apps/web/lib/i18n/locales/en/translation.json
Adds tags.delete_all_unused_tags_button with value "Delete All".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review focus:
    • apps/web/components/dashboard/cleanups/UnusedTags.tsx — pagination, fetch params, delete-all flow, and toast/error handling.
    • DuplicateTags.tsx — card layout changes, conditional badge logic, and loading state placement.
    • Cross-check usages of TagPill after adding showCount to ensure no visual regressions.
    • AllTagsView.tsx — ensure derived tag lists and effects remain correct after removing empty-tags logic.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding unused tags management to the Cleanups dashboard, addressing issue #1747.
Description check ✅ Passed The description clearly relates to the changeset, detailing UI improvements including badge additions, styling updates, and component refactoring with supporting screenshots.
Linked Issues check ✅ Passed The pull request successfully implements the core objective from issue #1747: adding unused tags management to the Cleanups menu alongside merge tag suggestions in a unified UI area.
Out of Scope Changes check ✅ Passed All changes are scoped to the unused tags feature implementation and related UI unification: new UnusedTags component, DuplicateTags refactoring, i18n additions, and minor TagPill enhancements are all directly supporting the feature objective.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sepej-osu sepej-osu changed the title feat: Add Unused Tags Management to Cleanups Dashboard Fixes #1747 Add Unused Tags Management to Cleanups Dashboard Dec 6, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/web/components/dashboard/cleanups/DuplicateTags.tsx (1)

208-307: Card-based DuplicateTags UI is solid; consider i18n + immutability tweaks

The new Card layout, badge, and collapsible table integrate well and keep the UX clear. Two small follow-ups to consider:

  • The header and helper copy ("Duplicate Tags", "Merge similar tags to keep your collection organized", "You have X suggestions for tag merging.", "Hide All"/"Show All") are hardcoded strings. Since you already have cleanups.cleanups and cleanups.duplicate_tags.title in the locale file, it would be cleaner to use useTranslation here as well so this section is fully localizable.
  • In the useEffect, allTags.tags.sort(...) mutates the array returned from api.tags.list.useQuery. To avoid side effects on the cached query data, consider cloning first, e.g. const sortedTags = [...(allTags?.tags ?? [])].sort(...).
apps/web/components/dashboard/cleanups/Cleanups.tsx (1)

6-16: Cleanups wrapper is fine; consider localizing the title

The layout composition looks good. To stay consistent with the rest of the app, you might want to:

  • Use useTranslation and t("cleanups.cleanups") instead of the hardcoded "Cleanups" string.
  • Optionally switch the <span className="text-2xl"> to an actual heading element (e.g. <h1>) for better semantics and a11y.
apps/web/app/dashboard/cleanups/page.tsx (1)

1-4: Page wrapper is good; async can be dropped

Routing the page through the shared Cleanups component keeps things tidy. Since CleanupsPage doesn’t await anything, you can drop the async keyword to reduce mental overhead, but it’s optional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de98873 and 7fd0f39.

📒 Files selected for processing (7)
  • apps/web/app/dashboard/cleanups/page.tsx (1 hunks)
  • apps/web/components/dashboard/cleanups/Cleanups.tsx (1 hunks)
  • apps/web/components/dashboard/cleanups/DuplicateTags.tsx (4 hunks)
  • apps/web/components/dashboard/cleanups/UnusedTags.tsx (1 hunks)
  • apps/web/components/dashboard/tags/AllTagsView.tsx (3 hunks)
  • apps/web/components/dashboard/tags/TagPill.tsx (2 hunks)
  • apps/web/lib/i18n/locales/en/translation.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript for type safety in all source files

Files:

  • apps/web/components/dashboard/cleanups/Cleanups.tsx
  • apps/web/components/dashboard/tags/TagPill.tsx
  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
  • apps/web/app/dashboard/cleanups/page.tsx
  • apps/web/components/dashboard/cleanups/DuplicateTags.tsx
  • apps/web/components/dashboard/tags/AllTagsView.tsx
**/*.{ts,tsx,js,jsx,json,css,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier according to project standards

Files:

  • apps/web/components/dashboard/cleanups/Cleanups.tsx
  • apps/web/lib/i18n/locales/en/translation.json
  • apps/web/components/dashboard/tags/TagPill.tsx
  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
  • apps/web/app/dashboard/cleanups/page.tsx
  • apps/web/components/dashboard/cleanups/DuplicateTags.tsx
  • apps/web/components/dashboard/tags/AllTagsView.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Lint code using oxlint and fix issues with pnpm lint:fix

Files:

  • apps/web/components/dashboard/cleanups/Cleanups.tsx
  • apps/web/components/dashboard/tags/TagPill.tsx
  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
  • apps/web/app/dashboard/cleanups/page.tsx
  • apps/web/components/dashboard/cleanups/DuplicateTags.tsx
  • apps/web/components/dashboard/tags/AllTagsView.tsx
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Use Tailwind CSS for styling in the web application
Use Next.js for building the main web application

Files:

  • apps/web/components/dashboard/cleanups/Cleanups.tsx
  • apps/web/components/dashboard/tags/TagPill.tsx
  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
  • apps/web/app/dashboard/cleanups/page.tsx
  • apps/web/components/dashboard/cleanups/DuplicateTags.tsx
  • apps/web/components/dashboard/tags/AllTagsView.tsx
🧬 Code graph analysis (5)
apps/web/components/dashboard/tags/TagPill.tsx (1)
apps/web/components/ui/separator.tsx (1)
  • Separator (30-30)
apps/web/components/dashboard/cleanups/UnusedTags.tsx (7)
packages/shared-react/hooks/tags.ts (2)
  • useDeleteUnusedTags (106-118)
  • usePaginatedSearchTags (7-18)
apps/web/components/ui/use-toast.ts (1)
  • toast (188-188)
apps/web/components/ui/action-confirming-dialog.tsx (1)
  • ActionConfirmingDialog (15-55)
packages/shared/types/tags.ts (1)
  • ZTagBasic (42-42)
apps/web/components/dashboard/tags/DeleteTagConfirmationDialog.tsx (1)
  • DeleteTagConfirmationDialog (8-58)
apps/web/components/dashboard/tags/TagPill.tsx (1)
  • TagPill (12-128)
apps/web/components/ui/card.tsx (5)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardDescription (84-84)
  • CardContent (85-85)
apps/web/app/dashboard/cleanups/page.tsx (1)
apps/web/components/dashboard/cleanups/Cleanups.tsx (1)
  • Cleanups (6-18)
apps/web/components/dashboard/cleanups/DuplicateTags.tsx (3)
apps/web/components/ui/card.tsx (5)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardDescription (84-84)
  • CardContent (85-85)
apps/web/components/ui/collapsible.tsx (3)
  • Collapsible (50-50)
  • CollapsibleTrigger (51-51)
  • CollapsibleContent (52-52)
apps/web/components/ui/table.tsx (5)
  • Table (108-108)
  • TableHeader (109-109)
  • TableRow (113-113)
  • TableHead (112-112)
  • TableBody (110-110)
apps/web/components/dashboard/tags/AllTagsView.tsx (1)
apps/web/components/dashboard/cleanups/UnusedTags.tsx (1)
  • UnusedTags (72-176)
🔇 Additional comments (3)
apps/web/lib/i18n/locales/en/translation.json (1)

605-629: New delete-all unused tags key looks consistent

tags.delete_all_unused_tags_button fits the existing naming pattern and matches the button usage in UnusedTags; no issues from an i18n perspective.

apps/web/components/dashboard/tags/TagPill.tsx (1)

12-26: TagPill showCount prop is backward compatible and clear

The new showCount prop (defaulting to true) cleanly gates the separator and count without impacting existing callers. This is a good, minimal API extension.

Also applies to: 91-96

apps/web/components/dashboard/tags/AllTagsView.tsx (1)

32-36: UnusedTags in AllTagsView ignores search/sort; confirm if that’s intentional

With the refactor, the top-level searchQuery and sortBy are applied only to human and AI tags; the UnusedTags component uses its own usePaginatedSearchTags with nameContains: "" and fixed sortBy: "usage". That means the search box and sort dropdown no longer affect the “Unused tags” section in this view.

If that’s not intended, consider threading searchQuery and sortBy into UnusedTags as props and using them in its internal query, so all three sections react consistently to the filters.

Also applies to: 101-117, 335-335

Comment on lines +29 to +65
function DeleteAllUnusedTags({ numUnusedTags }: { numUnusedTags: number }) {
const { t } = useTranslation();
const { mutate, isPending } = useDeleteUnusedTags({
onSuccess: () => {
toast({
description: `Deleted all ${numUnusedTags} unused tags`,
});
},
onError: () => {
toast({
description: "Something went wrong",
variant: "destructive",
});
},
});
return (
<ActionConfirmingDialog
title={t("tags.delete_all_unused_tags")}
description={`Are you sure you want to delete the ${numUnusedTags} unused tags?`}
actionButton={() => (
<ActionButton
variant="destructive"
loading={isPending}
onClick={() => mutate()}
>
<Trash2 className="mr-2 size-4" />
{t("tags.delete_all_unused_tags_button")}
</ActionButton>
)}
>
<Button variant="destructive" disabled={numUnusedTags == 0}>
<Trash2 className="mr-2 size-4" />
{t("tags.delete_all_unused_tags")}
</Button>
</ActionConfirmingDialog>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Delete-all dialog should close on success and text may understate what’s deleted

The delete-all behavior works functionally, but two details are worth tightening:

  • The confirmation dialog never closes automatically after a successful mutation because setDialogOpen(false) isn’t called. This differs from DeleteTagConfirmationDialog and forces users to hit “Close” manually. You can mirror the other pattern by wiring setDialogOpen into the mutation:
-    <ActionConfirmingDialog
+    <ActionConfirmingDialog
       title={t("tags.delete_all_unused_tags")}
       description={`Are you sure you want to delete the ${numUnusedTags} unused tags?`}
-      actionButton={() => (
+      actionButton={(setDialogOpen) => (
         <ActionButton
           variant="destructive"
           loading={isPending}
-          onClick={() => mutate()}
+          onClick={() =>
+            mutate(undefined, {
+              onSuccess: () => setDialogOpen(false),
+            })
+          }
         >
           <Trash2 className="mr-2 size-4" />
           {t("tags.delete_all_unused_tags_button")}
         </ActionButton>
       )}
     >
  • numUnusedTags is the count of tags currently loaded in the list, but useDeleteUnusedTags likely deletes all unused tags on the backend. The confirmation text and success toast (“Deleted all X unused tags”) may therefore under-report what’s actually being deleted if there are more pages. Consider either:
    • deriving the true deleted count from the mutation response, or
    • rewording the copy to something like “Delete all unused tags?” without echoing the possibly partial client-side count.

Comment on lines 72 to 175
export function UnusedTags({
showAsCard = true,
showCount = true,
}: UnusedTagsProps) {
const { t } = useTranslation();
const [selectedTag, setSelectedTag] = useState<ZTagBasic | null>(null);
const isDialogOpen = !!selectedTag;

const {
data: unusedTagsData,
isLoading: isUnusedTagsLoading,
hasNextPage: hasNextPageUnusedTags,
fetchNextPage: fetchNextPageUnusedTags,
isFetchingNextPage: isFetchingNextPageUnusedTags,
} = usePaginatedSearchTags({
nameContains: "",
sortBy: "usage",
attachedBy: "none",
limit: 50,
});

const unusedTags = unusedTagsData?.tags ?? [];
const numUnusedTags = unusedTags.length;

const handleOpenDialog = (tag: ZTagBasic) => {
setSelectedTag(tag);
};

const content = (
<>
{selectedTag && (
<DeleteTagConfirmationDialog
tag={selectedTag}
open={isDialogOpen}
setOpen={(o) => {
if (!o) {
setSelectedTag(null);
}
}}
/>
)}
{isUnusedTagsLoading && unusedTags.length === 0 ? (
<div className="flex justify-center py-8">
<LoadingSpinner />
</div>
) : (
<>
{unusedTags.length > 0 && (
<div className="flex flex-wrap gap-3">
{unusedTags.map((tag) => (
<TagPill
key={tag.id}
id={tag.id}
name={tag.name}
count={showCount ? tag.numBookmarks : 0}
isDraggable={false}
onOpenDialog={handleOpenDialog}
showCount={false}
/>
))}
</div>
)}
{unusedTags.length === 0 && (
<p className="py-4 text-center text-sm text-muted-foreground">
{t("tags.no_unused_tags")}
</p>
)}
{hasNextPageUnusedTags && (
<div className="mt-4">
<ActionButton
variant="secondary"
onClick={() => fetchNextPageUnusedTags()}
loading={isFetchingNextPageUnusedTags}
ignoreDemoMode
>
{t("actions.load_more")}
</ActionButton>
</div>
)}
{numUnusedTags > 0 && (
<div className="mt-4">
<DeleteAllUnusedTags numUnusedTags={numUnusedTags} />
</div>
)}
</>
)}
</>
);
if (!showAsCard) {
return content;
}

return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>{t("tags.unused_tags")}</span>
<Badge variant="secondary">{numUnusedTags}</Badge>
</CardTitle>
<CardDescription>{t("tags.unused_tags_info")}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">{content}</CardContent>
</Card>
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

UnusedTags showCount prop doesn’t currently change the UI

UnusedTagsProps exposes showCount, but inside the TagPill mapping you have:

<TagPill
  key={tag.id}
  id={tag.id}
  name={tag.name}
  count={showCount ? tag.numBookmarks : 0}
  isDraggable={false}
  onOpenDialog={handleOpenDialog}
  showCount={false}
/>

Because showCount is always forced to false when passed to TagPill, the count value is never actually rendered. From a consumer’s perspective, toggling showCount on UnusedTags has no visible effect.

If you want UnusedTags to truly control whether per-pill counts are shown, a cleaner wiring would be:

-                <TagPill
-                  key={tag.id}
-                  id={tag.id}
-                  name={tag.name}
-                  count={showCount ? tag.numBookmarks : 0}
-                  isDraggable={false}
-                  onOpenDialog={handleOpenDialog}
-                  showCount={false}
-                />
+                <TagPill
+                  key={tag.id}
+                  id={tag.id}
+                  name={tag.name}
+                  count={tag.numBookmarks}
+                  isDraggable={false}
+                  onOpenDialog={handleOpenDialog}
+                  showCount={showCount}
+                />

If the product decision is to never show per-pill counts for unused tags, you could instead drop the showCount prop from UnusedTags altogether and hardcode showCount={false} to avoid a misleading API.

🤖 Prompt for AI Agents
In apps/web/components/dashboard/cleanups/UnusedTags.tsx around lines 72 to 175,
the component accepts a showCount prop but always passes showCount={false} into
TagPill so the per-tag count never renders; change the TagPill usage to forward
the component's showCount (i.e., showCount={showCount}) so the count prop is
honored, or if the decision is to never display counts remove the showCount prop
from UnusedTags and hardcode TagPill to showCount={false} and update the
component signature and usages accordingly.

@MohamedBassem
Copy link
Collaborator

@codex review

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/web/components/dashboard/cleanups/UnusedTags.tsx (2)

44-57: Dialog should close automatically on successful deletion.

The actionButton callback receives setDialogOpen as a parameter (per ActionConfirmingDialog's signature), but it's not being used here. After a successful mutation, users must manually close the dialog.

     <ActionConfirmingDialog
       title={t("tags.delete_all_unused_tags")}
       description={`Are you sure you want to delete the ${numUnusedTags} unused tags?`}
-      actionButton={() => (
+      actionButton={(setDialogOpen) => (
         <ActionButton
           variant="destructive"
           loading={isPending}
-          onClick={() => mutate()}
+          onClick={() =>
+            mutate(undefined, {
+              onSuccess: () => setDialogOpen(false),
+            })
+          }
         >
           <Trash2 className="mr-2 size-4" />
           {t("tags.delete_all_unused_tags_button")}
         </ActionButton>
       )}
     >

130-138: showCount prop is not forwarded to TagPill.

The UnusedTags component accepts a showCount prop (defaulting to true), but TagPill always receives showCount={false}. This makes the prop ineffective—toggling it has no visible effect.

Either forward the prop consistently:

                   <TagPill
                     key={tag.id}
                     id={tag.id}
                     name={tag.name}
-                    count={showCount ? tag.numBookmarks : 0}
+                    count={tag.numBookmarks}
                     isDraggable={false}
                     onOpenDialog={handleOpenDialog}
-                    showCount={false}
+                    showCount={showCount}
                   />

Or remove the unused showCount prop from UnusedTagsProps if the design intent is to never show counts for unused tags.

🧹 Nitpick comments (1)
apps/web/components/dashboard/cleanups/UnusedTags.tsx (1)

93-94: Consider displaying total count instead of loaded count.

numUnusedTags reflects only the currently loaded tags (up to 50 per page), not the total unused tags on the server. The badge and delete-all confirmation may show a lower number than what actually exists if pagination is involved. Consider fetching or deriving a total count from the API response if available.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fd0f39 and b17857f.

📒 Files selected for processing (5)
  • apps/web/app/dashboard/cleanups/page.tsx (1 hunks)
  • apps/web/components/dashboard/cleanups/Cleanups.tsx (1 hunks)
  • apps/web/components/dashboard/cleanups/UnusedTags.tsx (1 hunks)
  • apps/web/components/dashboard/tags/AllTagsView.tsx (3 hunks)
  • apps/web/lib/i18n/locales/en/translation.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/components/dashboard/cleanups/Cleanups.tsx
  • apps/web/app/dashboard/cleanups/page.tsx
  • apps/web/lib/i18n/locales/en/translation.json
  • apps/web/components/dashboard/tags/AllTagsView.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript for type safety in all source files

Files:

  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
**/*.{ts,tsx,js,jsx,json,css,md}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier according to project standards

Files:

  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Lint code using oxlint and fix issues with pnpm lint:fix

Files:

  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/**/*.{ts,tsx}: Use Tailwind CSS for styling in the web application
Use Next.js for building the main web application

Files:

  • apps/web/components/dashboard/cleanups/UnusedTags.tsx
🧬 Code graph analysis (1)
apps/web/components/dashboard/cleanups/UnusedTags.tsx (6)
packages/shared-react/hooks/tags.ts (2)
  • useDeleteUnusedTags (106-118)
  • usePaginatedSearchTags (7-18)
apps/web/components/ui/action-confirming-dialog.tsx (1)
  • ActionConfirmingDialog (15-55)
packages/shared/types/tags.ts (1)
  • ZTagBasic (42-42)
apps/web/components/ui/card.tsx (5)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardDescription (84-84)
  • CardContent (85-85)
apps/web/components/dashboard/tags/DeleteTagConfirmationDialog.tsx (1)
  • DeleteTagConfirmationDialog (8-58)
apps/web/components/dashboard/tags/TagPill.tsx (1)
  • TagPill (12-128)
🔇 Additional comments (4)
apps/web/components/dashboard/cleanups/UnusedTags.tsx (4)

1-28: LGTM!

Imports are well-organized with proper type-only import for ZTagBasic and appropriate separation of UI components, hooks, and local modules.


110-120: LGTM!

The controlled dialog pattern with selectedTag state is clean. Setting selectedTag to null when the dialog closes properly resets the state.


121-146: LGTM!

Loading and empty states are properly handled. The spinner shows only during initial load (when no data exists yet), and the empty message uses proper i18n.


147-163: LGTM!

Pagination is properly implemented with loading state handling. The delete-all button is correctly hidden when there are no unused tags.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 110 to 114
useEffect(() => {
const allTags = [...allHumanTags, ...allAiTags, ...allEmptyTags];
const allTags = [...allHumanTags, ...allAiTags];
setVisibleTagIds(allTags.map((tag) => tag.id) ?? []);
return () => {
setVisibleTagIds([]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-enable bulk selection for unused tags

The bulk-edit store now only registers human and AI tags (setVisibleTagIds is populated from those arrays), while unused tags are rendered separately via UnusedTags with plain TagPill rows. When bulk edit is enabled or “Select all” is pressed on the All Tags page, unused tags are no longer selectable or included in bulk delete/merge operations—a regression from the previous implementation where unused tags participated in bulk actions.

Useful? React with 👍 / 👎.

@MohamedBassem
Copy link
Collaborator

Thanks a lot for the PR, looks good to me. I did some minor changes, but before merging, we also need to address codex's comment as bulk editing no longer works for unused tags.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Clean Unused Tags to the Cleanups menu

2 participants