mirror of
https://github.com/karakeep-app/karakeep.git
synced 2025-12-12 20:35:52 +01:00
feat: add icon prop to ActionButton for better loading UX
- Add optional icon prop to ActionButton component - When loading and icon is provided, spinner replaces icon while text remains - Improves UX by showing context during loading states - Update all ActionButton usages to pass icons explicitly - Maintains backwards compatibility for buttons without icons
This commit is contained in:
@@ -145,8 +145,8 @@ export default function AssetsSettingsPage() {
|
||||
{ onSettled: () => setDialogOpen(false) },
|
||||
)
|
||||
}
|
||||
icon={<Trash2 className="mr-2 size-4" />}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
{t("actions.delete")}
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
@@ -105,8 +105,8 @@ export default function BrokenLinksPage() {
|
||||
loading={isRecrawling}
|
||||
onClick={() => recrawlBookmark({ bookmarkId: b.id })}
|
||||
className="flex items-center gap-2"
|
||||
icon={<RefreshCw className="size-4" />}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
{t("actions.recrawl")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
@@ -114,8 +114,8 @@ export default function BrokenLinksPage() {
|
||||
onClick={() => deleteBookmark({ bookmarkId: b.id })}
|
||||
loading={isDeleting}
|
||||
className="flex items-center gap-2"
|
||||
icon={<Trash2 className="size-4" />}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
{t("actions.delete")}
|
||||
</ActionButton>
|
||||
</TableCell>
|
||||
|
||||
@@ -321,9 +321,8 @@ export default function BulkBookmarksAction() {
|
||||
variant="ghost"
|
||||
key={name}
|
||||
onClick={action}
|
||||
>
|
||||
{Icon}
|
||||
</ActionButtonWithTooltip>
|
||||
icon={Icon}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -156,9 +156,8 @@ export default function ManageListsModal({
|
||||
onClick={() =>
|
||||
deleteFromList({ bookmarkId, listId: list.id })
|
||||
}
|
||||
>
|
||||
<X className="size-4" />
|
||||
</ActionButton>
|
||||
icon={<X className="size-4" />}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -72,9 +72,8 @@ function AISummary({
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}
|
||||
loading={isResummarizing}
|
||||
onClick={() => resummarize({ bookmarkId })}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
</ActionButton>
|
||||
icon={<RefreshCw size={16} />}
|
||||
/>
|
||||
<ActionButton
|
||||
size="none"
|
||||
variant="none"
|
||||
@@ -85,9 +84,8 @@ function AISummary({
|
||||
onClick={() =>
|
||||
updateBookmark({ bookmarkId, summary: null })
|
||||
}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</ActionButton>
|
||||
icon={<Trash2 size={16} />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -83,9 +83,8 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
|
||||
favourited: !bookmark.favourited,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FavouritedActionIcon favourited={bookmark.favourited} />
|
||||
</ActionButton>
|
||||
icon={<FavouritedActionIcon favourited={bookmark.favourited} />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{bookmark.favourited
|
||||
@@ -105,9 +104,8 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
|
||||
archived: !bookmark.archived,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ArchivedActionIcon archived={bookmark.archived} />
|
||||
</ActionButton>
|
||||
icon={<ArchivedActionIcon archived={bookmark.archived} />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{bookmark.archived ? t("actions.unarchive") : t("actions.archive")}
|
||||
|
||||
@@ -121,8 +121,8 @@ export default function RuleList({
|
||||
deleteRule({ id: rule.id });
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
icon={<Trash2 className="mr-2 h-4 w-4" />}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{t("actions.delete")}
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
@@ -130,8 +130,8 @@ export function PromptEditor() {
|
||||
loading={isCreating}
|
||||
variant="default"
|
||||
className="items-center"
|
||||
icon={<Plus className="mr-2 size-4" />}
|
||||
>
|
||||
<Plus className="mr-2 size-4" />
|
||||
{t("actions.add")}
|
||||
</ActionButton>
|
||||
</form>
|
||||
@@ -253,8 +253,8 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
className="items-center"
|
||||
icon={<Save className="mr-2 size-4" />}
|
||||
>
|
||||
<Save className="mr-2 size-4" />
|
||||
{t("actions.save")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
@@ -263,8 +263,8 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
|
||||
onClick={() => deletePrompt({ promptId: prompt.id })}
|
||||
className="items-center"
|
||||
type="button"
|
||||
icon={<Trash2 className="mr-2 size-4" />}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
{t("actions.delete")}
|
||||
</ActionButton>
|
||||
</form>
|
||||
|
||||
@@ -196,8 +196,8 @@ function BackupConfigurationForm() {
|
||||
type="submit"
|
||||
loading={isUpdating}
|
||||
className="items-center"
|
||||
icon={<Save className="mr-2 size-4" />}
|
||||
>
|
||||
<Save className="mr-2 size-4" />
|
||||
{t("settings.backups.configuration.save_settings")}
|
||||
</ActionButton>
|
||||
</form>
|
||||
@@ -314,8 +314,8 @@ function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) {
|
||||
onClick={() => deleteBackup({ backupId: backup.id })}
|
||||
className="items-center"
|
||||
type="button"
|
||||
icon={<Trash2 className="mr-2 size-4" />}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
{t("settings.backups.list.actions.delete_backup")}
|
||||
</ActionButton>
|
||||
)}
|
||||
@@ -370,8 +370,8 @@ function BackupsList() {
|
||||
loading={isTriggering}
|
||||
variant="default"
|
||||
className="items-center"
|
||||
icon={<Play className="mr-2 size-4" />}
|
||||
>
|
||||
<Play className="mr-2 size-4" />
|
||||
{t("settings.backups.list.create_backup_now")}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
@@ -180,8 +180,8 @@ export function FeedsEditorDialog() {
|
||||
loading={isCreating}
|
||||
variant="default"
|
||||
className="items-center"
|
||||
icon={<Plus className="mr-2 size-4" />}
|
||||
>
|
||||
<Plus className="mr-2 size-4" />
|
||||
Add
|
||||
</ActionButton>
|
||||
</DialogFooter>
|
||||
@@ -328,8 +328,8 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
|
||||
})}
|
||||
type="submit"
|
||||
className="items-center"
|
||||
icon={<Save className="mr-2 size-4" />}
|
||||
>
|
||||
<Save className="mr-2 size-4" />
|
||||
{t("actions.save")}
|
||||
</ActionButton>
|
||||
</DialogFooter>
|
||||
@@ -424,9 +424,8 @@ export function FeedRow({ feed }: { feed: ZFeed }) {
|
||||
variant="ghost"
|
||||
className="items-center"
|
||||
onClick={() => fetchNow({ feedId: feed.id })}
|
||||
>
|
||||
<ArrowDownToLine className="size-4" />
|
||||
</ActionButton>
|
||||
icon={<ArrowDownToLine className="size-4" />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("actions.fetch_now")}</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -440,8 +439,8 @@ export function FeedRow({ feed }: { feed: ZFeed }) {
|
||||
onClick={() => deleteFeed({ feedId: feed.id })}
|
||||
className="items-center"
|
||||
type="button"
|
||||
icon={<Trash2 className="mr-2 size-4" />}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
{t("actions.delete")}
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
@@ -168,8 +168,8 @@ export function WebhooksEditorDialog() {
|
||||
loading={isCreating}
|
||||
variant="default"
|
||||
className="items-center"
|
||||
icon={<Plus className="mr-2 size-4" />}
|
||||
>
|
||||
<Plus className="mr-2 size-4" />
|
||||
Add
|
||||
</ActionButton>
|
||||
</DialogFooter>
|
||||
@@ -291,8 +291,8 @@ export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) {
|
||||
})}
|
||||
type="submit"
|
||||
className="items-center"
|
||||
icon={<Save className="mr-2 size-4" />}
|
||||
>
|
||||
<Save className="mr-2 size-4" />
|
||||
{t("actions.save")}
|
||||
</ActionButton>
|
||||
</DialogFooter>
|
||||
@@ -410,8 +410,8 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) {
|
||||
});
|
||||
})}
|
||||
className="items-center"
|
||||
icon={<Trash2 className="mr-2 size-4" />}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
{t("actions.delete")}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
@@ -421,8 +421,8 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) {
|
||||
})}
|
||||
type="submit"
|
||||
className="items-center"
|
||||
icon={<Save className="mr-2 size-4" />}
|
||||
>
|
||||
<Save className="mr-2 size-4" />
|
||||
{t("actions.save")}
|
||||
</ActionButton>
|
||||
</DialogFooter>
|
||||
@@ -462,8 +462,8 @@ export function WebhookRow({ webhook }: { webhook: ZWebhook }) {
|
||||
onClick={() => deleteWebhook({ webhookId: webhook.id })}
|
||||
className="items-center"
|
||||
type="button"
|
||||
icon={<Trash2 className="mr-2 size-4" />}
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
{t("actions.delete")}
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
@@ -15,11 +15,20 @@ interface ActionButtonProps extends ButtonProps {
|
||||
loading: boolean;
|
||||
spinner?: React.ReactNode;
|
||||
ignoreDemoMode?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
|
||||
(
|
||||
{ children, loading, spinner, disabled, ignoreDemoMode = false, ...props },
|
||||
{
|
||||
children,
|
||||
loading,
|
||||
spinner,
|
||||
disabled,
|
||||
ignoreDemoMode = false,
|
||||
icon,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const clientConfig = useClientConfig();
|
||||
@@ -31,9 +40,25 @@ const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
|
||||
} else if (loading) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
// Determine button content based on loading state and icon prop
|
||||
let content;
|
||||
if (icon) {
|
||||
// If icon is provided, show spinner instead of icon when loading, keep text
|
||||
content = (
|
||||
<>
|
||||
{loading ? spinner : icon}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// Fallback to old behavior: replace entire content with spinner when loading
|
||||
content = loading ? spinner : children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button ref={ref} {...props} disabled={disabled}>
|
||||
{loading ? spinner : children}
|
||||
{content}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -278,8 +278,8 @@ export default function ToolbarPlugin({
|
||||
onClick={() => {
|
||||
onSave?.();
|
||||
}}
|
||||
icon={<Save className="size-4" />}
|
||||
>
|
||||
<Save className="size-4" />
|
||||
Save
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user