8c41b33313
Features implemented across 6 batches: - Batch A: Fix ColorPicker gradient sync, tenant custom logo, unarchive cards, board backgrounds - Batch B: My Work view, card copy/duplicate, card numbering (BOARD-123) - Batch C: Due date reminders (cron), card watching with notifications - Batch D: Bulk card operations, column sorting, calendar view - Batch E: Invite by link, board import/export (Pounce + Trello JSON) - Batch F: Webhooks with HMAC signing, custom fields, automation rules engine Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
2.8 KiB
Svelte
101 lines
2.8 KiB
Svelte
<script lang="ts">
|
|
import { api } from '$lib/utils/api.js';
|
|
import { toast } from '$lib/stores/toasts.svelte.js';
|
|
|
|
type Props = {
|
|
boardId: string;
|
|
selectedCardIds: Set<string>;
|
|
columns: any[];
|
|
onaction: () => void;
|
|
oncancel: () => void;
|
|
};
|
|
|
|
let { boardId, selectedCardIds, columns, onaction, oncancel }: Props = $props();
|
|
|
|
let processing = $state(false);
|
|
let confirmDelete = $state(false);
|
|
|
|
async function bulkAction(action: string, extra: Record<string, any> = {}) {
|
|
processing = true;
|
|
const { ok, data } = await api(`/api/boards/${boardId}/cards/bulk`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
action,
|
|
cardIds: Array.from(selectedCardIds),
|
|
...extra
|
|
})
|
|
});
|
|
if (ok) {
|
|
toast.success(`${data.count} card(s) updated`);
|
|
onaction();
|
|
}
|
|
processing = false;
|
|
confirmDelete = false;
|
|
}
|
|
</script>
|
|
|
|
<div class="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 px-4 py-3 flex items-center gap-3 flex-wrap">
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{selectedCardIds.size} selected
|
|
</span>
|
|
|
|
<div class="h-5 w-px bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
<!-- Move to column -->
|
|
<select
|
|
onchange={(e) => {
|
|
const val = e.currentTarget.value;
|
|
if (val) bulkAction('move', { columnId: val });
|
|
e.currentTarget.value = '';
|
|
}}
|
|
disabled={processing}
|
|
class="text-sm rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 px-2 py-1"
|
|
>
|
|
<option value="">Move to...</option>
|
|
{#each columns as col}
|
|
<option value={col.id}>{col.title}</option>
|
|
{/each}
|
|
</select>
|
|
|
|
<button
|
|
onclick={() => bulkAction('archive')}
|
|
disabled={processing}
|
|
class="text-sm rounded bg-gray-100 dark:bg-gray-700 px-3 py-1.5 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 transition disabled:opacity-50"
|
|
>
|
|
Archive
|
|
</button>
|
|
|
|
{#if confirmDelete}
|
|
<button
|
|
onclick={() => bulkAction('delete')}
|
|
disabled={processing}
|
|
class="text-sm rounded bg-[var(--color-danger)] px-3 py-1.5 text-white hover:opacity-90 transition disabled:opacity-50"
|
|
>
|
|
Confirm Delete
|
|
</button>
|
|
<button
|
|
onclick={() => (confirmDelete = false)}
|
|
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
|
>
|
|
Cancel
|
|
</button>
|
|
{:else}
|
|
<button
|
|
onclick={() => (confirmDelete = true)}
|
|
disabled={processing}
|
|
class="text-sm rounded px-3 py-1.5 text-[var(--color-danger)] hover:bg-red-50 dark:hover:bg-red-900/30 transition disabled:opacity-50"
|
|
>
|
|
Delete
|
|
</button>
|
|
{/if}
|
|
|
|
<div class="h-5 w-px bg-gray-200 dark:bg-gray-700"></div>
|
|
|
|
<button
|
|
onclick={oncancel}
|
|
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition"
|
|
>
|
|
Done
|
|
</button>
|
|
</div>
|